JavaScriptCore学习之JavaScriptCore


JavaScriptCore框架的类


JavaScriptCore框架对外暴露的类实际上非常少,这样带来的好处是API非常简单。如下图所示,只有5个类,分别是JSContext,JSValue,JSManagedValue,JSVirtualMachine,JSExport,其中最核心的是JSContext和JSValue,我们平时打交道的基本就是这两个类了。

JavaScriptCore学习之JavaScriptCore_第1张图片


这些类的基本介绍如下:
JSVirtualMachine A JSVirtualMachine instance represents a self-contained environment for JavaScript execution.
JSContext A JSContext object represents a JavaScript execution environment.
JSValue A JSValue instance is a reference to a JavaScript value.
JSExport The JSExport protocol provides a declarative way to export Objective-C instance classes and their instance method, class methods, and properties to JavaScript code. (注意这里只能是OC)
JSManagedValue A JSManagedValue object wraps a JSValue object, adding “conditional retain” behavior to provide automatic memory management of values.



JSVirtualMachine


“A JSVirtualMachine instance represents a self-contained environment for JavaScript execution.”,这段话是官方文档中对JSVirtualMachine的定义,我们使用JSVirtualMachine的目的主要有两个:支持JavaScript并发执行,管理用于衔接JavaScript 与 OC(Swift) 代码的对象的内存。

1.Threading and Concurrent JavaScript Execution

每一个JavaScript content (a JSContext Object) 都属于一个VM (virtual machine) ,但一个virtual machine则能包含多个context。同时,对于运行在同一个VM的多个context,VM运行在它们之间互相传递values (JSValue Object)。但是,不同virtual machine之间则是相互隔绝的,即你不能将一个VM中的value传递到另外一个VM的context中。

JavaScriptCore API 是线程安全的,比如,你可以在任意线程中创建JSValue或者执行scripts,但是,同一个时间其他尝试使用这个VM的线程都会处于等待状态。如果需要在多线程中并发执行JS,为每个thread创建单独的 JSVirtual Machine即可。

2.Managing Memory for Exported Objects

注意,当我们 export 一个 OC 或 Swift object 到 JS 中时,不能在这个object 中存储对应的 JS values。这种行为会导致一个retain cycle,JSValue objects 持有他们对应的 JSContext 的强引用,JSContext 则持有export到JS的native object的强引用,即 native object(OC or Swift object) —> JSValue —> JSContext —> native object。

解决方法是使用JSManagedValue来有条件地持有JavaScript value,同时向VM报告这个managed value的native ownership chain。


refer:  https://developer.apple.com/library/ios/documentation/JavaScriptCore/Reference/JSVirtualMachine_Ref/index.html


JSContext


“A JSContext object represents a JavaScript execution environment.”,通过JavaScript Context,我们可以在OC或Swift中执行JavaScript scripts,访问JS中defined或者calculated的values,甚至直接在JS中访问native中的objects, methods, functions。


- init / - initWithVirtualMachine: 这两个初始化方法不同的地方是,-init会创建一个新的VM来执行代码;而 - initWithVirtualMachine: 则可以让多个context共用一个VM。可以参考上面关于JSVirtualMachine的描述。
- evaluateScript: / - evaluateScript:withSourceURL
1. Evaluating a script runs any top-level code and adds function and object definitions to the context’s global object.
2. 同步线程安全
3. sourceURL对JS的执行不会有任何影响,只是为方便在异常发生时多一些有用的调试信息。
+ currentContext / + currentCallee / + currentThis / + currentArguments 这几个方法的用途比较拗口,"Call this method within an Objective-C or Swift block or method invoked from within JavaScript"。也就是当我们在JS中调用native block或method时,在这些native block或method中,可以通过调用这几个方法获得一些关于JS的相关执行信息。
globalObject
“The JavaScript global object associated with the context. (read-only)”。如果学习过JS的同学对global object概念肯定不会陌生,比如浏览器的window,为了解决window中随时可能出现的命名冲突问题,还需要额外特定语法执行JS。

“Outside of web-browser use, a context’s global object serves a similar role, separating the JavaScript namespaces of different contexts.Global variables within a script appear as fields or subscripts in the global object.”  在JavaScriptCore中,JS的全局执行环境就是这个global object,定义的全局变量和函数都是作为这个global object的属性和方法而创建的。

