Hybird 开发之 JavaScriptCore

4,436 阅读14分钟

背景

通过 JavaScriptCore 框架,你可以在 Objective-C 或者基于 C 的程序中运行(evaluate) JavaScript 程序。它还能帮你插入一些自定义对象到 JavaScript 环境中去。

JavaScriptCore框架其实就是基于webkit中以C/C++实现的JavaScriptCore的一个包装,在旧版本iOS开发中,很多开发者也会自行将webkit的库引入项目编译使用。现在iOS7把它当成了标准库。

JavaScriptCore框架在OS X平台上很早就存在了,但是接口是纯C语言的,而在iOS7之前 苹果没有开放此框架,不少需要在iOS app中处理JavaScript的都要从开源的WebKit中编译出JavaScriptCore.a。之后苹果为了方便开发人员将JavaScriptCore框 架开放了,同时还提供了Objective-C的封装接口。

JavaScriptCore 的特性是自动化的、安全的、高保真的。本篇文章将要讨论的就是基于Objective-C封装的JavaScriptCore框架。

一、JavaScriptCore 框架的组成

1.1 类和协议

  • NSObject

    NSObject 是大部分 Objective-C 类的根类。

  • JSContext

    一个 JSContext 对象代表一个 JavaScript 执行环境(execution environment),负责原生和 JavaScript 的数据传递。通过jSCore执行的JS代码都得通过JSContext来执行。

    JSContext对应着一个全局对象,相当于浏览器中的window对象,JSContext中有一个GlobalObject属性,实际上JS代码都是在这个GlobalObject上执行的,但是为了容易理解,可以把JSContext等价于全局对象。

  • JSManagedValue

    一个 JSManagedValue 对象包装了一个 JSValue 对象,JSManagedValue 对象通过添加“有条件的持有(conditional retain)”行为来实现自动内存管理。

  • JSValue

    一个 JSValue 实例是 一个JavaScript 的值对象,用来记录 JavaScript的原始值,并提供进行原生值对象转换的接口方法。

    JS中的值不能直接拿到OC中使用,因此JSValue就是对JS值的封装,这个JS值可以是JS中的number,boolean等基本类型,也可能是对象,函数,甚至可以是undefined,或者null等。

    JSValue 不能独立存在,只能存在于某一个JSContext中。

    JSValue对其对应的JS值和其所属的JSContext对象都是强引用的关系。因为jSValue需要这两个东西来执行JS代码,所以JSValue会一直持有着它们

  • JSVirtualMachine

    一个 JSVirtualMachine 实例代表一个自包含的(self-contained) JavaScript 执行环境(execution environment),为JavaScript代码的运行提供一个虚拟机环境。

    在同一时间 内,JSVirtualMachine 只能执行一个线程,如果想要多个县城执行任务,你可以创建多个JSVirtualMachine。每个 JSVirtualMachine 都有自己的垃圾回收器,以便进行内存管理,所以多个 JSVirtualMachine 之间的对象无法传递。

  • JSExport

    JSExport 协议提供了一些关于将 Objective-C 实例的类和它们的实例方法,类方法以及属性转成 JavaScript 代码的接口声明。

1.2 JSVirtualMachine、JSContext、JSValue 之间的关系

首先我们先用一个图来表示他们之间的关系:

从图中可以看出,一个 JSVirtualMachine 包含多个 JSContext,同一个 JSContext 中 又包含多个 JSValue。这三个类提供的接口可以使原生 app 访问和执行JavaScript函数,也可以让JavaScript 执行原生代码。

接下来我们用两段代码来表示:

//计算从n 到1 所有的数字相乘的结果
var multiply = function(n) {
    if (n < 0) {
        return;
    } 
    if (n == 0) {
        return 1;
    }
    return n * multiply(n - 1);
};
//从bundle中加载这段JS代码。
NSString *multiplyScript = [self loadJSFromBundle];

