JavaScriptCore全面解析

2022-06-29 17:14:15 浏览数 (3)

| 导语JavaScript越来越多地出现在我们客户端开发的视野中,从ReactNative到JSpatch,JavaScript与客户端相结合的技术开始变得魅力无穷。本文主要讲解iOS中的JavaScriptCore框架,正是它为iOS提供了执行JavaScript代码的能力。未来的技术日新月异,JavaScript与iOS正在碰撞出新的激情。

作者:殷源--腾讯移动端工程师

@IMWeb前端社区

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

5. JavaScriptCore

JavaScriptCore是一个C 实现的开源项目。使用Apple提供的JavaScriptCore框架,你可以在Objective-C或者基于C的程序中执行Javascript代码,也可以向JavaScript环境中插入一些自定义的对象。JavaScriptCore从iOS 7.0之后可以直接使用。

在JavaScriptCore.h中,我们可以看到这个

这里已经很清晰地列出了JavaScriptCore的主要几个类:

JSContext JSValue JSManagedValue JSVirtualMachine JSExport

接下来我们会依次讲解这几个类的用法。

6. Hello World!

这段代码展示了如何在Objective-C中执行一段JavaScript代码,并且获取返回值并转换成OC数据打印

Output

三、 JSVirtualMachine

一个JSVirtualMachine的实例就是一个完整独立的JavaScript的执行环境,为JavaScript的执行提供底层资源。

这个类主要用来做两件事情:

1、实现并发的JavaScript执行

2、JavaScript和Objective-C桥接对象的内存管理

看下头文件SVirtualMachine.h里有什么:

每一个JavaScript上下文(JSContext对象)都归属于一个虚拟机(JSVirtualMachine)。每个虚拟机可以包含多个不同的上下文,并允许在这些不同的上下文之间传值(JSValue对象)。

然而,每个虚拟机都是完整且独立的,有其独立的堆空间和垃圾回收器(garbage collector ),GC无法处理别的虚拟机堆中的对象,因此你不能把一个虚拟机中创建的值传给另一个虚拟机。

线程和JavaScript的并发执行

JavaScriptCore API都是线程安全的。你可以在任意线程创建JSValue或者执行JS代码,然而,所有其他想要使用该虚拟机的线程都要等待。

如果想并发执行JS,需要使用多个不同的虚拟机来实现。

可以在子线程中执行JS代码。

通过下面这个demo来理解一下这个并发机制

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是:

四、 JSContext

一个JSContext对象代表一个JavaScript执行环境。在native代码中,使用JSContext去执行JS代码,访问JS中定义或者计算的值,并使JavaScript可以访问native的对象、方法、函数。

1. JSContext执行JS代码

调用evaluateScript函数可以执行一段top-level 的JS代码,并可向global对象添加函数和对象定义

其返回值是JavaScript代码中最后一个生成的值

API Reference

2. JSContext访问JS对象

一个JSContext对象对应了一个全局对象(global object)。例如web浏览器中中的JSContext,其全局对象就是window对象。在其他环境中,全局对象也承担了类似的角色,用来区分不同的JavaScript context的作用域。全局变量是全局对象的属性,可以通过JSValue对象或者context下标的方式来访问。

一言不合上代码:

Output:

这里列出了三种访问JavaScript对象的方法

通过context的实例方法objectForKeyedSubscript

通过context.globalObject的objectForKeyedSubscript实例方法

通过下标方式

设置属性也是对应的。

API Reference

五、 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所对应的值也会递归地进行拷贝和转换。

Output:

可见,JS中的对象可以直接转换成Objective-C中的NSDictionary,NSDictionary传入JavaScript也可以直接当作对象被使用。

3. NSArray与JS数组

NSArray对象与JavaScript中的array相互转转。其子元素也会递归地进行拷贝和转换。

Output:

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协议可选择性地导出属性和方法。

后面会详细讲解对象类型的转换。

六、 JSExport

JSExport协议提供了一种声明式的方法去向JavaScript代码导出Objective-C的实例类及其实例方法,类方法和属性。

1. 在JavaScript中调用native代码

两种方式:

1、Block

2、JSExport

Block的方式很简单,如下:

Output:

JSExport的方式需要通过继承JSExport协议的方式来导出指定的方法和属性:

继承于JSExport协议的MyPointExports协议中的实例变量,实例方法和类方法都会被导出,而MyPoint类的- (void)myPrivateMethod方法却不会被导出。 在OC代码中我们这样导出:

在JS代码中可以这样调用:

2. 导出OC方法和属性给JS

默认情况下,一个Objective-C类的方法和属性是不会导出给JavaScript的。你必须选择指定的方法和属性来导出。对于一个class实现的每个协议,如果这个协议继承了JSExport协议,JavaScriptCore就将这个协议的方法和属性列表导出给JavaScript。

