从JSCore了解Hybrid开发

2,766 阅读10分钟

Hybrid

前言

最近因为工作的原因,越来越多的动态化开发模式开始在项目中实施。为了对Hybrid的开发有一个深入的了解,查阅了相关的博客和官方文档之后,决定把学到的东西在这里做一个总结,方便日后查阅。好了,废话不多说,要研究Hybrid开发,其中必不可少的是要去了解JavaScriptCore(以下简称JSCore)。那么我们就先从 JSCore入手,看看到底是怎么一个玩法。

引用文档:


什么是JSCore

JSCoreWebKit默认内嵌的JS引擎。它建立起了Objective-CJavaScript两门语言沟通的桥梁。iOS7之后,苹果对WebKit中的JSCore进行了Objective-C的封装,并提供给所有的iOS开发者。JSCore框架给SwiftOC以及C语言编写的App提供了调用JS程序的能力。同时我们也可以使用JSCore往JS环境中去插入一些自定义对象。JSCore作为苹果的浏览器引擎WebKit中重要组成部分,这个JS引擎已经存在多年。

在业界中流行的动态化开发方案,如React NativeWeex等。其核心模块中必不可少的会用到JSCoreJSCore跟Google自己研发的浏览器引擎Chrome的V8一样,都是为了解释执行JS的脚本。

JSCore的四个基本类

JSCore基本类
上图是苹果官网JSCore的介绍。从图中我们可以很清晰的看到,四个主要核心类分别就是:JSContextJSManagedValueJSValueJSVirtualMachine(以下简称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的解读,请看我的下一篇帖子