//使用jsvm创建一个JSContext,并用他来执行这段JS代码,这句的效果就相当于在一个全局对象中声明了一个叫multiply的函数,但是没有调用它,只是声明,所以执行完这段JS代码后没有返回值。
JSContext *context = [[JSContext alloc] init];
[context evaluateScript:multiplyScript]; 

//再从这个全局对象中获取这个函数,这里我们用到了一种类似字典的下标写法来获取对应的JS函数,就像在一个字典中取这个key对应的value一样简单,实际上,JS中的对象就是以 key : Value 的形式存储属性的,且JS中的object对象类型,对应到OC中就是字典类型,所以这种写法自然且合理。
JSValue *function = context[@"multiply"];

//调用callWithArguments方法,即可调用该函数,这个方法接收一个数组为参数,这是因为JS中的函数的参数都不是固定的,我们构建了一个数组,并把NSNumber类型的5传了过去,然而JS肯定是不知道什么是NSNumber的,但是别担心,JSCore会帮我们自动转换JS中对应的类型, 这里会把NSNumber类型的5转成JS中number类型的5,然后再去调用这个函数(这就是前面说的API目标中自动化的体现)。
JSValue *result = [function callWithArguments:@[@5]];
NSLog(@"%d",[result toInt32]);      

二、JavaScript 和原生交互

首先我们先通过一张图来解释他们之间的交互关系:

我们可以看到,每一个JavaScriptCore 中的 JSVirtualMachine 对应着一个原生线程,同一个JSVirtualMachine 中可以使用 JSValue 和原生线程通信,遵循的是 JSExport 协议:原生线程可以将类方法和属性提供给JavaScriptCore 使用,JavaScriptCore 可以将 JSValue 提供给原生线程使用。

2.1 原生调用 JavaScript

2.1.1 原生获取 JavaScript 中的一个变量

我们用一段代码来表示在OC中调用JS,定义一段js代码 "var i = 4 + 8" 声明了一个变量 i。代码如下所示:


JSContext *context  = [[JSContext alloc] init];
// 解析执行 JavaScript 脚本
JSValue *value = [context evaluateScript:@"var i = 4 + 8"];
// 转换 i 变量为原生对象
JSValue *i = value[@"i"];
NSNumber *number = [i toNumber];
NSLog(@"var i is %@, number is %@",context[@"i"], number);

我们可以看到JSContext 调用 evaluateScript 方法 返回一个 JSValue 对象。通过value[i] 获取到js 变量 i, 然后通过toNumber 方法将 js 变量类型转换为原生的变量类型,可以通过点击链接,来查看官网是怎么实现js 值和原生值之间的转换的。

我们来列举一下我们比较常用的 2 个转换类型的方法:

  • toArray :将 JS 类型的 array 数组转为 OC 中的 NSArray 类型
  • toDictionary :将 JS 中的字典 dictionary 转换为 NSDictionary 类型的值。

2.1.2 原生调用 JavaScript 中的函数对象

在OC代码中使用 JavaScript 的函数, 我们可以通过callWithArguments 方法并传入参数,并实现函数的调用,我们可以用以下代码来帮助理解:

JSContext *context  = [[JSContext alloc] init];
// 解析执行 JavaScript 脚本
[context evaluateScript:@"function addition(x, y) { return x + y}"];
// 获得 addition 函数
JSValue *addition = context[@"addition"];
// 以数组的形式传入参数执行 addition 函数
JSValue *resultValue = [addition callWithArguments:@[@(4), @(8)]];
// 将 addition 函数执行的结果转成原生 NSNumber 来使用。
NSLog(@"function is %@; reslutValue is %@",addition, [resultValue toNumber]);

从代码中可以看出,JSContext 先通过evaluateScript 方法获取 JavaScript 代码中的 JSValue类型的 addtion 函数, 再通过JSValue 的callWithArguments 方法,通过数组的形式传入函数所需参数x、y来执行函数。

