前言
最近因为工作的原因,越来越多的动态化开发模式开始在项目中实施。为了对Hybrid的开发有一个深入的了解,查阅了相关的博客和官方文档之后,决定把学到的东西在这里做一个总结,方便日后查阅。好了,废话不多说,要研究Hybrid开发,其中必不可少的是要去了解JavaScriptCore(以下简称JSCore)。那么我们就先从 JSCore入手,看看到底是怎么一个玩法。
引用文档:
什么是JSCore
JSCore是WebKit默认内嵌的JS引擎。它建立起了Objective-C和JavaScript两门语言沟通的桥梁。iOS7之后,苹果对WebKit中的JSCore进行了Objective-C的封装,并提供给所有的iOS开发者。JSCore框架给Swift、OC以及C语言编写的App提供了调用JS程序的能力。同时我们也可以使用JSCore往JS环境中去插入一些自定义对象。JSCore作为苹果的浏览器引擎WebKit中重要组成部分,这个JS引擎已经存在多年。
在业界中流行的动态化开发方案,如React Native和Weex等。其核心模块中必不可少的会用到JSCore。JSCore跟Google自己研发的浏览器引擎Chrome的V8一样,都是为了解释执行JS的脚本。
JSCore的四个基本类
上图是 苹果官网对 JSCore的介绍。从图中我们可以很清晰的看到,四个主要核心类分别就是:JSContext
、
JSManagedValue
、
JSValue
、
JSVirtualMachine
(以下简称JSVM)。那么我们接下来就来分别看看这些类是干嘛用的。
JSContext
一个JSContext表示了一次JS的执行环境。我们可以通过创建一个JSContext去调用JS脚本,访问一些JS定义的值和函数,同时也提供了让JS访问Native对象,方法的接口。
从字面上面来看,JSContext好像就是“上下文”的意思。那么什么是上下文呢?
比如在一篇文章中,我们看到一句话:“他飞快的跑了出去。”但是如果我们不看上下文的话,我们并不知道这句话究竟是什么意思:谁跑了出去?他是谁?他为什么要跑? 写计算机理解的程序语言跟写文章是相似的,我们运行任何一段语句都需要有这样一个“上下文”的存在。比如之前外部变量的引入、全局变量、函数的定义、已经分配的资源等等。有了这些信息,我们才能准确的执行每一句代码。
所以说,JSContext也就是JS的执行环境(也可以说是执行上下文),所有的JS代码都必须在一个JSContext中执行。如果我们要在WebView中去获取JSContext,可以直接通过KVC的方式直接获取。
JSContext *context = [[JSContext alloc] init];
[context evaluateScript:@"var a = 1;var b = 2;"];
NSInteger sum = [[context evaluateScript:@"a + b"] toInt32];//sum=3
复制代码
我们先创建一个JSContext
的环境,然后直接通过evaluateScript
方法就可以直接运行一段写好的JS的代码。然后返回值是通过JSValue(后面会有介绍)进行包装后返回。
上面提到了我们要获取WebView中的JSContext,可以用KVC的方式。同样的,我们要给JSContext塞全局对象和全局函数,也可以使用KVC的方式:
JSContext *context = [[JSContext alloc] init];
context[@"globalFunc"] = ^() {
NSArray *args = [JSContext currentArguments];
for (id obj in args) {
NSLog(@"拿到了参数:%@", obj);
}
};
context[@"globalProp"] = @"全局变量字符串";
[context evaluateScript:@"globalFunc(globalProp)"];//console输出:“拿到了参数:全局变量字符串”
复制代码
在JSContext的API中,有一个值得注意的只读属性 – JSValue类型的globalObject。它返回当前执行JSContext的全局对象,例如在WebKit中,JSContext就会返回当前的Window对象。而这个全局对象其实也是JSContext最核心的东西,当我们通过KVC方式与JSContext进去取值赋值的时候,实际上都是在跟这个全局对象做交互,几乎所有的东西都在全局对象里,可以说,JSContext只是globalObject的一层壳。
JSManagedValue
一个 JSManagedValue 对象是用来包装一个 JSValue 对象的,JSManagedValue 对象通过添加“有条件的持有(conditional retain)”行为来实现自动内存管理。一个managed value 的基本用法就是用来在一个要导出(exported)到 JavaScript 的 Objective-C 或者 Swift 对象中存储一个 JavaScript 值。
这里顺便说一下JS的GC机制: JS同样也不需要我们去手动管理内存。JS的内存管理使用的是GC机制。不同于OC的引用计数,GC是由GCRoot(context)开始维护的一条引用链,一旦引用链无法触达某对象节点,这个对象就会被回收掉。
JSValue
JSValue实例是一个指向JS值的引用指针。我们可以使用JSValue类,在OC和JS的基础数据类型之间相互转换。同时我们也可以使用这个类,去创建包装了Native自定义类的JS对象,或者是那些由Native方法或者Block提供实现JS方法的JS对象。
其实我们从上面的JSContext解释里面能看到,每个JSValue都存在于一个JSContext之中,也就是说这个context就是JSValue的作用域。JSCore帮我们用JSValue在底层自动做了一个OC转JS的类型转换之后,我们就可以通过JSValue拿到JS执行结果的返回值。
JSCore提供了10种类型转换
Objective-C type | JavaScript type |
---|---|
nil | undefined |
NSNull | null |
NSString | string |
NSNumber | number,boolean |
NSDictionary | Object object |
NSArray | Array object |
NSDate | Date object |
NSBlock | Funtion object |
id | Wrapper object |
Class | Constructor object |
同时还提供了对应的互换API:
+ (JSValue *)valueWithDouble:(double)value inContext:(JSContext *)context;
+ (JSValue *)valueWithInt32:(int32_t)value inContext:(JSContext *)context;
- (NSArray *)toArray;
- (NSDictionary *)toDictionary;
复制代码
NSDictionary <-> Object
上面我们说到JSContext的globalObject可以转换成OC对象,然后转成的OC对象是一个NSDictionary类型。其实,在JS中,对象就是一个引用类型的实例,因为JS中并不存在类的概念(ECMA把对象定义为:无序属性的集合,其属性可以包含基本值、对象或者函数)。于是我们可以发现JS中的对象就是无序的键值对,这就跟NSDictionary相差无几了。
NSBlock <-> Funtion Object
在前面我们说到,在JSContext赋值了一个”globalFunc”的Block,并可以在JS代码中当成一个函数直接调用。我还可以使用”typeof”关键字来判断globalFunc在JS中的类型:
NSString *type = [[context evaluateScript:@"typeof globalFunc"] toString];//type的值为"function"
复制代码
通过这个例子,我们也能发现传入的Block对象在JS中已经被转成了”function”类型。”Function Object”这个概念对于我们写惯传统面向对象语言的开发者来说,可能会比较晦涩。而实际上,JS这门语言,除了基本类型以外,就是引用类型。函数实际上也是一个”Function”类型的对象,每个函数名实则是指向一个函数对象的引用。比如我们可以这样在JS中定义一个函数:
var sum = function(num1,num2){
return num1 + num2;
}
复制代码
同时我们还可以这样定义一个函数(不推荐):
var sum = new Function("num1","num2","return num1 + num2");
复制代码
按照第二种写法,我们就能很直观的理解到函数也是对象,它的构造函数就是Function,函数名只是指向这个对象的指针。而NSBlock是一个包裹了函数指针的类,JSCore把Function Object转成NSBlock对象,可以说是很合适的。
JSVirtualMachine
一个JSVirtualMachine(以下简称JSVM)实例代表了一个自包含的JS运行环境,或者是一系列JS运行所需的资源。该类有两个主要的使用用途:一是支持并发的JS调用,二是管理JS和Native之间桥对象的内存。
JSVM是我们要学习的第一个概念。官方介绍JSVM为JavaScript的执行提供底层资源,而从类名直译过来,一个JSVM就代表一个JS虚拟机,我们在上面也提到了虚拟机的概念,那我们先讨论一下什么是虚拟机。首先我们可以看看(可能是)最出名的虚拟机——JVM(Java虚拟机)。 JVM主要做两个事情:
1、首先它要做的是把JavaC编译器生成的ByteCode(ByteCode其实就是JVM的虚拟机器指令)生成每台机器所需要的机器指令,让Java程序可执行(如下图)。 2、第二步,JVM负责整个Java程序运行时所需要的内存空间管理、GC以及Java程序与Native(即C,C++)之间的接口等等。
从功能上来看,一个高级语言虚拟机主要分为两部分,一个是解释器部分,用来运行高级语言编译生成的ByteCode,还有一部分则是Runtime运行时,用来负责运行时的内存空间开辟、管理等等。实际上,JSCore常常被认为是一个JS语言的优化虚拟机,它做着JVM类似的事情,只是相比静态编译的Java,它还多承担了把JS源代码编译成字节码的工作。
既然JSCore被认为是一个虚拟机,那JSVM又是什么?实际上,JSVM就是一个抽象的JS虚拟机,让开发者可以直接操作。在App中,我们可以运行多个JSVM来执行不同的任务。而且每一个JSContext(下节介绍)都从属于一个JSVM。但是需要注意的是每个JSVM都有自己独立的堆空间,GC也只能处理JSVM内部的对象(在下节会简单讲解JS的GC机制)。所以说,不同的JSVM之间是无法传递值的。
JSExport
实现JSExport协议可以开放OC类和它们的实例方法,类方法,以及属性给JS调用。
如果我们想在JS环境中使用OC中的类和对象,就需要他们实现JSExport协力来确定暴露给JS环境中的属性和方法。
@protocol PersonProtocol <JSExport>
- (NSString *)stuFullInfo;//stuFullInfo用来拼接stuName和stuID,并返回学生的全部信息
@end
@interface JSStudent : NSObject <PersonProtocol>
- (NSString *)sayStuFullInfo;//sayStuFullInfo方法
@property (nonatomic, copy) NSString *stuName;
@property (nonatomic, copy) NSString *stuID;
@end
复制代码
然后我们把JSStudent
的一个实例传入JSContext,并且可以直接执行stuFullInfo
方法:
JSStudent *student = [[JSStudent alloc] init];
context[@"student"] = student;
student.stuName = @"LiHeng Xue";
student.stuID =@"ID0018888";
[context evaluateScript:@"log(student.stuFullInfoe())"];//调Native方法,打印出student实例的学生的全部信息
[context evaluateScript:@"student.sayStuFullInfo())"];//提示TypeError,'student.sayStuFullInfo' is undefined
复制代码
在这里我们就能看得出来了,只有在JSExport里面开放出去的方法才能够使用,如果没有开放出去,如上面的sayStuFullInfo
方法,直接调用的时候是会报类型错误的。
总结一下JSCore
jscore其实就是给APP提供了一个js可以解释执行的运行环境与资源。我们主要使用的是JSContext和JSValue这两个类。JSContext提供互相调用的接口,JSValue为这个互相调用提供数据类型的桥接转换。让JS可以执行Native方法,并让Native回调JS,反之亦然。
JSCore怎么实现桥方法
ok,在这里我们看完了JSCore的一些基本原理,那么我们就要再来看看JSCore是怎么实现桥方法的呢?
市面上常见的桥方法调用有两种:
- 通过UIWebView的delegate方法:shouldStartLoadWithRequest来处理桥接JS请求。JSRequest会带上methodName,通过WebViewBridge类调用该method。执行完之后,会使用WebView来执行JS的回调方法,当然实际上也是调用的WebView中的JSContext来执行JS,完成整个调用回调流程。
- 通过UIWebView的delegate方法:在webViewDidFinishLoadwebViewDidFinishLoad里通过KVC的方式获取UIWebView的JSContext,然后通过这个JSContext设置已经准备好的桥方法供JS环境调用。
这里面使用的最广泛的就是一个开源库:WebViewJavaScriptBridge。
WebViewJavaScript的解读,请看我的下一篇帖子。