这里不额外多做解释,想要详细了解的可以参考
exception/exceptionHandler 用于JavaScript函数调用时异常
virtualMachine 参考上面关于VM的介绍
name 用来远程调试
- objectForKeyedSubscript: / - setObject:forKeyedSubscript: JSContext基于OC的Object subscripting语法 (参考:http://nshipster.com/object-subscripting/),可以让我们以一种非常简洁的方式实现 JavaScript 与 Native之间的 objects和functions的交互。我们使用这个语法操作JSContext或着JSContext.golbalObject都是可以的。


refer:  https://developer.apple.com/library/ios/documentation/JavaScriptCore/Reference/JSContext_Ref/


JSValue


A JSValue instance is a reference to a JavaScript value. 

每一个JSValue instance都是来自于包含这个value的JSContext,这个value则包含了对context对象的强应用,这点需要特别注意,如果不注意可能会造成内存泄露。这也就意味着只有context包含的所有JSValue都被释放之后,这个context才可能被释放。当我们通过JSValue调用方法时,返回的新JSValue跟之前的JSValue是属于同一个context的。还有一点是关于VM的,JSValue通过它的context属性也会跟一个特定JSVirtualMachine产生关联,如之前所述,JSValue只能在同一个VM之传递,如果传递到另外一个VM,就会产生一个OC异常。

JavaScript 与 OC/Swift 的数据类型的之间的转换关系如下:
JavaScriptCore学习之JavaScriptCore_第2张图片

JSValue变量是对JavaScript Value的引用。我们可以通过JSValue类在JavaScript和OC(Swift)之间对一些basic values (例如numbers和strings) 进行转换,

refer:  https://developer.apple.com/library/ios/documentation/JavaScriptCore/Reference/JSValue_Ref/

JSExport


“The JSExport protocol provides a declarative way to export Objective-C instance classes and their instance methods, class methods, and properties to JavaScript code."

通过JSExport可以Exporting OC objects to JavaScript。当我们通过OC实例创建一个JavaScript Value的时候,并且JSValue类没有特别指明赋值特性,JavaScriptCore会通过创建一个JavaScript Wrapper Object来wrap这个OC实例。(对于特定的类,JavaScriptCore会自动赋值values到适当的JavaScript类型,例如,NSString instances 变成 JavaScript strings)。

在JS中,继承是通过原型链来实现的。对于每个我们export的OC class,JavaScriptCore都会在对应的JavaScript context中创建一个prototype。对于NSObject class,对应的prototype object是JavaScript Context的Object prototype。对于其他的OC类,JavaScriptCore会创建一个prototype,这个protocol的内部 [Prototype] 属性会指向JavaScriptCore为它的super class创建的那个protocol。通过这种方式,JavaScript wrapper object的原型链就能反应wrapper的类继承关系。

另外,JavaScriptCore还为每一个OC class都创建了一个JavaScript constructor。

默认情况下,OC class的所有方法和属性都不会被暴露给JavaScript,而是你必须要选择要暴露的方法和属性。对于class 实现的任意protocol,如果它包含了JSExport protocol,则JavaScriptCore就会认为这个protocol中包含的方法和属性列表是暴露给JavaScript的。这个时候,我们才能在JavaScript调用OC class的exported的方法和属性。

下面这段话需要特别注意一下,对于我们自定义的类,也就是实现了JSExport protocol的类。JavaScriptCore都只是创建一个JavaScript wrapper object来wrap这个OC类,而不是像NSString那样直接copy赋值给JavaScript的string原始数据类型。
JavaScriptCore学习之JavaScriptCore_第3张图片

由于OC消息的声明格式与JavaScript的函数声明格式差异很大,所以调用方式也有差异,基本转换规则有两个:一. 去掉所有的冒号。二. 冒号前面的单词大写。比如说:doFoo:withBar,在JavaScript的调用格式就是doFooWithBar。当然也可以使用 JSExportAs 宏来优化JS中调用的方法名称,这个宏只对有参数的selector起作用,例如:
JavaScriptCore学习之JavaScriptCore_第4张图片

refer:  https://developer.apple.com/library/ios/documentation/JavaScriptCore/Reference/JSExport_Ref/


JSManagedValue


OC的内存管理是引用技术,JS的内存管理机制是垃圾回收,但是大部分情况下我们不需要考虑这个之间的转换,JavaScriptCore已经帮我们搞定了。但是有一点需要特别注意,JSValue持有对JSContext的强引用。假如我们要在OC的class中保存JSValue,就会非常容易造成内存泄露。为了解决这个问题,JavaScriptCore引入了JSMangedValue,这个类与JSVirtualMachine的两个方法结合 (- addManagedReference:withOwner: 和 -removeManagedReference:withOwner),可以解决循环应用的问题。

A JSManagedValue object wraps a JSValue object, adding “conditional retain” behavior to provide automatic memory management of values. The primary use case for a managed value is to store a JavaScript value in Objective-c or Swift object that is itself exported to JavaScript.

managed value的 “conditional retain” 特性保证了只要在下面任意一个条件为true的情况下,managed value的underlying JavaScript value就不会被释放:
  1. The JavaScript value is reachable through the JavaScript object graph (that is, not subject to JavaScript garbage collection)
  2. The JSManagedValue object is reachable through the Objective-C or Swift object graph, as reported to the JavaScriptCore virtual machine using the addManagedReference:withOwner: method
也就是说,JSManagedValue内存会同时被JavaScript的GC机制和OC/Swift的引用技术机制管理,只有两个都释放的情况下,JSManagedValue才会被释放。

JSMangedValue类是ARC的弱引用,如果不使用 addManagedReference:withOwner:方法添加”conditional retain”行为,当JS的GC销毁underlying JavaScript value之后,managed value就会把value的值自动置为nil。
JavaScriptCore学习之JavaScriptCore_第5张图片

refer:  https://developer.apple.com/library/ios/documentation/JavaScriptCore/Reference/JSManagedValue_Ref/index.html



Debug


Apple为前端提供了一个非常方便的debug工具,这个工具对JavaScriptCore也是能起作用的。

首先我们启动模拟器,然后打开safari,在safari的菜单中,选择 Develop -> Simulator -> JSContext。当我们打开之后,发现会出来一个Web Inspector,我们可以在console中直接执行一些JS,可以发现这个调试工具非常方便。
JavaScriptCore学习之JavaScriptCore_第6张图片

如果你之前有过前端方面的一些调试经验,就会有点纳闷为什么 Resource 和 Debugger 都是空的,没有任何内容啊。这就涉及到前面的一个用于Debug的API, - evaluateScript:withSourceURL:,source URL的用途就在此,不过需要注意一下,这个API是从8.0才引入的。我们修改一下API调用,使用方法执行JS代码:


然后再次重新运行,打开safari的开发工具,就会发现Resource和Debugger都有内容了。这个时候就可以直接在这个工具内调试JS代码了,可以打断点和单步执行等基本的debug功能都有。
JavaScriptCore学习之JavaScriptCore_第7张图片


前面说到的关于debug用途的API,除了上面那个指定source URL,还有一个就是JSContext的name,这个是起什么作用呢?比如说我们的App有多个JSContext,这个时候使用Safari的develop工具的时候,该如何选择呢?如果我们为JSContext指定了一个name,就能解决这个问题了。 假设我们设置context的name为JSCoreDemo,就会发现Develope选择的时候的名字也变成了JSCoreDemo

JavaScriptCore学习之JavaScriptCore_第8张图片



第一,基本的内存循环引用的问题就不说了,需要强掉的是,不要在block内捕获JSContext,而是要使用 [JSContext currentContext]获取当前的context,避免循环应用。

第二,我在写测试代码的时候,遇到一个很奇怪的问题,假如把block或者Object (JSExport)直接通过subscripting语法赋值给context的global object的属性,然后再次置为nil,发现内存竟然不会被释放!直到JSContext被释放之后,这些OC的对象才会被释放。

这跟JavaScript的GC机制有关系,跟UC的hursing大牛交流了一下,尝试把这个变量当做globalObject的一个变量,或者使用delete删除这个变量(貌似V8调用这个方法是可以立即释放的)。最终的结果都是JavaScript无法立即回收,从而导致OC的对象也无法被释放。

最终的解决方式是调用JavaScriptCore的API:JSGarbageCollect , 来强制进行GC操作。这个API的问题是会带来性能问题,频繁的GC对JS的性能会产生比较大的影响,不过如果我们对于JavaScritCore的封装比较简单的情况下,调用这个方法影响应该不大。

第三,对于未confirms to JSExport的对象,也是可以传递到JS执行环境的,但是需要注意的是,JS对这种对象的处理仅仅简单的wrap,可以进行传递,但是不能调用这个对象的任何方法和属性!

测试代码


写了一些小demo,用于测试代码,github:  https://github.com/lihei12345/JSCoreDemo

参考

  • JavaScriptCore:http://nshipster.cn/javascriptcore/
  • https://www.bignerdranch.com/blog/javascriptcore-and-ios-7/,https://www.bignerdranch.com/blog/javascriptcore-example/
  • https://developer.apple.com/videos/play/wwdc2013-615/
  • https://developer.apple.com/videos/play/wwdc2014-512/
  • JavaScriptCore初探:https://hjgitbook.gitbooks.io/ios/content/04-technical-research/04-javascriptcore-note.html
  • http://blog.iderzheng.com/introduction-to-ios7-javascriptcore-framework/
  • https://github.com/kishikawakatsumi/JavaScriptBridge
  • https://github.com/bang590/JSPatch/wiki/JS-%E6%96%AD%E7%82%B9%E8%B0%83%E8%AF%95

你可能感兴趣的:(iOS)