2.1.3 原生调用 JavaScript 中的全局函数

我们通常使用invokeMethod:withArguments 方法来调用 JavaScript 中的全局函数。例如,Weex 框架 就是使用的这个方法来获取JS中的全局函数的。

代码的路径是incubator-weex/ios/sdk/WeexSDK/Sources/Bridge/WXJSCoreBridge.mm ,核心代码如下所示:

- (JSValue *)callJSMethod:(NSString *)method args:(NSArray *)args {
    WXLogDebug(@"Calling JS... method:%@, args:%@", method, args);
    return [[_jsContext globalObject] invokeMethod:method withArguments:args];
}

从以上代码中可以看出,JSContext 先通过[_jsContext globalObject] 获取到 JSValue 类型的属性globalObject ,属性中记录了 JSContext 的全局对象,使用 globalObject 执行的 JavaScript 函数能够使用全局 JavaScript 对象。因此,通过 globalObject 执行 invokeMethod:withArguments 方法就能够去使用全局 JavaScript 对象了。

2.2 JavaScript 调用原生代码

2.2.1 通过 Block 调用原生函数

我们先给出一段代码来帮助大家理解 JavaScript 怎样 调用原生代码的:

// 在 JSContext 中使用原生 Block 设置一个减法 subtraction 函数
context[@"subtraction"] = ^(int x, int y) {
    return x - y;
};

// 在同一个 JSContext 里用 JavaScript 代码来调用原生 subtraction 函数
JSValue *subValue = [context evaluateScript:@"subtraction(4,8);"];
NSLog(@"substraction(4,8) is %@",[subValue toNumber]);

从以上的代码可以看出,JavaScript 通过 Block 调用原生代码的方式是:

  • 第一步:在 JSContext 中使用原生 block 设置一个减法函数 subtraction;
  • 第二步:在同一个 JSContext 中用JavaScript 代码来调用原生 subtraction 函数。

2.2.2 通过 JSExport 协议调用原生代码

在原生代码中让遵循 JSExport 协议的类,能够供 JavaScript 使用。在Weex 框架中,就有一个遵循了 JSExport 协议的 WXPolyfillSet 类,使得 JavaScript 也能够使用原生代码中的 NSMutableSet 类型。

WXPolyfillSet 的头文件代码路径是 incubator-weex/ios/sdk/WeexSDK/Sources/Bridge/WXPolyfillSet.h,内容如下:


@protocol WXPolyfillSetJSExports <JSExport>

// JavaScript 可以使用的方法
+ (instancetype)create;
- (BOOL)has:(id)value;
- (NSUInteger)size;
- (void)add:(id)value;
- (BOOL)delete:(id)value;
- (void)clear;

@end

// WXPolyfillSet 遵循 JSExport 协议
@interface WXPolyfillSet : NSObject <WXPolyfillSetJSExports>

@end

我们从上面的代码中可以看出WXPolyfillSet 通过 JSExport 协议,提供了一系列方法给 JavaScript 使用。

三、JavaScriptCore 引擎

我们知道 JavaScript 和原生之间的互通依赖于虚拟机环境 JSVirtualMachine。接下来就让我们深入的了解 JavaScriptCore 引擎吧,了解了之后我们会知道JavaScriptCore 是怎么通过直接使用缓存JIT 编译的机器码来提高性能的,又是怎么对部分函数进行性能测试编译优化的。

JSVirtualMachine 是一个抽象的 JavaScript 虚拟机,是提供给开发者进行开发的,而其核心的 JavaScriptCore 引擎则是一个真实的虚拟机,包含了虚拟机都有的解释器和运行时部分,其中,解释器主要是来将高级的脚本语言编译成字节码,运行时主要用来管理运行时的内存空间。当内存出现问题的时候,需要调试内存问题时候,我们科室使用JavaScriptCCore 里面的 WebInspector,或者通过手动触发 Full GC的方式来排查内存的问题。

