腾讯云技术分享专栏 JavaScriptCore 全面解析
本文由云 + 社区发表
作者:殷源,专注移动客户端开发,微软 Imagine Cup 中国区特等奖获得者
JavaScript 越来越多地出现在我们客户端开发的视野中,从 ReactNative 到 JSpatch,JavaScript 与客户端相结合的技术开始变得魅力无穷。本文主要讲解 iOS 中的 JavaScriptCore 框架,正是它为 iOS 提供了执行 JavaScript 代码的能力。未来的技术日新月异,JavaScript 与 iOS 正在碰撞出新的激情。
JavaScriptCore 是JavaScript的虚拟机,为 JavaScript 的执行提供底层资源。
一、JavaScript
在讨论 JavaScriptCore 之前,我们首先必须对 JavaScript 有所了解。
1. JavaScript 干啥的?
- 说的高大上一点:一门基于原型、函数先行的高级编程语言,通过解释执行,是动态类型的直译语言。是一门多范式的语言,它支持面向对象编程,命令式编程,以及函数式编程。
- 说的通俗一点:主要用于网页,为其提供动态交互的能力。可嵌入动态文本于 HTML 页面,对浏览器事件作出响应,读写 HTML 元素,控制 cookies 等。
- 再通俗一点:抢月饼,button.click()。(PS:请谨慎使用 while 循环)
2. JavaScript 起源与历史
- 1990 年底,欧洲核能研究组织(CERN)科学家 Tim Berners-Lee,在互联网的基础上,发明了万维网(World Wide Web),从此可以在网上浏览网页文件。
- 1994 年 12 月,Netscape 发布了一款面向普通用户的新一代的浏览器 Navigator 1.0 版,市场份额一举超过 90%。
- 1995 年,Netscape 公司雇佣了程序员 Brendan Eich 开发这种嵌入网页的脚本语言。最初名字叫做 Mocha,1995 年 9 月改为 LiveScript。
- 1995 年 12 月,Netscape 公司与 Sun 公司达成协议,后者允许将这种语言叫做 JavaScript。
3. JavaScript 与 ECMAScript
- “JavaScript” 是 Sun 公司的注册商标,用来特制网景(现在的 Mozilla)对于这门语言的实现。网景将这门语言作为标准提交给了 ECMA——欧洲计算机制造协会。由于商标上的冲突,这门语言的标准版本改了一个丑陋的名字 “ECMAScript”。同样由于商标的冲突,微软对这门语言的实现版本取了一个广为人知的名字 “Jscript”。
- ECMAScript 作为 JavaScript 的标准,一般认为后者是前者的实现。
4. Java 和 JavaScript
《雷锋和雷峰塔》
Java 和 JavaScript 是两门不同的编程语言 一般认为,当时 Netscape 之所以将 LiveScript 命名为 JavaScript,是因为 Java 是当时最流行的编程语言,带有 “Java” 的名字有助于这门新生语言的传播。
二、 JavaScriptCore
1. 浏览器演进
- 演进完整图
https://upload.wikimedia.org/wikipedia/commons/7/74/Timeline_of_web_browsers.svg
- WebKit 分支
现在使用 WebKit 的主要两个浏览器 Sfari 和 Chromium(Chorme 的开源项目)。WebKit 起源于 KDE 的开源项目 Konqueror 的分支,由苹果公司用于 Sfari 浏览器。其一条分支发展成为 Chorme 的内核,2013 年 Google 在此基础上开发了新的 Blink 内核。
2. WebKit 排版引擎
webkit 是 sfari、chrome 等浏览器的排版引擎,各部分架构图如下
- webkit Embedding API 是 browser UI 与 webpage 进行交互的 api 接口;
- platformAPI 提供与底层驱动的交互, 如网络, 字体渲染, 影音文件解码, 渲染引擎等;
- WebCore 它实现了对文档的模型化,包括了 CSS, DOM, Render 等的实现;
- JSCore 是专门处理 JavaScript 脚本的引擎;
3. JavaScript 引擎
- JavaScript 引擎是专门处理 JavaScript 脚本的虚拟机,一般会附带在网页浏览器之中。第一个 JavaScript 引擎由布兰登·艾克在网景公司开发,用于 Netscape Navigator 网页浏览器中。JavaScriptCore 就是一个 JavaScript 引擎。
- 下图是当前主要的还在开发中的 JavaScript 引擎
4. JavaScriptCore 组成
JavaScriptCore 主要由以下模块组成:
- Lexer 词法分析器,将脚本源码分解成一系列的 Token
- Parser 语法分析器,处理 Token 并生成相应的语法树
- LLInt 低级解释器,执行 Parser 生成的二进制代码
- Baseline JIT 基线 JIT(just in time 实施编译)
- DFG 低延迟优化的 JIT
- FTL 高通量优化的 JIT
关于更多 JavaScriptCore 的实现细节,参考 https://trac.webkit.org/wiki/JavaScriptCore
5. 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 的主要几个类:
- JSContext
- JSValue
- JSManagedValue
- JSVirtualMachine
- JSExport
接下来我们会依次讲解这几个类的用法。
6. Hello World!
这段代码展示了如何在 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
一个 JSVirtualMachine 的实例就是一个完整独立的 JavaScript 的执行环境,为 JavaScript 的执行提供底层资源。
这个类主要用来做两件事情:
- 实现并发的 JavaScript 执行
- JavaScript 和 Objective-C 桥接对象的内存管理
看下头文件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 代码,然而,所有其他想要使用该虚拟机的线程都要等待。
- 如果想并发执行 JS,需要使用多个不同的虚拟机来实现。
- 可以在子线程中执行 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
一个 JSContext 对象代表一个 JavaScript 执行环境。在 native 代码中,使用 JSContext 去执行 JS 代码,访问 JS 中定义或者计算的值,并使 JavaScript 可以访问 native 的对象、方法、函数。
1. JSContext 执行 JS 代码
- 调用 evaluateScript 函数可以执行一段
top-level
的 JS 代码,并可向 global 对象添加函数和对象定义 - 其返回值是 JavaScript 代码中最后一个生成的值
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
2. JSContext 访问 JS 对象
一个 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 对象的方法
- 通过 context 的实例方法 objectForKeyedSubscript
- 通过 context.globalObject 的 objectForKeyedSubscript 实例方法
- 通过下标方式
设置属性也是对应的。
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
一个 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 异常。
1. JSValue 类型转换
JSValue 提供了一系列的方法将 native 与 JavaScript 的数据类型进行相互转换:
2. NSDictionary 与 JS 对象
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 也可以直接当作对象被使用。
3. NSArray 与 JS 数组
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
4. Block/函数和 JS function
Objective-C 中的 block 转换成 JavaScript 中的 function 对象。参数以及返回类型使用相同的规则转换。
将一个代表 native 的 block 或者方法的 JavaScript function 进行转换将会得到那个 block 或方法。
其他的 JavaScript 函数将会被转换为一个空的 dictionary。因为 JavaScript 函数也是一个对象。
5. OC 对象和 JS 对象
对于所有其他 native 的对象类型,JavaScriptCore 都会创建一个拥有 constructor 原型链的 wrapper 对象,用来反映 native 类型的继承关系。默认情况下,native 对象的属性和方法并不会导出给其对应的 JavaScript wrapper 对象。通过 JSExport 协议可选择性地导出属性和方法。
后面会详细讲解对象类型的转换。
此文已由腾讯云 + 社区在各渠道发布
获取更多新鲜技术干货,可以关注我们腾讯云技术社区 - 云加社区官方号及知乎机构号