JavaScriptCore
JavaScriptCore是webkit的一个重要组成部分,主要是对JS进行解析和提供执行环境。代码是开源的,可以下下来看看(源码)。iOS7后苹果在iPhone平台推出,极大的方便了我们对js的操作。我们可以脱离webview直接运行我们的js。iOS7以前我们对JS的操作只有webview里面一个函数 stringByEvaluatingJavaScriptFromString,JS对OC的回调都是基于URL的拦截进行的操作。大家用得比较多的是WebViewJavascriptBridge和EasyJSWebView这两个开源库,很多混合都采用的这种方式。
JavaScriptCore和我们相关的类不是很多,使用起来也非常简单。
1 | #import "JSContext.h" |
JSContext
JSContext 是JS代码的执行环境
JSContext 为JS代码的执行提供了上下文环境,通过jSCore执行的JS代码都得通过JSContext来执行。
JSContext对应于一个 JS 中的全局对象
JSContext对应着一个全局对象,相当于浏览器中的window对象,JSContext中有一个GlobalObject属性,实际上JS代码都是在这个GlobalObject上执行的,但是为了容易理解,可以把JSContext等价于全局对象。
JSValue
JSValue 是对 JS 值的包装
JSValue 顾名思义,就是JS值嘛,但是JS中的值拿到OC中是不能直接用的,需要包装一下,这个JSValue就是对JS值的包装,一个JSValue对应着一个JS值,这个JS值可能是JS中的number,boolean等基本类型,也可能是对象,函数,甚至可以是undefined,或者null。转换如下:
1 | Objective-C type | JavaScript type |
其实,就相当于JS 中的 var。
JSValue存在于JSContext中
JSValue是不能独立存在的,它必须存在于某一个JSContext中,就像浏览器中所有的元素都包含于Window对象中一样,一个JSContext中可以包含多个JSValue。
都是强引用
这点很关键,JSValue对其对应的JS值和其所属的JSContext对象都是强引用的关系。因为jSValue需要这两个东西来执行JS代码,所以JSValue会一直持有着它们。
通过下面这些方法来创建一个JSValue对象:
1 | + (JSValue *)valueWithObject:(id)value inContext:(JSContext *)context; |
你可以将OC中的类型,转换成JS中的对应的类型(参见前面那个类型对照表),并包装在JSValue中,包括基本类型,Null和undfined。
或者你也可以创建一个新的对象,数组,正则表达式,错误,这几个方法达到的效果就相当于在JS中写 var a = new Array();
也可以将一个OC对象,转成JS中的对象,但是这样转换后的对象中的属性和方法,在JS中是获取不到的,怎样才能让JS中获取的OC对象中的属性和方法,我们后面再说。
OC调用JS
首先是一段JS代码,一个简单的递归函数,计算阶乘的:
1 | var factorial = function(n) { |
OC中调用这个JS中的函数
1 | //初始化JSContext |
首先,从bundle中加载这段JS代码。
然后,创建一个JSContext,并用他来执行这段JS代码,这句的效果就相当于在一个全局对象中声明了一个叫fatorial的函数,但是没有调用它,只是声明,所以执行完这段JS代码后没有返回值。
再从这个全局对象中获取这个函数,这里我们用到了一种类似字典的下标写法来获取对应的JS函数,就像在一个字典中取这个key对应的value一样简单,实际上,JS中的对象就是以 key : Value 的形式存储属性的,且JS中的object对象类型,对应到OC中就是字典类型,所以这种写法自然且合理。
这种类似字典的下标方式不仅可以取值,也可以存值。不仅可以作用于Context,也可以作用与JSValue,他会用中括号中填的key值去匹配JSValue包含的JS值中有没有对应的属性字段,找到了就返回,没找到就返回undefined。
然后,我们拿到了包装这个阶乘函数的的JSValue对象,在其上调用callWithArguments方法,即可调用该函数,这个方法接收一个数组为参数,这是因为JS中的函数的参数都不是固定的,我们构建了一个数组,并把NSNumber类型的值传了过去,然而JS肯定是不知道什么是NSNumber的,但是别担心,JSCore会帮我们自动转换JS中对应的类型, 这里会把NSNumber类型的值转成JS中number类型的值,然后再去调用这个函数。
最后,如果函数有返回值,就会将函数返回值返回,如果没有返回值则返回undefined,当然在经过JSCore之后,这些JS中的类型都被包装成了JSValue,最后我们拿到返回的JSValue对象,转成对应的类型并输出。
JS调用OC
JavaScript 与 Objective-C 交互主要通过2种方式:
1.Block : 第一种方式是使用block,block也可以称作闭包和匿名函数,使用block可以很方便的将OC中的单个方法暴露给JS调用。
2.JSExport 协议 : 第二种方式,是使用JSExport协议,可以将OC的中某个对象直接暴露给JS使用,而且在JS中使用就像调用JS的对象一样自然。
Block方式
首先看Html和JS代码:
1 | <!DOCTYPE html> |
OC代码
加载上方的Html代码
1 | NSString *path = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"JS调用OC.html"]; |
实现WebView的加载完成代理
1 | - (void)webViewDidFinishLoad:(UIWebView *)webView{ |
使用 Block 的坑
使用Block暴露方法很方便,但是有2个坑需要注意一下:
1.不要在Block中直接使用JSValue
2.不要在Block中直接使用JSContext
因为Block会强引用它里面用到的外部变量,如果直接在Block中使用JSValue的话,那么这个JSvalue就会被这个Block强引用,而每个JSValue都是强引用着它所属的那个JSContext的,这是前面说过的,而这个Block又是注入到这个Context中,所以这个Block会被context强引用,这样会造成循环引用,导致内存泄露。不能直接使用JSContext的原因同理。
那怎么办呢,针对第一点,建议把JSValue当做参数传到Block中,而不是直接在Block内部使用,这样Block就不会强引用JSValue了。
针对第二点,可以使用[JSContext currentContext] 方法来获取当前的Context。
JSExport协议
在.h声明JSExport协议
1 | //声明JSExport协议 |
此协议声明了两个方法
1 | - (void)handleFactorialCalculateWithNumber:(NSNumber *)number |
对应的会通过JSCore转换成JScalculateForJS
的方法和pushViewControllerTitle
方法
需要注意的是,OC中的函数声明格式与JS中的不太一样(应该说和大部分语言都不一样。。),OC函数中多个参数是用冒号:声明的,这显然不能直接暴露给JS调用,这不高保真。
所以需要对带参数的方法名做一些调整,当我们暴露一个带参数的OC方法给JS时,JSCore会用以下两个规则生成一个对应的JS函数:
1.移除所有的冒号
2.将跟在冒号后面的第一个小写字母大写
比如上面的那个类方法,转换之前方法名应该是 pushViewController: title:,在JS中生成的对应的方法名就会变成 pushViewControllerTitle。
苹果知道这种不一致可能会逼死某些强迫症。。所以加了一个宏JSExportAs来处理这种情况,它的作用是:给JSCore在JS中为OC方法生成的对应方法指定名字。
比如,还是上面这个方法handleFactorialCalculateWithNumber: number:,可以这样写:calculateForJS
注意:这个宏只对带参数的OC方法有效。
在.m里面实现声明的方法
1 | 注意这一句话 |
内存管理
我们都知道,Objective-C 用的是ARC (Automatic Reference Counting),不能自动解决循环引用问题(retain cycle),需要程序员手动处理,而JavaScript 用的是GC (准确的说是 Tracing Garbage Collection),所有的引用都是强引用,但是垃圾回收器会帮我解决循环引用问题,JavaScriptCore 也一样,一般来说,大多数时候不需要我们去手动管理内存。
但是下面2种情况需要注意一下:
1.不要在JS中给OC对象增加成员变量,这句话的意思就是说,当我们将一个OC对象暴露给JS后,就像前面说的使用JSExport协议,我们能想操纵JS对象一样操纵OC对象,但是这时候,不要在JS中给这个OC对象添加成员变量,因为这个动作产生的后果就是,只会在JS中为这个OC对象增加一个额外的成员变量,但是OC中并不会同步增加。所以说这样做并没有什么意义,还有可能造成一些奇怪的内存管理问题。
2.OC对象不要直接强引用JSValue对象,这句话再说直白点,就是不要直接将一个JSValue类型的对象当成属性或者成员变量保存在一个OC对象中,尤其是这个OC对象还暴露给JS的时候。这样会造成循环引用。
如何解决这个问题呢?你可能会想,不能强引用, 那就弱引用呗,但是这样做也是不行的,因为JSValue没用对象引用他,他就会被释放了。
那怎么办?分析一下,在这里,我们需要一种弱的引用关系,因为强引用会造成循环引用,但是又不能让这个JSValue因无人引用它而被释放。简而言之就是,弱引用但能保持JSValue不被释放。
于是,苹果退出了一种新的引用关系,叫conditional retain,有条件的强引用,通过这种引用就能实现我们前面分析所需要的效果,而JSManagedValue就是苹果用来实现conditional retain的类。
JSManagedValue
这是JSManagedValue的一般使用步骤:
1 | JSManagedValue *managedValue = [JSManagedValue managedValueWithValue:jsValue]; |
1.首先,用JSValue创建一个JSManagedValue对象,JSManagedValue里面其实就是包着一个JSValue对象,可以通过它里面一个只读的value属性取到,这一步其实是添加一个对JSValue的弱引用。
2.如果只有第一步,这个JSValue会在其对应的JS值被垃圾回收器回收之后被释放,这样效果就和弱引用一样,所以还需要加一步,在虚拟机上为这个JSManagedValue对象添加Owner(这个虚拟机就是给JS执行提供资源的,待会再讲),这样做之后,就给JSValue增加一个强关系,只要以下两点有一点成立,这个JSManagedValue里面包含的JSValue就不会被释放:
JSValue对应的JS值没有被垃圾回收器回收
Owner对象没有被释放
看个Demo
1 | //定义一个JSExport protocol |
上面的例子很简单,调用JS方法,进行赋值,JS对象保留了传进来的obj,最后,JS将自己的回调callback赋值给了obj,方便obj下次回调给JS;由于JS那边保存了obj,而且obj这边也保留了JS的回调。这样就形成了循环引用。
怎么解决这个问题?我们只需要打破obj对JSValue对象的引用即可。当然,不是我们OC中的weak。而是之前说的内存管理辅助对象JSManagedValue。
JSManagedValue 帮助我们保存JSValue,那里面保存的JS对象必须在JS中存在,同时 JSManagedValue 的owner在OC中也存在。
创建JSManagedValue的两种方法
1 | + (JSManagedValue *)managedValueWithValue:(JSValue *)value; |
建立弱引用关系方法
1 | - (void)addManagedReference:(id)object withOwner:(id)owner; |
移除弱引用方法
1 | - (void)removeManagedReference:(id)object withOwner:(id)owner; |
修改代码如下
1 | //定义一个JSExport protocol |
最后一点
一个 JSVirtualMachine可以运行多个context,由于都是在同一个堆内存和同一个垃圾回收下,所以相互之间传值是没问题的。但是如果在不同的 JSVirtualMachine传值,垃圾回收就不知道他们之间的关系了,可能会引起异常。