JavaScript 引擎的组成

JavaScriptCore 内部是由 Parser、Interpreter、Compiler、GC等部分组成,其中Compiler 负责把字节码翻译成为机器码,并进行优化。我们可以查看 WebKit 官方文档来查看JavaScriptCore 引擎的介绍。

JavaScriptCore 解释执行 JavaScript 代码的流程,可以分为以下两步。

  • Parser 负责进行语法分析、词法分析、生成字节码。
  • 由Interpreter 进行解释执行,解释执行的过程是先由 LLInt (Low Level Interpreter)来执行Parser 生成的字节码,JavaScriptCore 会对进行频次高的函数或者循环进行优化。优化器有Baseline JIT、DFG JIT、FTL JIT。对于多优化层级进行切换,JavaScriptCore使用OSR(On Stack Replacement)来管理。

如果大家想更深入的了解JavaScript 引擎,这里有一篇戴铭大神的博客,可以帮助你更好地了解,点击链接查看

四、内存管理

目前Objective-C 使用的是ARC,不能自动解决循环引用的问题,需要我们程序员手动去解除循环,但是 JavaScript 使用的是GC(垃圾回收机制),所有的引用都是强引用,同时垃圾回收器可以帮我们解决循环引用的问题, JavaScriptCore 也是一样的,一般来说,大多数情况下不需要我们去手动的管理内存。

有两个情况需要我们注意一下:

  • 第一:不要在JavaScript 中给 Objective-C 对象增加成员变量

如果增加的话,只能够在JavaScript 中为这个Objective-C 对象增加一个额外的成员变量,但是在原生代码中并不会同步增加这个成员变量,这样做没意义并且还可能造成一些奇怪的内存问题。

  • 第二:在Objective-C中的对象不要直接强引用 JsValue 对象

不要直接将一个 JSValue 类型的对象当成属性或者成员变量保存在一个Objective-C对象中,特别是当这个Objective-C对象还是暴露给JavaScript的时候,这样做的话会导致循环引用。如下图所示:

Objective-C不能直接强引用 JSValue类型的对象,其实也是不能直接弱引用的,如果弱引用的话,JSValue 对象就会被释放了。如下图所示:

举个例子说明一下:

//定义一个JSExport protocol
@protocol JSExportTest <JSExport>
//用来保存JS的对象
@property (nonatomic, strong) JSvalue *jsValue;

@end

//建一个对象去实现这个协议:

@interface JSProtocolObj : NSObject<JSExportTest>
@end

@implementation JSProtocolObj

@synthesize jsValue = _jsValue;

@end

//在VC中进行测试
@interface ViewController () <JSExportTest>

@property (nonatomic, strong) JSProtocolObj *obj;
@property (nonatomic, strong) JSContext *context;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    //创建context
    self.context = [[JSContext alloc] init];
    //设置异常处理
    self.context.exceptionHandler = ^(JSContext *context, JSValue *exception) {
        [JSContext currentContext].exception = exception;
        NSLog(@"exception:%@",exception);
    };
   //加载JS代码到context中
   [self.context evaluateScript:
   @"function callback (){};
   
    function setObj(obj) {
    this.obj = obj;
    obj.jsValue=callback;
}"];
   //调用JS方法
   [self.context[@"setObj"] callWithArguments:@[self.obj]];  
}

上面的例子很简单,调用JS方法,进行赋值,JS对象保留了传进来的obj,最后,JS将自己的回调callback赋值给了obj,方便obj下次回调给JS;由于JS那边保存了obj,而且obj这边也保留了JS的回调。这样就形成了循环引用。

难道就没有办法了吗?办法是有的,只需要通过弱引用并且能保持JSValue对象不会被释放就行。

在此,苹果给出了一种新的引用关系,叫做 conditional ration ,就是有条件的强引用。通过这种引用我们就能实现我们想要的效果了。JSManageValue 就是苹果用来实现 conditional ration 的一个类。

