Hybird 开发之 JavaScriptCore

背景

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

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

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

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

1、JavaScriptCore 框架的组成

1.1 类和协议

  • NSObject程序员

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

  • JSContextweb

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

    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]);      

复制代码

2、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 使用。

3、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 引擎,这里有一篇戴铭大神的博客,能够帮助你更好地了解,点击连接查看

4、内存管理

目前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就不会被释放:

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

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

这样作,就即避免了循环引用,又保证了JSValue不会由于弱引用而被马上释放。

5、多线程

咱们先来讲一下 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。

6、获取 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…

相关文章
相关标签/搜索