JavaScript 越来越多的出现在客户端的开发当中,ReactNative、JSPatch 都是利用 JavaScript 与客户端语言结合实现的。在 iOS 中,使 iOS 拥有执行 JavaScript 代码能力的的便是 JavaScriptCore 框架。
JavaScriptCore 简介
JavaScriptCore 是苹果 Safari 浏览器的 JavaScript 引擎,是专门处理 JavaScript 脚本的虚拟机。当前主要的还在开发中的 JavaScript 引擎如下
JavaScriptCore框架 是苹果在 iOS7 引入的框架,使用该框架可以在 Objective-C 或者基于C的程序中执行 Javascript 代码,也可以向 JavaScript 环境中插入一些自定义的对象。
JavaScriptCore框架 其实就是基于 webkit 中以C/C++实现的 JavaScriptCore 的一个包装,在旧版本iOS开发中,很多开发者也会自行将 webkit 的库引入项目编译使用。现在iOS7把它当成了标准库。
JavaScriptCore 解析
查看 JavaScriptCore.h,可以看到 JavaScriptCore 的几个主要类或协议
1 |
- JSContext
- JSValue
- JSManagedValue
- JSVirtualMachine
- JSExport
JSVirtualMachine
一个 JSVirtualMachine
的实例就是一个完整独立的 JavaScript 的执行环境,为 JavaScript 的执行提供底层资源,它有自己独立的堆栈以及垃圾回收机制
这个类主要做两件事情:
- 实现并发的 JavaScript 执行
- JavaScript 和 Objective-C 桥接对象的内存管理
1 | NS_CLASS_AVAILABLE(10_9, 7_0) |
每一个JavaScript上下文(JSContext对象)都归属一个虚拟机(JSVirtualMachine对象)。每个虚拟机可以包含多个不同的上下文,并允许在这些不同的上下文之间传值(JSValue对象)。
每个虚拟机都是独立的,有其自己的堆栈以及垃圾回收机制(garbage collector)。GC无法处理别的虚拟机堆中的对象,因此你不能把一个虚拟机中创建的值传给另一个虚拟机。
JavaScriptCore API 都是线程安全的,是通过锁定虚拟机实现的。你可以在任意线程创建JSValue或者执行JS代码,然而,所有其他想要使用该虚拟机的线程都要等待。
- 如果想并发执行JS,需要使用多个不同的虚拟机来实现。
- 可以在子线程中执行JS代码。
JSContext
一个 JSContext 对象代表一个JavaScript 执行环境。在native代码中,使用 JSContext 去执行JS代码,访问JS中定义或者计算的值,并使 JavaScript 可以访问native的对象、方法、函数。
1 | NS_CLASS_AVAILABLE(10_9, 7_0) |
JSContext 执行 JS 代码
- 调用
evaluateScript
函数可以执行一段top-level
的JS代码,并可向 global 对象添加函数和对象定义 - 其返回值是 JavaScript 代码中最后一个生成的值
- 调用
JSContext访问JS对象
一个 JSContext 对象对应了一个全局对象(global object)。例如web浏览器中的 JSContext,其全局对象就是window 对象。在其他环境中,全局对象也承担了类似的角色,用来区分不同的JavaScript context的作用域,实际上JS代码都是在这个 GlobalObject 上执行的,但是为了容易理解,可以把 JSContext 等价于全局对象。
JavaScript 中的任何一个全局函数或变量都是 window 的属性。可以通过JSValue对象或者context下标的方式来访问。
1 | // 获取上下文 |
这里有三那种访问 JavaScript 对象的方法:
- 通过 context 实例对象的
objectForKeyedSubscript:
方法访问 - 通过 context.globalObject(JSValue类型)对象的
objectForKeyedSubscript:
方法访问 - 通过下标
设置属性也是对应的操作
1 | /* 为JSContext提供下标访问元素的方式 */ |
JSValue
一个 JSValue 实例就是一个JavaScript值的引用。是对 JS 值得包装,例如 JS 的 number,boolean等基本类型,也可以是对象,函数。对 JS 和 OC 应类型如下:
JSValue 是不能独立存在的,它必须存在于某一个 JSContext 中,一个 JSContext 中可以包含多个 JSValue。JSValue 对其对应的 JS 值和其所属的 JSContext 对象都是强引用的关系,只要有任何一个 JSValue 被持有(retain),对应的 JSContext就不会被销毁。
每个 JSValue 都通过其 JSContext 间接关联了一个特定的 JSVirtualMachine 对象。你只能将一个 JSValue 对象传给由相同虚拟机管理(host)的 JSValue 或者 JSContext 的实例方法。如果尝试把一个虚拟机的 JSValue 传给另一个虚拟机,将会触发一个Objective-C异常。
NSDictionary,NSArray 与 JS 对象
NSDictionary 对象以及其包含的 keys 与 JavaScript 中的对应名称的 Object类型对象 相互转换。
JS 中的对象可以直接转换成 Objective-C 中的 NSDictionary,NSDictionary传入 JavaScript 也可以直接当作对象被使用
NSArray 对象也是类似的转换方式。
1 | [context evaluateScript:@"var color = {red:255, green:45, blue:89}"]; |
Block/函数 和 JS function
Objective-C中的block转换成 JavaScript 中的 function 对象。参数以及返回类型使用相同的规则转换。
将一个代表 native 的block或者方法的 JavaScript function 进行转换将会得到那个 block 或方法。
其他的 JavaScript 函数将会被转换为一个空的 dictionary。因为 JavaScript 函数也是一个对象。
OC 对象和 JS 对象
对于所有其他 native 的对象类型,JavaScriptCore 都会创建一个拥有 constructor 原型链的 wrapper 对象,用来反映native类型的继承关系。默认情况下,native对象的属性和方法并不会导出给其对应的 JavaScript wrapper 对象。通过 JSExport 协议可选择性地导出属性和方法。
JSExport
JSExport 协议提供了一种声明式的方法去向 JavaScript 代码导出 Objective-C 的实例类及其实例方法,类方法和属性。
在 JavaScript 中调用 OC 代码
有两种方式:
- Block
- JSExport 协议
Block 的方式如下:
1 | JSContext *context = [[JSContext alloc] init]; |
JSExport 的方式,需要通过继承 JSExport 协议的方式来导出指定的方法和属性:
1 | // 声明一个继承自 JSExport 的协议,在其中声明可以被 JS 导出的方法和属性 |
1 | JSContext *context = [[JSContext alloc] init]; |
- 继承自 JSExport 的协议中,声明的类方法,实例方法和属性(如:
x
,-add
,+JSObjectWithX: y:
),都可以背 JS 获取到并且调用。未在协议中声明的方法或属性(如:-doSomething
),则无法被 JS 获取到。 - 对于每一个导出的实例方法,JavaScriptCore 都会在 prototype 中创建一个存取器属性。对于每一个导出的类方法,JavaScriptCore会在 constructor 对象中创建一个对应的 JavaScript function。
- 在Objective-C中通过
@property
声明的属性决定了JavaScript中的对应属性的特征: - Objective-C类中的属性,成员变量以及返回值都将根据JSValue指定的拷贝协议进行转换。
函数名转换
转换成驼峰形式:
- 去掉所有的冒号
- 所有冒号后的第一个小写字母都会被转为大写
1 | // oc 方法 |
如果不喜欢默认的转换规则,也可以使用 JSExportAs 进行自定义转换
1 | // JSExportAs 的宏定义(JSExport.h) |
实现原理
当声明一个继承自 JSExport 的自定义协议时,就是在告诉 JSCore,这个自定义协议中声明的属性,实例方法和类方法需要被暴露给JS使用。(不在这个协议中的方法不会被暴露出去。)
当把实现这个协议的类的对象暴露给 JS 时,JS 中会生成一个对应的 JS 对象,然后,JSCore会按照这个协议中声明的内容,去遍历实现这个协议的类,把协议中声明的属性,转换成 JS 对象中的属性,实质上是转换成 getter 和 setter 方法,转换方法和之前说的block类似,创建一个JS方法包装着OC中的方法,然后协议中声明的实例方法,转换成 JS 对象上的实例方法,类方法转换成 JS 中某个全局对象上的方法。
JSManagedValue
循环引用
由于每个 JSValue 对其对应的 JS 值和其所属的 JSContext 对象都是强引用的关系,只要有任何一个 JSValue 被持有(retain),对应的 JSContext就不会被销毁。如果我们将一个 native 对象导出给 JavaScript,即将这个对象交由JavaScript 的全局对象持有,引用关系如下:
如果我们在 native 对象(如Block)中强引用持有 JSContext 或者 JSValue,便会造成循环引用:
因此在使用时要注意:
避免直接使用外部context
- 避免在导出的 block/native 函数中直接使用 JSContext
- 使用 [JSContext currentContext] 来获取当前context能够避免循环引用
1 | // 错误用法 |
避免直接使用外部JSValue
- 避免在导出的 block/native 函数中直接使用 JSValue
1 | // 错误用法 |
JSManagedValue
一个JSManagedValue对象包含了一个JSValue对象,“有条件地持有(conditional retain)”的特性使其可以自动管理内存。
所谓“有条件地持有(conditional retain)”,是指在以下两种情况任何一个满足的情况下保证其管理的 JSValue 被持有:
可以通过 JavaScript 的对象图找到该 JSValue
可以通过 native 对象能找到该 JSManagedValue。
使用 addManagedReference:withOwner:
方法可向虚拟机记录该关系。反之,如果以上条件都不满足,JSManagedValue 对象就会将其 value 置为 nil 并释放该 JSValue。JSManagedValue 对其包含的 JSValue 的持有关系与ARC下的虚引用(weak reference)类似。
通常我们使用 weak 来修饰 block 内需要使用的外部引用以避免循环引用,由于 JSValue 对应的JS对象内存由虚拟机进行管理并负责回收,这种方法不能准确地控制block内的引用JSValue的生命周期,可能在block内需要使用JSValue的时候,其已经被虚拟机回收。所以做好不使用 weak 引用,而是使用 JSManagedValue。
1 | // 可以使用 JSVirtualMachine 手动管理 |