JSManagedValue

//从bundle中加载这段JS代码。
 NSString *multiplyScript = [self loadJSFromBundle];
 JSContext *context = [[JSContext alloc] init];
 [context evaluateScript:multiplyScript];
 JSValue *function = context[@"multiply"];
 JSValue *result = [function callWithArguments:@[@5]];
 
 JSManagedValue *managedValue = [JSManagedValue managedValueWithValue:result];
    [context.virtualMachine addManagedReference:managedValue withOwner:self];   

以下是JSManagedValue的一般使用步骤:

  • 第一步,用JSValue对象创建一个JSManagedValue对象,JSManagedValue里面其实就是包着一个JSValue对象,可以通过它里面一个只读的value属性取到,这一步其实是添加一个对JSValue的弱引用。如果是只执行这一步的话,JSValue会在其对应的JS值被垃圾回收器回收之后被释放。因此我们还需要执行第二步。

  • 第二步,在虚拟机上为这个JSManagedValue对象添加Owner(这个虚拟机就是给JS执行提供资源的,待会再讲),这样做之后,就给JSValue增加一个强关系,只要以下两点有一点成立,这个JSManagedValue里面包含的JSValue就不会被释放:

  • 1、JSValue对应的JS值没有被垃圾回收器回收。

  • 2、Owner对象没有被释放。

这样做,就即避免了循环引用,又保证了JSValue不会因为弱引用而被立刻释放。

五、多线程

我们先来说一下 JSVirtualMachine,它为JavaScript 的运行提供了底层资源,有自己独立的堆栈以及垃圾回收机制。

JSVirtualMachine 还是JSContext 的容器,可以包含若干个JSContext,在一个进程之中,可以存在多个JSVirtualMachine,JSVirtualMachine/JSContext/JSValue之间的关系我们前面 1.2 章节说过,我们可以在同一个 JSVirtualMachine 的不同 JSContext 中互相传递 JSValue ,但是我们不能在不同的 JSVirtualMachine 中的 JSContext 之间传递 JSValue。

这些都是因为每一个 JSVirtualMachine 都有自己独立的堆栈和垃圾回收器,一个 JSVirtualMachine 的垃圾回收器不知道怎么处理从另一个堆栈传递过来的值。

事实上,JavaScriptCore 提供的API 本身就是线程安全的。

我们可以在不同的线程之中创建 JSValue,使用 JSContext 执行JS语句,但是当一个线程正在执行 JS语句的时候,其他线程想要使用这个正在执行 JS 语句的 JSContext 所属的 JSVirtualMachine 就必须得等待,等待前前一个线程执行完,才能使用这个JSVirtualMachine。

这个强制串行的粒度是 JSVirtualMachine,如果你想要在不用线程中并发执行JS代码,可以为不同的线程创建不同 JSVirtualMachine。

六、获取 UIWebView 中的 JSContext

在 UIWebView 的代理方法中获取 JSContext,代码如下:

- (void)webViewDidFinishLoad:(UIWebView *)webView
{
    self.context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
    
}

上面代码中我们使用了私有属性 "documentView.webView.mainFrame.javaScriptContext" ,可能会被苹果拒绝上架。这里我们要注意的是每个页面加载完都是一个新的context,但是都是同一个JSVirtualMachine。如果 JavaScript 调用OC方法进行操作UI的时候,请注意当前线程是不是在主线程。

总结

本章文章我跟大家分享了 JavaScriptCore 框架的组成、JavaScript 和原生交互、JavaScriptCore 引擎、内存管理、多线程、获取 UIWebView 中的 JSContext等内容,或许我写的也不是太完整,希望大家能留言沟通指出问题,并进一步探讨关于 JavaScriptCore 相关的内容。

参考

JavaScriptCore API Reference

time.geekbang.org/column/arti…

www.jianshu.com/p/ac534f508…