本文不再详述Objective-C与JavaScript之间交互的UIWebView和WKWebView常用实现,网上有太多此类文章可供参考,本文着重介绍JavaScriptCore以及一个使用JavaScriptCore黑魔法的UIWebView分类。JavaScriptCore框架提供了在Swift,Objective-C和基于C的应用程序中运行JavaScript程序的能力。您也可以使用JavaScriptCore将自定义对象插入JavaScript环境。JavaScriptCore是从iOS7开始引入的可实现Objective-C与JavaScript之间完美交互的框架,
JavaScriptCore框架主要包括四个类:JSContext,JSManagedValue,JSValue,JSVirtualMachine和一个协议:JSExport。
JSContext
JSContext对象表示一个JavaScript执行环境。您可以创建和使用JavaScript上下文来运行Objective-C或Swift代码中的JavaScript脚本,访问在JavaScript中定义或计算的值,并使JavaScript可访问Objective-C本地对象,方法或函数。Objective-C与JavaScript交互的类,对象,函数等都将保存在JSContext对象中。
JavaManagedValue
JSManagedValue对象包装JSValue对象,并添加“conditional retain”行为以提供值的自动内存管理,它是JavaScript和Objective-C对象的内存管理辅助对象。JavaManagedValue主要用来将JavaScript值存储在本身导出为JavaScript的Objective-C或Swift对象中。
要点
由于JavaScript内存管理机制和Objective-C内存管理机制并不相同,JavaScript是垃圾回收机制,Objective-C是引用计数机制,不要将非托管的JSValue对象存储在导出为JavaScript的原生对象中。由于JSValue对象强引用其封闭的JSContext对象,因此此操作会创建一个引用循环,而无法释放上下文,造成内存泄漏,JavaManagedValue就是保存JSValue并避免循环引用的扩充类。
托管值的“conditional retain”行为可确保只要满足以下任一条件,就会保留其基础JavaScript值:
- JavaScript值可通过JavaScript对象图访问(即,不受JavaScript垃圾回收管理)
- JSManagedValue对象可通过Objective-C/Swift对象图访问,如使用
addManagedReference:withOwner:
方法报告给JavaScriptCore虚拟机
然而,如果这两个条件都不成立,托管值将其value
属性设置为nil,并释放基础JSValue对象。
注意
JSManagedValue对象本身的行为类似于对其底层JSValue对象的ARC弱引用 - 也就是说,如果不使用
addManagedReference:withOwner:
方法添加“conditional retain”行为,当JavaScript垃圾收集器销毁基础JavaScript值时,则受托管值的value属性会自动变为nil。
JSValue
JSValue实例是对JavaScript值的引用。您可以使用JSValue类在JavaScript和Objective-C/Swift表示之间转换基本值(例如数字和字符串),以便在原生代码和JavaScript代码之间传递数据。您还可以使用此类创建JavaScript对象,该对象包装原生的自定义类的对象或由原生方法或块(blocks)提供的JavaScript函数实现。此对象会对创建自身的JSContext产生强引用。
每个JSValue实例都来自一个JSContext对象,该对象表示包含该值的JavaScript执行环境。该值持有对其上下文对象的强引用 - 只要保留与特定JSContext实例关联的任何值,该上下文就保持活动状态。当你调用实例方法时,就是在JSValue对象上调用,如果该方法返回另一个JSValue对象,则返回的值与原始值属于相同的上下文。
每个JavaScript值还(间接通过上下文属性)与特定的JSVirtualMachine对象关联,该对象表示上下文的底层执行资源集合。您可以将JSValue实例仅传递给由同一虚拟机托管的JSValue和JSContext实例方法 - 尝试将值传递给其他虚拟机会引发Objective-C异常。
在JavaScript和原生类型之间转换
JavaScriptCore会使用下面总结的规则自动将原生值转换为JavaScript值,反之亦然。
- NSDictionary对象或Swift字典及其包含的键成为具有相匹配命名属性的JavaScript对象,反之亦然。 键的值被递归复制和转换。
- NSArray对象或Swift数组成为JavaScript数组,反之亦然,并且元素被递归地复制和转换。
- Objective-C块(或带有@convention(block)属性的Swift闭包)成为JavaScript Function对象,参数和返回类型使用与值相同的规则进行转换。原生块或方法转换为支持的JavaScript函数块或方法;所有其他都会转换为空字典。
- 对于所有其他原生对象类型(以及类类型或元类型),JavaScriptCore使用反映原生类层次结构的构造函数原型链创建JavaScript包装器对象。默认情况下,原生对象的JavaScript包装器并不能使该对象的属性和方法在JavaScript中可用。要选择导出到JavaScript的属性和方法,请参阅JSExport。
在转换对象,方法或块时,JavaScriptCore使用表1中汇总的规则隐式转换对象属性和方法参数的类型和值。
Objective-C (and Swift) Types | JavaScript Types | Notes |
---|---|---|
nil | undefined | |
NSNull | null | |
NSString (Swift String) | String | |
NSNumber and primitive numeric types | Number, Boolean | Conversion is consistent with the following methods: valueWithBool:inContext:, valueWithDouble:inContext:, valueWithInt32:inContext:, valueWithUInt32:inContext: |
NSDictionary (Swift Dictionary) | Object | Recursive conversion. |
NSArray (Swift Array) | Array | Recursive conversion. |
NSDate | Date | |
Objective-C or Swift object (id or AnyObject) Objective-C or Swift class (Class or AnyClass) |
Object | Converts with valueWithObject:inContext:/toObject. |
Structure types: | Object | Other structure types are not supported. |
Objective-C block (Swift closure) | Function | Convert explicitly with valueWithObject:inContext:/toObject. JavaScript functions do not convert to native blocks/closures unless already backed by a native block/closure. |
JSVirtualMachine
JavaScript虚拟机,JSVirtualMachine实例为JavaScript执行提供了一个自包含的环境,有独立的堆空间和内存回收机制(垃圾回收机制),因此不同虚拟机之间不能交换数据。这个类主要有两个作用:支持JavaScript并行执行(同一个虚拟机里不能执行多线程),以及管理在JavaScript和Objective-C/Swift之间桥接的对象的内存。
线程和并行JavaScript执行
每个JavaScript上下文(一个JSContext对象)都属于一个虚拟机。每个虚拟机都可以包含多个上下文,允许在上下文之间传递值(JSValue对象)。但是,每个虚拟机都是不同的 - 您不能将在一个虚拟机中创建的值传递到另一个虚拟机中的上下文。
JavaScriptCore API是线程安全的 - 例如,您可以创建JSValue对象或从任何线程运行脚本 - 但是,尝试使用同一虚拟机的所有其他线程都将等待。要在多个线程上同时运行JavaScript,请为每个线程使用单独的JSVirtualMachine实例。
管理导出对象的内存
当您将Objective-C或Swift对象导出到JavaScript时,不得在该对象中存储JavaScript值。此操作会创建一个引用循环-JSValue对象拥有对其封闭JavaScript上下文的强引用,并且JSContext对象也拥有对导出到JavaScript的原生对象的强引用。相反,请使用JSManagedValue类有条件地保留JavaScript值,并将该受管值的本地所有权链(native ownership chain)报告给JavaScriptCore虚拟机。使用addManagedReference:withOwner:
和removeManagedReference:withOwner:
方法将原生对象图描述给JavaScriptCore。在删除对象的最后一个托管引用后,JavaScript垃圾收集器可以安全地销毁该对象。
JSExport
实现这个协议,用于将Objective-C类及其实例方法,类方法和属性导出为JavaScript代码。
使用JavaScriptCore时,OC与JS的交互
Objective-C调用JavaScript
1 | JSContext *ctx = [[JSContext alloc] init]; |
[ctx evaluateScript:js];
此处代码执行完,实际在JSContext里注册了JS的add函数(生成引用add函数的 JSValue)。上述代码创建了JSContext对象,执行了一段JS代码,代码声明了一个函数,后面调用此函数,返回值为 JSValue。实际上JSContext是实现OC与JS交互的核心。
JavaScript调用Objective-C
使用block
1 | JSContext *ctx = [[JSContext alloc] init]; |
OC的block导入到JS中,实际上是转换成了JS的function(函数)。
使用JSExport
使用JSExport可以实现将OC中更复杂的对象导入到JS中使用。下面是苹果官方文档提供的示例代码:
1 | @protocol MyPointExports <JSExport> |
将JS代码保存在script.js的文件中,文件直接添加到工程里:
1 | // Objective-C properties become fields. |
创建JSContext,执行上述JS代码:
1 |
|
上述代码同时还使用了JS执行异常的处理exceptionHandler
。注意这里没有直接引用ctx
而是使用 [JSContext currentContext]
查找到当前的JSContext
,是为了避免在自身引用的block中引用自身导致循环引用。同时从上述代码中可以看到,OC导出到JS的函数名转换规则,不过我们也可以通过 JSExportAs
宏定义函数名的转换,以下是转换的代码:
1 | @protocol MyPointExports <JSExport> |
则相应的js代码则修改为
1 | var q = MyPoint.make(0, 0); |
UIWebView黑魔法
UIWebView在加载页面时,其实会创建JS的运行环境JSContext,因此可以利用此JSContext,完成OC与JS的交互。
获取UIWebView加载页面时的JSContext的方法,通过KVO访问:
1 | - (void)webViewDidFinishLoad:(UIWebView *)webView { |
注意因为webViewDidFinishLoad:的调用时间晚,会导致上述方法加载JSContext较晚。另外还要注意在 UIWebView的生命周期内,可能会进行多次加载,多次调用webViewDidFinishLoad:。还因为是私有属性,存在着因为版本更新而变动的可能,因此不建议使用。
WebFrameLoadDelegate
此协议定义在WebKit框架下,但并没有公开头文件,因此无法直接使用(虽然无法看到头文件,但奇怪的是在官方文档中却可以查找到该协议的API文档),但我们仍然可以直接重载此代理方法,示例代码可在这里找到:https://github.com/TomSwift/UIWebView-TS_JavaScriptContext:
1 | @protocol TSWebFrame <NSObject> |
在上述的 webView:didCreateJavaScriptContext:forFrame: 代理方法中可以获取到context,注意这里回调 的webView类型为 WebView,不是我们熟悉的UIWebView类型,所以不建议使用该对象。因此如果需要查找到此 context关联的是哪个webView,可以通过全局的方式:
1 | static NSHashTable* g_webViews = nil; |
为了将所有UIWebView对象都添加到NSHashTable中,可以重写UIWebView的allocWithZone:方法:
1 | + (id) allocWithZone:(struct _NSZone *)zone |
到这里,所有的都一目了然了,我们获取了UIWebView的JSContext的,然后导入OC中的对象到JSContext中,剩下的就是在JS中调用此对象了。
内存管理
block中捕获JSContext
JSValue会对其所在的JSContext维持一个强引用,因此在JSContext中注入的block时,不要直接捕获JSContext或 JSValue,可以使用[JSContext currentContext];获取JSContext以及对JSValue进行弱引用处理。
1 | JSContext *ctx = [[JSContext alloc] init]; |
Objective-C与JavaScript循环引用
OC中有对象持有JSValue,并且该对象被传递到JS中使用的时候,会产生循环引用,这时候需要用 JSManagedValue保存JSValue,避免循环引用。
1 | //定义一个JSExport protocol |
1 | self.obj = [[JSExportObj alloc] init]; |
上述代码中,JS对象保留了传进来的obj,而obj.jsValue对callback也产生强引用,导致JS对象与OC对象相互引 用,产生了循环引用。鉴于JS与OC内存管理方法的不同(JS为垃圾回收,OC为引用计数),系统提供了 JSManagedValue解决这个问题。将代码改写如下:
1 | @interface JSExportObj () |