本文由云 + 社区发表
作者:殷源,专注移动客户端开发,微软 Imagine Cup 中国区特等奖获得者
JavaScript 越来越多地出现在我们客户端开发的视野中,从 ReactNative 到 JSpatch,JavaScript 与客户端相结合的技术开始变得魅力无穷。本文主要讲解 iOS 中的 JavaScriptCore 框架,正是它为 iOS 提供了执行 JavaScript 代码的能力。未来的技术日新月异,JavaScript 与 iOS 正在碰撞出新的激情。
JavaScriptCore 是JavaScript的虚拟机,为 JavaScript 的执行提供底层资源。
在讨论 JavaScriptCore 之前,我们首先必须对 JavaScript 有所了解。
《雷锋和雷峰塔》
Java 和 JavaScript 是两门不同的编程语言 一般认为,当时 Netscape 之所以将 LiveScript 命名为 JavaScript,是因为 Java 是当时最流行的编程语言,带有 “Java” 的名字有助于这门新生语言的传播。
https://upload.wikimedia.org/wikipedia/commons/7/74/Timeline_of_web_browsers.svg
现在使用 WebKit 的主要两个浏览器 Sfari 和 Chromium(Chorme 的开源项目)。WebKit 起源于 KDE 的开源项目 Konqueror 的分支,由苹果公司用于 Sfari 浏览器。其一条分支发展成为 Chorme 的内核,2013 年 Google 在此基础上开发了新的 Blink 内核。
webkit 是 sfari、chrome 等浏览器的排版引擎,各部分架构图如下
JavaScriptCore 主要由以下模块组成:
关于更多 JavaScriptCore 的实现细节,参考 https://trac.webkit.org/wiki/JavaScriptCore
JavaScriptCore 是一个 C++ 实现的开源项目。使用 Apple 提供的 JavaScriptCore 框架,你可以在 Objective-C 或者基于 C 的程序中执行 Javascript 代码,也可以向 JavaScript 环境中插入一些自定义的对象。JavaScriptCore 从 iOS 7.0 之后可以直接使用。
在 JavaScriptCore.h 中,我们可以看到这个
#ifndef JavaScriptCore_h
#define JavaScriptCore_h
#include <JavaScriptCore/JavaScript.h>
#include <JavaScriptCore/JSStringRefCF.h>
#if defined(__OBJC__) && JSC_OBJC_API_ENABLED
#import "JSContext.h"
#import "JSValue.h"
#import "JSManagedValue.h"
#import "JSVirtualMachine.h"
#import "JSExport.h"
#endif
#endif /* JavaScriptCore_h */
这里已经很清晰地列出了 JavaScriptCore 的主要几个类:
接下来我们会依次讲解这几个类的用法。
这段代码展示了如何在 Objective-C 中执行一段 JavaScript 代码,并且获取返回值并转换成 OC 数据打印
//创建虚拟机
JSVirtualMachine *vm = [[JSVirtualMachine alloc] init];
//创建上下文
JSContext *context = [[JSContext alloc] initWithVirtualMachine:vm];
//执行JavaScript代码并获取返回值
JSValue *value = [context evaluateScript:@"1+2*3"];
//转换成OC数据并打印
NSLog(@"value = %d", [value toInt32]);
Output
value = 7
一个 JSVirtualMachine 的实例就是一个完整独立的 JavaScript 的执行环境,为 JavaScript 的执行提供底层资源。
这个类主要用来做两件事情:
看下头文件SVirtualMachine.h
里有什么:
NS_CLASS_AVAILABLE(10_9, 7_0)
@interface JSVirtualMachine : NSObject
/* 创建一个新的完全独立的虚拟机 */
(instancetype)init;
/* 对桥接对象进行内存管理 */
- (void)addManagedReference:(id)object withOwner:(id)owner;
/* 取消对桥接对象的内存管理 */
- (void)removeManagedReference:(id)object withOwner:(id)owner;
@end
每一个 JavaScript 上下文(JSContext 对象)都归属于一个虚拟机(JSVirtualMachine)。每个虚拟机可以包含多个不同的上下文,并允许在这些不同的上下文之间传值(JSValue 对象)。
然而,每个虚拟机都是完整且独立的,有其独立的堆空间和垃圾回收器(garbage collector ),GC 无法处理别的虚拟机堆中的对象,因此你不能把一个虚拟机中创建的值传给另一个虚拟机。
线程和 JavaScript 的并发执行
JavaScriptCore API 都是线程安全的。你可以在任意线程创建 JSValue 或者执行 JS 代码,然而,所有其他想要使用该虚拟机的线程都要等待。
通过下面这个 demo 来理解一下这个并发机制
JSContext *context = [[CustomJSContext alloc] init];
JSContext *context1 = [[CustomJSContext alloc] init];
JSContext *context2 = [[CustomJSContext alloc] initWithVirtualMachine:[context virtualMachine]];
NSLog(@"start");
dispatch_async(queue, ^{
while (true) {
sleep(1);
[context evaluateScript:@"log('tick')"];
}
});
dispatch_async(queue1, ^{
while (true) {
sleep(1);
[context1 evaluateScript:@"log('tick_1')"];
}
});
dispatch_async(queue2, ^{
while (true) {
sleep(1);
[context2 evaluateScript:@"log('tick_2')"];
}
});
[context evaluateScript:@"sleep(5)"];
NSLog(@"end");
context 和 context2 属于同一个虚拟机。
context1 属于另一个虚拟机。
三个线程分别异步执行每秒 1 次的 js log,首先会休眠 1 秒。
在 context 上执行一个休眠 5 秒的 JS 函数。
首先执行的应该是休眠 5 秒的 JS 函数,在此期间,context 所处的虚拟机上的其他调用都会处于等待状态,因此 tick 和tick_2
在前 5 秒都不会有执行。
而 context1 所处的虚拟机仍然可以正常执行tick_1
。
休眠 5 秒结束后,tick 和tick_2
才会开始执行(不保证先后顺序)。
实际运行输出的 log 是:
start
tick_1
tick_1
tick_1
tick_1
end
tick
tick_2
一个 JSContext 对象代表一个 JavaScript 执行环境。在 native 代码中,使用 JSContext 去执行 JS 代码,访问 JS 中定义或者计算的值,并使 JavaScript 可以访问 native 的对象、方法、函数。
top-level
的 JS 代码,并可向 global 对象添加函数和对象定义API Reference
NS_CLASS_AVAILABLE(10_9, 7_0)
@interface JSContext : NSObject
/* 创建一个JSContext,同时会创建一个新的JSVirtualMachine */
(instancetype)init;
/* 在指定虚拟机上创建一个JSContext */
(instancetype)initWithVirtualMachine:
(JSVirtualMachine*)virtualMachine;
/* 执行一段JS代码,返回最后生成的一个值 */
(JSValue *)evaluateScript:(NSString *)script;
/* 执行一段JS代码,并将sourceURL认作其源码URL(仅作标记用) */
- (JSValue *)evaluateScript:(NSString *)script withSourceURL:(NSURL*)sourceURL NS_AVAILABLE(10_10, 8_0);
/* 获取当前执行的JavaScript代码的context */
+ (JSContext *)currentContext;
/* 获取当前执行的JavaScript function*/
+ (JSValue *)currentCallee NS_AVAILABLE(10_10, 8_0);
/* 获取当前执行的JavaScript代码的this */
+ (JSValue *)currentThis;
/* Returns the arguments to the current native callback from JavaScript code.*/
+ (NSArray *)currentArguments;
/* 获取当前context的全局对象。WebKit中的context返回的便是WindowProxy对象*/
@property (readonly, strong) JSValue *globalObject;
@property (strong) JSValue *exception;
@property (copy) void(^exceptionHandler)(JSContext *context, JSValue
*exception);
@property (readonly, strong) JSVirtualMachine *virtualMachine;
@property (copy) NSString *name NS_AVAILABLE(10_10, 8_0);
@end
一个 JSContext 对象对应了一个全局对象(global object)。例如 web 浏览器中中的 JSContext,其全局对象就是 window 对象。在其他环境中,全局对象也承担了类似的角色,用来区分不同的 JavaScript context 的作用域。全局变量是全局对象的属性,可以通过 JSValue 对象或者 context 下标的方式来访问。
一言不合上代码:
JSValue *value = [context evaluateScript:@"var a = 1+2*3;"];
NSLog(@"a = %@", [context objectForKeyedSubscript:@"a"]);
NSLog(@"a = %@", [context.globalObject objectForKeyedSubscript:@"a"]);
NSLog(@"a = %@", context[@"a"]);
/
Output:
a = 7
a = 7
a = 7
这里列出了三种访问 JavaScript 对象的方法
设置属性也是对应的。
API Reference
/* 为JSContext提供下标访问元素的方式 */
@interface JSContext (SubscriptSupport)
/* 首先将key转为JSValue对象,然后使用这个值在JavaScript context的全局对象中查找这个名字的属性并返回 */
(JSValue *)objectForKeyedSubscript:(id)key;
/* 首先将key转为JSValue对象,然后用这个值在JavaScript context的全局对象中设置这个属性。
可使用这个方法将native中的对象或者方法桥接给JavaScript调用 */
(void)setObject:(id)object forKeyedSubscript:(NSObject <NSCopying>*)key;
@end
/* 例如:以下代码在JavaScript中创建了一个实现是Objective-C block的function */
context[@"makeNSColor"] = ^(NSDictionary *rgb){
float r = [rgb[@"red"] floatValue];
float g = [rgb[@"green"] floatValue];
float b = [rgb[@"blue"] floatValue];
return [NSColor colorWithRed:(r / 255.f) green:(g / 255.f) blue:(b / 255.f) alpha:1.0];
};
JSValue *value = [context evaluateScript:@"makeNSColor({red:12, green:23, blue:67})"];
一个 JSValue 实例就是一个 JavaScript 值的引用。使用 JSValue 类在 JavaScript 和 native 代码之间转换一些基本类型的数据(比如数值和字符串)。你也可以使用这个类去创建包装了自定义类的 native 对象的 JavaScript 对象,或者创建由 native 方法或者 block 实现的 JavaScript 函数。
每个 JSValue 实例都来源于一个代表 JavaScript 执行环境的 JSContext 对象,这个执行环境就包含了这个 JSValue 对应的值。每个 JSValue 对象都持有其 JSContext 对象的强引用,只要有任何一个与特定 JSContext 关联的 JSValue 被持有(retain),这个 JSContext 就会一直存活。通过调用 JSValue 的实例方法返回的其他的 JSValue 对象都属于与最始的 JSValue 相同的 JSContext。
每个 JSValue 都通过其 JSContext 间接关联了一个特定的代表执行资源基础的 JSVirtualMachine 对象。你只能将一个 JSValue 对象传给由相同虚拟机管理(host)的 JSValue 或者 JSContext 的实例方法。如果尝试把一个虚拟机的 JSValue 传给另一个虚拟机,将会触发一个 Objective-C 异常。
JSValue 提供了一系列的方法将 native 与 JavaScript 的数据类型进行相互转换:
NSDictionary 对象以及其包含的 keys 与 JavaScript 中的对应名称的属性相互转换。key 所对应的值也会递归地进行拷贝和转换。
[context evaluateScript:@"var color = {red:230, green:90, blue:100}"];
//js->native 给你看我的颜色
JSValue *colorValue = context[@"color"];
NSLog(@"r=%@, g=%@, b=%@", colorValue[@"red"], colorValue[@"green"], colorValue[@"blue"]);
NSDictionary *colorDic = [colorValue toDictionary];
NSLog(@"r=%@, g=%@, b=%@", colorDic[@"red"], colorDic[@"green"], colorDic[@"blue"]);
//native->js 给你点颜色看看
context[@"color"] = @{@"red":@(0), @"green":@(0), @"blue":@(0)};
[context evaluateScript:@"log('r:'+color.red+'g:'+color.green+' b:'+color.blue)"];
Output:
r=230, g=90, b=100
r=230, g=90, b=100
r:0 g:0 b:0
可见,JS 中的对象可以直接转换成 Objective-C 中的 NSDictionary,NSDictionary 传入 JavaScript 也可以直接当作对象被使用。
NSArray 对象与 JavaScript 中的 array 相互转转。其子元素也会递归地进行拷贝和转换。
[context evaluateScript:@“var friends = ['Alice','Jenny','XiaoMing']"];
//js->native 你说哪个是真爱?
JSValue *friendsValue = context[@"friends"];
NSLog(@"%@, %@, %@", friendsValue[0], friendsValue[1], friendsValue[2]);
NSArray *friendsArray = [friendsValue toArray];
NSLog(@"%@, %@, %@", friendsArray[0], friendsArray[1], friendsArray[2]);
//native->js 我觉XiaoMing和不不错,给你再推荐个Jimmy
context[@"girlFriends"] = @[friendsArray[2], @"Jimmy"];
[context evaluateScript:@"log('girlFriends :'+girlFriends[0]+' '+girlFriends[1])"];
Output:
Alice, Jenny, XiaoMing
Alice, Jenny, XiaoMing
girlFriends : XiaoMing Jimmy
Objective-C 中的 block 转换成 JavaScript 中的 function 对象。参数以及返回类型使用相同的规则转换。
将一个代表 native 的 block 或者方法的 JavaScript function 进行转换将会得到那个 block 或方法。
其他的 JavaScript 函数将会被转换为一个空的 dictionary。因为 JavaScript 函数也是一个对象。
对于所有其他 native 的对象类型,JavaScriptCore 都会创建一个拥有 constructor 原型链的 wrapper 对象,用来反映 native 类型的继承关系。默认情况下,native 对象的属性和方法并不会导出给其对应的 JavaScript wrapper 对象。通过 JSExport 协议可选择性地导出属性和方法。
后面会详细讲解对象类型的转换。
此文已由腾讯云 + 社区在各渠道发布
获取更多新鲜技术干货,可以关注我们腾讯云技术社区 - 云加社区官方号及知乎机构号