JavaScript越来越多地出现在我们客户端开发的视野中,从ReactNative到JSpatch,JavaScript与客户端相结合的技术开始变得魅力无穷。本文主要讲解iOS中的JavaScriptCore框架,正是它为iOS提供了执行JavaScript代码的能力。未来的技术日新月异,JavaScript与iOS正在碰撞出新的激情。
JavaScriptCore是JavaScript的虚拟机,为JavaScript的执行提供底层资源。
一、JavaScript
在讨论JavaScriptCore之前,我们首先必须对JavaScript有所了解。
1. JavaScript干啥的?
说的高大上一点:一门基于原型、函数先行的高级编程语言,通过解释执行,是动态类型的直译语言。是一门多范式的语言,它支持面向对象编程,命令式编程,以及函数式编程。
说的通俗一点:主要用于网页,为其提供动态交互的能力。可嵌入动态文本于HTML页面,对浏览器事件作出响应,读写HTML元素,控制cookies等。
再通俗一点:抢月饼,button.click()。(PS:请谨慎使用while循环)
2. JavaScript起源与历史
1990年底,欧洲核能研究组织(CERN)科学家Tim Berners-Lee,在互联网的基础上,发明了万维网(World Wide Web),从此可以在网上浏览网页文件。
1994年12月,Netscape 发布了一款面向普通用户的新一代的浏览器Navigator 1.0版,市场份额一举超过90%。
1995年,Netscape公司雇佣了程序员Brendan Eich开发这种嵌入网页的脚本语言。最初名字叫做Mocha,1995年9月改为LiveScript。
1995年12月,Netscape公司与Sun公司达成协议,后者允许将这种语言叫做JavaScript。
3. JavaScript与ECMAScript
“JavaScript”是Sun公司的注册商标,用来特制网景(现在的Mozilla)对于这门语言的实现。网景将这门语言作为标准提交给了ECMA——欧洲计算机制造协会。由于商标上的冲突,这门语言的标准版本改了一个丑陋的名字“ECMAScript”。同样由于商标的冲突,微软对这门语言的实现版本取了一个广为人知的名字“Jscript”。
ECMAScript作为JavaScript的标准,一般认为后者是前者的实现。
4. Java和JavaScript
《雷锋和雷峰塔》
Java 和 JavaScript 是两门不同的编程语言
一般认为,当时 Netscape 之所以将 LiveScript 命名为 JavaScript,是因为 Java 是当时最流行的编程语言,带有 “Java” 的名字有助于这门新生语言的传播。
二、 JavaScriptCore
1. 浏览器演进
演进完整图
https://upload.wikimedia.org/wikipedia/commons/7/74/Timeline_of_web_browsers.svg
WebKit分支
现在使用WebKit的主要两个浏览器Sfari和Chromium(Chorme的开源项目)。WebKit起源于KDE的开源项目Konqueror的分支,由苹果公司用于Sfari浏览器。其一条分支发展成为Chorme的内核,2013年Google在此基础上开发了新的Blink内核。
2. WebKit排版引擎
webkit是sfari、chrome等浏览器的排版引擎,各部分架构图如下
webkit Embedding API是browser UI与webpage进行交互的api接口;
platformAPI提供与底层驱动的交互, 如网络, 字体渲染, 影音文件解码, 渲染引擎等;
WebCore它实现了对文档的模型化,包括了CSS, DOM, Render等的实现;
JSCore是专门处理JavaScript脚本的引擎;
3. JavaScript引擎
JavaScript引擎是专门处理JavaScript脚本的虚拟机,一般会附带在网页浏览器之中。第一个JavaScript引擎由布兰登·艾克在网景公司开发,用于Netscape Navigator网页浏览器中。JavaScriptCore就是一个JavaScript引擎。
下图是当前主要的还在开发中的JavaScript引擎
4. JavaScriptCore组成
JavaScriptCore主要由以下模块组成:
Lexer 词法分析器,将脚本源码分解成一系列的Token
Parser 语法分析器,处理Token并生成相应的语法树
LLInt 低级解释器,执行Parser生成的二进制代码
Baseline JIT 基线JIT(just in time 实施编译)
DFG 低延迟优化的JIT
FTL 高通量优化的JIT
关于更多JavaScriptCore的实现细节,参考https://trac.webkit.org/wiki/JavaScriptCore
5. JavaScriptCore
JavaScriptCore是一个C++实现的开源项目。使用Apple提供的JavaScriptCore框架,你可以在Objective-C或者基于C的程序中执行Javascript代码,也可以向JavaScript环境中插入一些自定义的对象。JavaScriptCore从iOS 7.0之后可以直接使用。
在JavaScriptCore.h中,我们可以看到这个
#ifndef JavaScriptCore_h#define JavaScriptCore_h#include#include#ifdefined(__OBJC__)&&JSC_OBJC_API_ENABLED#import"JSContext.h"#import"JSValue.h"#import"JSManagedValue.h"#import"JSVirtualMachine.h"#import"JSExport.h"#endif#endif/* JavaScriptCore_h */
这里已经很清晰地列出了JavaScriptCore的主要几个类:
JSContext
JSValue
JSManagedValue
JSVirtualMachine
JSExport
接下来我们会依次讲解这几个类的用法。
6. Hello World!
这段代码展示了如何在Objective-C中执行一段JavaScript代码,并且获取返回值并转换成OC数据打印
//创建虚拟机JSVirtualMachine*vm=[[JSVirtualMachine alloc]init];//创建上下文JSContext*context=[[JSContext alloc]initWithVirtualMachine:vm];//执行JavaScript代码并获取返回值JSValue*value=[context evaluateScript:@"1+2*3"];//转换成OC数据并打印NSLog(@"value = %d",[value toInt32]);Outputvalue=7
三、 JSVirtualMachine
一个JSVirtualMachine的实例就是一个完整独立的JavaScript的执行环境,为JavaScript的执行提供底层资源。
这个类主要用来做两件事情:
实现并发的JavaScript执行
JavaScript和Objective-C桥接对象的内存管理
看下头文件SVirtualMachine.h里有什么:
NS_CLASS_AVAILABLE(10_9,7_0)@interfaceJSVirtualMachine:NSObject/* 创建一个新的完全独立的虚拟机 */(instancetype)init;/* 对桥接对象进行内存管理 */-(void)addManagedReference:(id)object withOwner:(id)owner;/* 取消对桥接对象的内存管理 */-(void)removeManagedReference:(id)object withOwner:(id)owner;@end
每一个JavaScript上下文(JSContext对象)都归属于一个虚拟机(JSVirtualMachine)。每个虚拟机可以包含多个不同的上下文,并允许在这些不同的上下文之间传值(JSValue对象)。
然而,每个虚拟机都是完整且独立的,有其独立的堆空间和垃圾回收器(garbage collector ),GC无法处理别的虚拟机堆中的对象,因此你不能把一个虚拟机中创建的值传给另一个虚拟机。
线程和JavaScript的并发执行
JavaScriptCore API都是线程安全的。你可以在任意线程创建JSValue或者执行JS代码,然而,所有其他想要使用该虚拟机的线程都要等待。
如果想并发执行JS,需要使用多个不同的虚拟机来实现。
可以在子线程中执行JS代码。
通过下面这个demo来理解一下这个并发机制
JSContext*context=[[CustomJSContext alloc]init];JSContext*context1=[[CustomJSContext alloc]init];JSContext*context2=[[CustomJSContext alloc]initWithVirtualMachine:[context virtualMachine]];NSLog(@"start");dispatch_async(queue,^{while(true){sleep(1);[context evaluateScript:@"log('tick')"];}});dispatch_async(queue1,^{while(true){sleep(1);[context1 evaluateScript:@"log('tick_1')"];}});dispatch_async(queue2,^{while(true){sleep(1);[context2 evaluateScript:@"log('tick_2')"];}});[context evaluateScript:@"sleep(5)"];NSLog(@"end");
context和context2属于同一个虚拟机。
context1属于另一个虚拟机。
三个线程分别异步执行每秒1次的js log,首先会休眠1秒。
在context上执行一个休眠5秒的JS函数。
首先执行的应该是休眠5秒的JS函数,在此期间,context所处的虚拟机上的其他调用都会处于等待状态,因此tick和tick_2在前5秒都不会有执行。
而context1所处的虚拟机仍然可以正常执行tick_1。
休眠5秒结束后,tick和tick_2才会开始执行(不保证先后顺序)。
实际运行输出的log是:
start
tick_1
tick_1
tick_1
tick_1
end
tick
tick_2
四、 JSContext
一个JSContext对象代表一个JavaScript执行环境。在native代码中,使用JSContext去执行JS代码,访问JS中定义或者计算的值,并使JavaScript可以访问native的对象、方法、函数。
1. JSContext执行JS代码
调用evaluateScript函数可以执行一段top-level的JS代码,并可向global对象添加函数和对象定义
其返回值是JavaScript代码中最后一个生成的值
API Reference
NS_CLASS_AVAILABLE(10_9,7_0)@interfaceJSContext:NSObject/* 创建一个JSContext,同时会创建一个新的JSVirtualMachine */(instancetype)init;/* 在指定虚拟机上创建一个JSContext */(instancetype)initWithVirtualMachine:(JSVirtualMachine*)virtualMachine;/* 执行一段JS代码,返回最后生成的一个值 */(JSValue*)evaluateScript:(NSString*)script;/* 执行一段JS代码,并将sourceURL认作其源码URL(仅作标记用) */-(JSValue*)evaluateScript:(NSString*)script withSourceURL:(NSURL*)sourceURLNS_AVAILABLE(10_10,8_0);/* 获取当前执行的JavaScript代码的context */+(JSContext*)currentContext;/* 获取当前执行的JavaScript function*/+(JSValue*)currentCalleeNS_AVAILABLE(10_10,8_0);/* 获取当前执行的JavaScript代码的this */+(JSValue*)currentThis;/* Returns the arguments to the current native callback from JavaScript code.*/+(NSArray*)currentArguments;/* 获取当前context的全局对象。WebKit中的context返回的便是WindowProxy对象*/@property(readonly,strong)JSValue*globalObject;@property(strong)JSValue*exception;@property(copy)void(^exceptionHandler)(JSContext*context,JSValue*exception);@property(readonly,strong)JSVirtualMachine*virtualMachine;@property(copy)NSString*nameNS_AVAILABLE(10_10,8_0);@end
2. JSContext访问JS对象
一个JSContext对象对应了一个全局对象(global object)。例如web浏览器中中的JSContext,其全局对象就是window对象。在其他环境中,全局对象也承担了类似的角色,用来区分不同的JavaScript context的作用域。全局变量是全局对象的属性,可以通过JSValue对象或者context下标的方式来访问。
一言不合上代码:
JSValue*value=[context evaluateScript:@"var a = 1+2*3;"];NSLog(@"a = %@",[context objectForKeyedSubscript:@"a"]);NSLog(@"a = %@",[context.globalObject objectForKeyedSubscript:@"a"]);NSLog(@"a = %@",context[@"a"]);/Output:a=7a=7a=7
这里列出了三种访问JavaScript对象的方法
通过context的实例方法objectForKeyedSubscript
通过context.globalObject的objectForKeyedSubscript实例方法
通过下标方式
设置属性也是对应的。
API Reference
/* 为JSContext提供下标访问元素的方式 */@interfaceJSContext(SubscriptSupport)/* 首先将key转为JSValue对象,然后使用这个值在JavaScript context的全局对象中查找这个名字的属性并返回 */(JSValue*)objectForKeyedSubscript:(id)key;/* 首先将key转为JSValue对象,然后用这个值在JavaScript context的全局对象中设置这个属性。
可使用这个方法将native中的对象或者方法桥接给JavaScript调用 */(void)setObject:(id)object forKeyedSubscript:(NSObject*)key;@end/* 例如:以下代码在JavaScript中创建了一个实现是Objective-C block的function */context[@"makeNSColor"]=^(NSDictionary*rgb){float r=[rgb[@"red"]floatValue];float g=[rgb[@"green"]floatValue];float b=[rgb[@"blue"]floatValue];return[NSColor colorWithRed:(r/255.f)green:(g/255.f)blue:(b/255.f)alpha:1.0];};JSValue*value=[context evaluateScript:@"makeNSColor({red:12, green:23, blue:67})"];
五、 JSValue
一个JSValue实例就是一个JavaScript值的引用。使用JSValue类在JavaScript和native代码之间转换一些基本类型的数据(比如数值和字符串)。你也可以使用这个类去创建包装了自定义类的native对象的JavaScript对象,或者创建由native方法或者block实现的JavaScript函数。
每个JSValue实例都来源于一个代表JavaScript执行环境的JSContext对象,这个执行环境就包含了这个JSValue对应的值。每个JSValue对象都持有其JSContext对象的强引用,只要有任何一个与特定JSContext关联的JSValue被持有(retain),这个JSContext就会一直存活。通过调用JSValue的实例方法返回的其他的JSValue对象都属于与最始的JSValue相同的JSContext。
每个JSValue都通过其JSContext间接关联了一个特定的代表执行资源基础的JSVirtualMachine对象。你只能将一个JSValue对象传给由相同虚拟机管理(host)的JSValue或者JSContext的实例方法。如果尝试把一个虚拟机的JSValue传给另一个虚拟机,将会触发一个Objective-C异常。
1. JSValue类型转换
JSValue提供了一系列的方法将native与JavaScript的数据类型进行相互转换:
2. NSDictionary与JS对象
NSDictionary对象以及其包含的keys与JavaScript中的对应名称的属性相互转换。key所对应的值也会递归地进行拷贝和转换。
[context evaluateScript:@"var color = {red:230, green:90, blue:100}"];//js->native 给你看我的颜色JSValue*colorValue=context[@"color"];NSLog(@"r=%@, g=%@, b=%@",colorValue[@"red"],colorValue[@"green"],colorValue[@"blue"]);NSDictionary*colorDic=[colorValue toDictionary];NSLog(@"r=%@, g=%@, b=%@",colorDic[@"red"],colorDic[@"green"],colorDic[@"blue"]);//native->js 给你点颜色看看context[@"color"]=@{@"red":@(0),@"green":@(0),@"blue":@(0)};[context evaluateScript:@"log('r:'+color.red+'g:'+color.green+' b:'+color.blue)"];Output:r=230,g=90,b=100r=230,g=90,b=100r:0g:0b:0
可见,JS中的对象可以直接转换成Objective-C中的NSDictionary,NSDictionary传入JavaScript也可以直接当作对象被使用。
3. NSArray与JS数组
NSArray对象与JavaScript中的array相互转转。其子元素也会递归地进行拷贝和转换。
[context evaluateScript:@“varfriends=['Alice','Jenny','XiaoMing']"];//js->native 你说哪个是真爱?JSValue*friendsValue=context[@"friends"];NSLog(@"%@, %@, %@",friendsValue[0],friendsValue[1],friendsValue[2]);NSArray*friendsArray=[friendsValue toArray];NSLog(@"%@, %@, %@",friendsArray[0],friendsArray[1],friendsArray[2]);//native->js 我觉XiaoMing和不不错,给你再推荐个Jimmycontext[@"girlFriends"]=@[friendsArray[2],@"Jimmy"];[context evaluateScript:@"log('girlFriends :'+girlFriends[0]+' '+girlFriends[1])"];
Output:
Alice,Jenny,XiaoMingAlice,Jenny,XiaoMinggirlFriends:XiaoMing Jimmy
4. Block/函数和JS function
Objective-C中的block转换成JavaScript中的function对象。参数以及返回类型使用相同的规则转换。
将一个代表native的block或者方法的JavaScript function进行转换将会得到那个block或方法。
其他的JavaScript函数将会被转换为一个空的dictionary。因为JavaScript函数也是一个对象。
5. OC对象和JS对象
对于所有其他native的对象类型,JavaScriptCore都会创建一个拥有constructor原型链的wrapper对象,用来反映native类型的继承关系。默认情况下,native对象的属性和方法并不会导出给其对应的JavaScript wrapper对象。通过JSExport协议可选择性地导出属性和方法。
后面会详细讲解对象类型的转换。