对于每一个导出的实例方法,JavaScriptCore都会在prototype中创建一个存取器属性。对于每一个导出的类方法,JavaScriptCore会在constructor对象中创建一个对应的JavaScript function。

在Objective-C中通过@property声明的属性决定了JavaScript中的对应属性的特征:

Objective-C类中的属性,成员变量以及返回值都将根据JSValue指定的拷贝协议进行转换。

3. 函数名转换

转换成驼峰形式:

去掉所有的冒号

所有冒号后的第一个小写字母都会被转为大写

4. 自定义导出函数名

如果不喜欢默认的转换规则,也可以使用JSExportAs来自定义转换

5. 导出OC对象给JS

如何导出自定义的对象?

自定义对象有复杂的继承关系是如何导出的?

在讨论这个话题之前,我们首先需要对JavaScript中的对象与继承关系有所了解。

七、 JavaScript对象继承

如果你已经了解JavaScript的对象继承,可以跳过本节。

这里会快速介绍JavaScript对象继承的一些知识:

1. JavaScript的数据类型

最新的 ECMAScript 标准定义了 7 种数据类型:

6 种 原始类型:

Boolean

Null

Undefined

Number

String

Symbol (ECMAScript 6 新定义)

和 Object

2. JavaScript原始值

除 Object 以外的所有类型都是不可变的(值本身无法被改变)。我们称这些类型的值为“原始值”。

布尔类型:两个值:true 和 false

Null 类型:只有一个值: null

Undefined 类型:一个没有被赋值的变量会有个默认值 undefined

数字类型

字符串类型:不同于类 C 语言,JavaScript 字符串是不可更改的。这意味着字符串一旦被创建,就不能被修改

符号类型

3. JavaScript对象

在 Javascript 里,对象可以被看作是一组属性的集合。这些属性还可以被增减。属性的值可以是任意类型,包括具有复杂数据结构的对象。

以下代码构造了一个point对象:

4. JavaScript属性

ECMAScript定义的对象中有两种属性:数据属性和访问器属性。

数据属性 数据属性是键值对,并且每个数据属性拥有下列特性:

访问器属性 访问器属性有一个或两个访问器函数 (get 和 set) 来存取数值,并且有以下特性:

5. JavaScript属性设置与检测

设置一个对象的属性会只会修改或新增其自有属性,不会改变其继承的同名属性

调用一个对象的属性会依次检索本身及其继承的属性,直到检测到

Output:

在chrome的控制台中,我们分别打印设置x属性前后point对象的内部结构:

可见,设置一个对象的属性并不会修改其继承的属性,只会修改或增加其自有属性。

这里我们谈到了proto和继承属性,下面我们详细讲解。

八、 Prototype

JavaScript对于有基于类的语言经验的开发人员来说有点令人困惑 (如Java或C ) ,因为它是动态的,并且本身不提供类实现。(在ES2015/ES6中引入了class关键字,但是只是语法糖,JavaScript 仍然是基于原型的)。

当谈到继承时,Javascript 只有一种结构:对象。每个对象都有一个内部链接到另一个对象,称为它的原型 prototype。该原型对象有自己的原型,等等,直到达到一个以null为原型的对象。根据定义,null没有原型,并且作为这个原型链 prototype chain中的最终链接。

任何一个对象都有一个__proto__属性,用来表示其继承了什么原型。

以下代码定一个具有继承关系的对象,point对象继承了一个具有x,y属性的原型对象。

在Chrome的控制台中,我们打印对象结构:

可见继承关系,point继承的原型又继承了Object.prototype,而Object.prototype的__proto__指向null,因而它是继承关系的终点。 这里我们首先要知道prototype和__proto__是两种属性,前者只有function才有,后者所有的对象都有。后面会详细讲到。

1. JavaScript类?

Javascript 只有一种结构:对象。类的概念又从何而来?

在JavaScript中我们可以通过function来模拟类,例如我们定义一个MyPoint的函数,并把他认作MyPoint类,就可以通过new来创建具有x,y属性的对象

打印point对象结构:

这里出现一个constructor的概念

2. JavaScript constructor

每个JavaScript函数都自动拥有一个prototype的属性,这个prototype属性是一个对象,这个对象包含唯一一个不可枚举属性constructor。constructor属性值是一个函数对象

执行以下代码我们会发现对于任意函数 F.prototype.constructor == F

这里即存在一个反向引用的关系:

3. new发生了什么?

当调用new MyPoint(99, 66)时,虚拟机生成了一个point对象,并调用了MyPoint的prototype的constructor对象对point进行初始化,并且自动将MyPoint.prototype作为新对象point的原型。 相当于下面的伪代码

