| 导语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