4. __proto__与prototype

简单地说:

__proto__是所有对象的属性,表示对象自己继承了什么对象

prototype是Function的属性,决定了new出来的新对象的__proto__

如图详细解释了两者的区别

5. 打印JavaScript对象结构

在浏览器提供的JavaScript调试工具中,我们可以很方便地打印出JavaScript对象的内部结构

在Mac/iOS客户端JavaScriptCore中并没有这样的打印函数,这里我自定义了一个打印函数

鉴于对象的内部结构容易出现循环引用导致迭代打印陷入死循环,我们在这里简单地处理,对属性不进行迭代打印。

为了描述对象的原型链,这里手动在对象末尾对其原型进行打印。

6. log

我们为所有的context都添加一个log函数,方便我们在JS中向控制台输出日志

九、 导出OC对象给JS

现在我们继续回到Objective-C中,看下OC对象是如何导出的

1. 简单对象的导出

当你从一个未指定拷贝协议的Objective-C实例创建一个JavaScript对象时,JavaScriptCore会创建一个JavaScript的wrapper对象。对于具体类型,JavaScriptCore会自动拷贝值到合适的JavaScript类型。

以下代码定义了一个继承自NSObject的简单类

导出对象

然后我们打印JavaScript中的d_point对象结构如下:

可见,其type属性并没有被导出。 JS中的对象原型是就是Object.prototype。

2. 继承关系的导出

在JavaScript中,继承关系是通过原型链(prototype chain)来支持的。对于每一个导出的Objective-C类,JavaScriptCore会在context中创建一个prototype。对于NSObject类,其prototype对象就是JavaScript context的Object.prototype。对于所有其他的Objective-C类,JavaScriptCore会创建一个prototype属性指向其父类的原型属性的原型对象。如此,JavaScript中的wrapper对象的原型链就反映了Objective-C中类型的继承关系。

我们让DPoint继承子MyPoint

在OC中,它的继承关系是这样的

在JS中,它的继承关系是这样的

打印对象结构来验证:

Output:

可见,DPoint自身的未导出的属性type没有在JS对象中反应出来,其继承的MyPoint的导出的属性和函数都在JS对象的原型中。

十、 内存管理

1. 循环引用

之前已经讲到, 每个JSValue对象都持有其JSContext对象的强引用,只要有任何一个与特定JSContext关联的JSValue被持有(retain),这个JSContext就会一直存活。如果我们将一个native对象导出给JavaScript,即将这个对象交由JavaScript的全局对象持有,引用关系是这样的:

这时如果我们在native对象中强引用持有JSContext或者JSValue,便会造成循环引用:

因此在使用时要注意以下几点:

2. 避免直接使用外部context

避免在导出的block/native函数中直接使用JSContext

使用 [JSContext currentContext] 来获取当前context能够避免循环引用

3. 避免直接使用外部JSValue

避免在导出的block/native函数中直接使用JSValue

这里我们使用了JSManagedValue来解决这个问题

十一、 JSManagedValue

一个JSManagedValue对象包含了一个JSValue对象,“有条件地持有(conditional retain)”的特性使其可以自动管理内存。

最基本的用法就是用来在导入到JavaScript的native对象中存储JSValue。

不要在在一个导出到JavaScript的native对象中持有JSValue对象。因为每个JSValue对象都包含了一个JSContext对象,这种关系将会导致循环引用,因而可能造成内存泄漏。

1. 有条件地持有

所谓“有条件地持有(conditional retain)”,是指在以下两种情况任何一个满足的情况下保证其管理的JSValue被持有:可以通过JavaScript的对象图找到该JSValue

可以通过native对象图找到该JSManagedValue。使用addManagedReference:withOwner:方法可向虚拟机记录该关系反之,如果以上条件都不满足,JSManagedValue对象就会将其value置为nil并释放该JSValue。

JSManagedValue对其包含的JSValue的持有关系与ARC下的虚引用(weak reference)类似。

2. 为什么不直接用虚引用?

通常我们使用weak来修饰block内需要使用的外部引用以避免循环引用,由于JSValue对应的JS对象内存由虚拟机进行管理并负责回收,这种方法不能准确地控制block内的引用JSValue的生命周期,可能在block内需要使用JSValue的时候,其已经被虚拟机回收。

API Reference

十二、 异常处理

JSContext的exceptionHandler属性可用来接收JavaScript中抛出的异常

默认的exceptionHandler会将exception设置给context的exception属性

因此,默认的表现就是从JavaScript中抛给native的未处理的异常又被抛回到JavaScript中,异常并未被捕获处理。

将context.exception设置为nil将会导致JavaScript认为异常已经被捕获处理。

扫码下方二维码,

随时关注更多前端干货文章!

微信:IMWebTech

0 人点赞