深刻理解JSCore

背景

动态化做为移动客户端技术的一个重要分支,一直是业界积极探索的方向。目前业界流行的动态化方案,如Facebook的React Native,阿里巴巴的Weex都采用了前端系的DSL方案,而它们在iOS系统上可以顺利的运行,都离不开一个背后的功臣:JavaScriptCore(如下简称JSCore),它创建起了Objective-C(如下简称OC)和JavaScript(如下简称JS)两门语言之间沟通的桥梁。不管是这些流行的动态化方案,仍是WebView Hybrid方案,亦或是以前普遍流行的JSPatch,JSCore都在其中发挥了举足轻重的做用。做为一名iOS开发工程师,了解JSCore已经逐渐成为了必备技能之一。javascript

从浏览器谈起

在iOS 7以后,JSCore做为一个系统级Framework被苹果提供给开发者。JSCore做为苹果的浏览器引擎WebKit中重要组成部分,这个JS引擎已经存在多年。若是想去追本溯源,探究JSCore的奥秘,那么就应该从JS这门语言的诞生,以及它最重要的宿主-Safari浏览器开始谈起。php

JavaScript历史简介

JavaScript诞生于1995年,它的设计者是Netscape的Brendan Eich,而此时的Netscape正是浏览器市场的霸主。html

而二十多年前,当时人们在浏览网页的体验极差,由于那会儿的浏览器几乎只有页面的展现能力,没有和用户的交互逻辑处理能力。因此即便一个必填输入框传空,也须要通过服务端验证,等到返回结果以后才给出响应,再加上当时的网速很慢,可能半分钟过去了,返回的结果是告诉你某个必填字段未填。因此Brendan花了十天写出了JavaScript,由浏览器解释执行,今后以后浏览器也有了一些基本的交互处理能力,以及表单数据验证能力。前端

而Brendan可能没有想到,在二十多年后的今天。JS这门解释执行的动态脚本语言,不光成为前端届的“正统”,还入侵了后端开发领域,在编程语言排行榜上进入前三甲,仅次于Python和Java。而如何解释执行JS,则是各家引擎的核心技术。目前市面上比较常见的JS引擎有Google的V8(它被运用在Android操做系统以及Google的Chrome上),以及咱们今天的主角–JSCore(它被运用在iOS操做系统以及Safari上)。java

WebKit

咱们天天都会接触浏览器,使用浏览器进行工做、娱乐。让浏览器可以正常工做最核心的部分就是浏览器的内核,每一个浏览器都有本身的内核,Safari的内核就是WebKit。WebKit诞生于1998年,并于2005年由Apple公司开源,Google的Blink也是在WebKit的分支上进行开发的。ios

WebKit由多个重要模块组成,经过下图咱们能够对WebKit有个总体的了解:git

简单点讲,WebKit就是一个页面渲染以及逻辑处理引擎,前端工程师把HTML、JavaScript、CSS这“三驾马车”做为输入,通过WebKit的处理,就输出成了咱们能看到以及操做的Web页面。从上图咱们能够看出来,WebKit由图中框住的四个部分组成。而其中最主要的就是WebCore和JSCore(或者是其它JS引擎),这两部分咱们会分红两个小章节详细讲述。除此以外,WebKit Embedding API是负责浏览器UI与WebKit进行交互的部分,而WebKit Ports则是让Webkit更加方便的移植到各个操做系统、平台上,提供的一些调用Native Library的接口,好比在渲染层面,在iOS系统中,Safari是交给CoreGraphics处理,而在Android系统中,Webkit则是交给Skia。github

WebCore

在上面的WebKit组成图中,咱们能够发现只有WebCore是红色的。这是由于时至今日,WebKit已经有不少的分支以及各大厂家也进行了不少优化改造,惟独WebCore这个部分是全部WebKit共享的。WebCore是WebKit中代码最多的部分,也是整个WebKit中最核心的渲染引擎。那首先咱们来看看整个WebKit的渲染流程:web

首先浏览器经过URL定位到了一堆由HTML、CSS、JS组成的资源文件,经过加载器(这个加载器的实现也很复杂,在此很少赘述)把资源文件给WebCore。以后HTML Parser会把HTML解析成DOM树,CSS Parser会把CSS解析成CSSOM树。最后把这两棵树合并,生成最终须要的渲染树,再通过布局,与具体WebKit Ports的渲染接口,把渲染树渲染输出到屏幕上,成为了最终呈如今用户面前的Web页面。sql

JSCore

概述

终于讲到咱们这期的主角 – JSCore。JSCore是WebKit默认内嵌的JS引擎,之因此说是默认内嵌,是由于不少基于WebKit分支开发的浏览器引擎都开发了自家的JS引擎,其中最出名的就是Chrome的V8。这些JS引擎的使命都相同,那就是解释执行JS脚本。而从上面的渲染流程图咱们能够看到,JS和DOM树之间存在着互相关联,这是由于浏览器中的JS脚本最主要的功能就是操做DOM树,并与之交互。一样的,咱们也经过一张图看下它的工做流程:

能够看到,相比静态编译语言生成语法树以后,还须要进行连接,装载生成可执行文件等操做,解释型语言在流程上要简化不少。这张流程图右边画框的部分就是JSCore的组成部分:Lexer、Parser、LLInt以及JIT的部分(之因此JIT的部分是用橙色标注,是由于并非全部的JSCore中都有JIT部分)。接下来咱们就搭配整个工做流程介绍每一部分,它主要分为如下三个部分:词法分析、语法分析以及解释执行。

PS:严格的讲,语言自己并不存在编译型或者是解释型,由于语言只是一些抽象的定义与约束,并不要求具体的实现,执行方式。这里讲JS是一门“解释型语言”只是JS通常是被JS引擎动态解释执行,而并非语言自己的属性。

词法分析 – Lexer

词法分析很好理解,就是把一段咱们写的源代码分解成Token序列的过程,这一过程也叫分词。在JSCore,词法分析是由Lexer来完成(有的编译器或者解释器把分词叫作Scanner)。

这是一句很简单的C语言表达式:

 

将其标记化以后能够获得下表的内容:

元素 标记类型
sum 标识符
= 赋值操做符
3 数字
+ 加法操做符
2 数字
; 语句结束

这就是词法分析以后的结果,可是词法分析并不会关注每一个Token之间的关系,是否匹配,仅仅是把它们区分开来,等待语法分析来把这些Token“串起来”。词法分析函数通常是由语法分析器(Parser)来进行调用的。在JSCore中,词法分析器Lexer的代码主要集中在parser/Lexer.h、Lexer.cpp中。

语法分析 – Parser

跟人类语言同样,咱们讲话的时候实际上是按照约定俗成,交流习惯按照必定的语法讲出一个又一个词语。那类比到计算机语言,计算机要理解一门计算机语言,也要理解一个语句的语法。例如如下一段JS语句:

 

Parser会把Lexer分析以后生成的token序列进行语法分析,并生成对应的一棵抽象语法树(AST)。这个树长什么样呢?在这里推荐一个网站:esprima Parser,输入JS语句能够立马生成咱们所需的AST。例如,以上语句就被生成这样的一棵树:

以后,ByteCodeGenerator会根据AST来生成JSCore的字节码,完成整个语法解析步骤。

解释执行 – LLInt和JIT

JS源代码通过了词法分析和语法分析这两个步骤,转成了字节码,其实就是通过任何一门程序语言必经的步骤–编译。可是不一样于咱们编译运行OC代码,JS编译结束以后,并不会生成存放在内存或者硬盘之中的目标代码或可执行文件。生成的指令字节码,会被当即被JSCore这台虚拟机进行逐行解释执行。

运行指令字节码(ByteCode)是JS引擎中很核心的部分,各家JS引擎的优化也主要集中于此。JSByteCode的解释执行是一套很复杂的系统,特别是加入了OSR和多级JIT技术以后,整个解释执行变的愈来愈高效,而且让整个ByteCode的执行在低延时之间和高吞吐之间有个很好的平衡:由低延时的LLInt来解释执行ByteCode,当遇到屡次重复调用或者是递归,循环等条件会经过OSR切换成JIT进行解释执行(根据具体触发条件会进入不一样的JIT进行动态解释)来加快速度。因为这部份内容较为复杂,并且不是本文重点,故只作简单介绍,不作深刻的讨论。

JSCore值得注意的Feature

除了以上部分,JSCore还有几个值得注意的Feature。

基于寄存器的指令集结构

JSCore采用的是基于寄存器的指令集结构,相比于基于栈的指令集结构(好比有些JVM的实现),由于不须要把操做结果频繁入栈出栈,因此这种架构的指令集执行效率更高。可是因为这样的架构也形成内存开销更大的问题,除此以外,还存在移植性弱的问题,由于虚拟机中的虚拟寄存器须要去匹配到真实机器中CPU的寄存器,可能会存在真实CPU寄存器不足的问题。

基于寄存器的指令集结构一般都是三地址或者二地址的指令集,例如:

 

在三地址的指令集中的运算过程是把a和b分别mov到两个寄存器,而后把这两个寄存器的值求和以后,存入第三个寄存器。这就是三地址指令运算过程。

而基于栈的通常都是零地址指令集,由于它的运算不依托于具体的寄存器,而是使用对操做数栈和具体运算符来完成整个运算。

单线程机制

值得注意的是,整个JS代码是执行在一条线程里的,它并不像咱们使用的OC、Java等语言,在本身的执行环境里就能申请多条线程去处理一些耗时任务来防止阻塞主线程。JS代码自己并不存在多线程处理任务的能力。可是为何JS也存在多线程异步呢?强大的事件驱动机制,是让JS也能够进行多线程处理的关键。

事件驱动机制

以前讲到,JS的诞生就是为了让浏览器也拥有一些交互,逻辑处理能力。而JS与浏览器之间的交互是经过事件来实现的,好比浏览器检测到发生了用户点击,会传递一个点击事件通知JS线程去处理这个事件。 那经过这一特性,咱们可让JS也进行异步编程,简单来说就是遇到耗时任务时,JS能够把这个任务丢给一个由JS宿主提供的工做线程(WebWorker)去处理。等工做线程处理完以后,会发送一个message让JS线程知道这个任务已经被执行完了,并在JS线程上去执行相应的事件处理程序。(可是须要注意,因为工做线程和JS线程并不在一个运行环境,因此它们并不共享一个做用域,故工做线程也不能操做window和DOM。)

JS线程和工做线程,以及浏览器事件之间的通讯机制叫作事件循环(EventLoop),相似于iOS的runloop。它有两个概念,一个是Call Stack,一个是Task Queue。当工做线程完成异步任务以后,会把消息推到Task Queue,消息就是注册时的回调函数。当Call Stack为空的时候,主线程会从Task Queue里取一条消息放入Call Stack来执行,JS主线程会一直重复这个动做直到消息队列为空。

以上这张图大概描述了JSCore的事件驱动机制,整个JS程序其实就是这样跑起来的。这个其实跟空闲状态下的iOS Runloop有点像,当基于Port的Source事件唤醒runloop以后,会去处理当前队列里的全部source事件。JS的事件驱动,跟消息队列实际上是“殊途同归”。也正由于工做线程和事件驱动机制的存在,才让JS有了多线程异步能力。

iOS中的JSCore

iOS7以后,苹果对WebKit中的JSCore进行了Objective-C的封装,并提供给全部的iOS开发者。JSCore框架给Swift、OC以及C语言编写的App提供了调用JS程序的能力。同时咱们也可使用JSCore往JS环境中去插入一些自定义对象。

iOS中可使用JSCore的地方有多处,好比封装在UIWebView中的JSCore,封装在WKWebView中的JSCore,以及系统提供的JSCore。实际上,即便同为JSCore,它们之间也存在不少区别。由于随着JS这门语言的发展,JS的宿主愈来愈多,有各类各样的浏览器,甚至是常见于服务端的Node.js(基于V8运行)。随时使用场景的不一样,以及WebKit团队自身不停的优化,JSCore逐渐分化出不一样的版本。除了老版本的JSCore,还有2008年宣布的运行在Safari、WKWebView中的Nitro(SquirrelFish)等等。而在本文中,咱们主要介绍iOS系统自带的JSCore Framework。

iOS官方文档对JSCore的介绍很简单,其实主要就是给App提供了调用JS脚本的能力。咱们首先经过JSCore Framework的15个开放头文件来“管中窥豹”,以下图所示:

乍一看,概念不少。可是除去一些公共头文件以及一些很细节的概念,其实真正经常使用的并很少,笔者认为颇有必要了解的概念只有4个:JSVM,JSContext,JSValue,JSExport。鉴于讲述这些概念的文章已经有不少,本文尽可能从一些不一样的角度(好比原理,延伸对比等)去解释这些概念。

JSVirtualMachine

一个JSVirtualMachine(如下简称JSVM)实例表明了一个自包含的JS运行环境,或者是一系列JS运行所需的资源。该类有两个主要的使用用途:一是支持并发的JS调用,二是管理JS和Native之间桥对象的内存。

JSVM是咱们要学习的第一个概念。官方介绍JSVM为JavaScript的执行提供底层资源,而从类名直译过来,一个JSVM就表明一个JS虚拟机,咱们在上面也提到了虚拟机的概念,那咱们先讨论一下什么是虚拟机。首先咱们能够看看(多是)最出名的虚拟机——JVM(Java虚拟机)。 JVM主要作两个事情:

  1. 首先它要作的是把JavaC编译器生成的ByteCode(ByteCode其实就是JVM的虚拟机器指令)生成每台机器所须要的机器指令,让Java程序可执行(以下图)。
  2. 第二步,JVM负责整个Java程序运行时所须要的内存空间管理、GC以及Java程序与Native(即C,C++)之间的接口等等。

从功能上来看,一个高级语言虚拟机主要分为两部分,一个是解释器部分,用来运行高级语言编译生成的ByteCode,还有一部分则是Runtime运行时,用来负责运行时的内存空间开辟、管理等等。实际上,JSCore经常被认为是一个JS语言的优化虚拟机,它作着JVM相似的事情,只是相比静态编译的Java,它还多承担了把JS源代码编译成字节码的工做。

既然JSCore被认为是一个虚拟机,那JSVM又是什么?实际上,JSVM就是一个抽象的JS虚拟机,让开发者能够直接操做。在App中,咱们能够运行多个JSVM来执行不一样的任务。并且每个JSContext(下节介绍)都从属于一个JSVM。可是须要注意的是每一个JSVM都有本身独立的堆空间,GC也只能处理JSVM内部的对象(在下节会简单讲解JS的GC机制)。因此说,不一样的JSVM之间是没法传递值的。

值得注意的还有,在上面的章节中,咱们提到的JS单线程机制。这意味着,在一个JSVM中,只有一条线程能够跑JS代码,因此咱们没法使用JSVM进行多线程处理JS任务。若是咱们须要多线程处理JS任务的场景,就须要同时生成多个JSVM,从而达到多线程处理的目的。

JS的GC机制

JS一样也不须要咱们去手动管理内存。JS的内存管理使用的是GC机制(Tracing Garbage Collection)。不一样于OC的引用计数,Tracing Garbage Collection是由GCRoot(Context)开始维护的一条引用链,一旦引用链没法触达某对象节点,这个对象就会被回收掉。以下图所示:

JSContext

一个JSContext表示了一次JS的执行环境。咱们能够经过建立一个JSContext去调用JS脚本,访问一些JS定义的值和函数,同时也提供了让JS访问Native对象,方法的接口。

JSContext是咱们在实际使用JSCore时,常常用到的概念之一。”Context”这个概念咱们都或多或少的在其它开发场景中见过,它最常被翻译成“上下文”。那什么是上下文?好比在一篇文章中,咱们看到一句话:“他飞快的跑了出去。”可是若是咱们不看上下文的话,咱们并不知道这句话到底是什么意思:谁跑了出去?他是谁?他为何要跑?

写计算机理解的程序语言跟写文章是类似的,咱们运行任何一段语句都须要有这样一个“上下文”的存在。好比以前外部变量的引入、全局变量、函数的定义、已经分配的资源等等。有了这些信息,咱们才能准确的执行每一句代码。

同理,JSContext就是JS语言的执行环境,全部JS代码的执行必须在一个JSContext之中,在WebView中也是同样,咱们能够经过KVC的方式获取当时WebView的JSContext。经过JSContext运行一段JS代码十分简单,以下面这个例子:

 

借助evaluateScript API,咱们就能够在OC中搭配JSContext执行JS代码。它的返回值是JS中最后生成的一个值,用属于当前JSContext中的JSValue(下一节会有介绍)包裹返回。

咱们还能够经过KVC的方式,给JSContext塞进去不少全局对象或者全局函数:

 

这是一个很好用并且很重要的特性,有不少著名的借助JSCore的框架如JSPatch,都利用了这个特性去实现一些很巧妙的事情。在这里咱们不过多探讨能够利用它作什么,而是去研究它到底是怎样运做的。在JSContext的API中,有一个值得注意的只读属性 – JSValue类型的globalObject。它返回当前执行JSContext的全局对象,例如在WebKit中,JSContext就会返回当前的Window对象。而这个全局对象其实也是JSContext最核心的东西,当咱们经过KVC方式与JSContext进去取值赋值的时候,实际上都是在跟这个全局对象作交互,几乎全部的东西都在全局对象里,能够说,JSContext只是globalObject的一层壳。对于上述两个例子,本文取了context的globalObject,并转成了OC对象,以下图:

能够看到这个globalObject保存了全部的变量与函数,这更加印证了上文的说法(至于为何globalObject对应OC对象是NSDictionary类型,咱们将在下节中讲述)。因此咱们还能得出另一个结论,JS中所谓的全局变量,全局函数不过是全局对象的属性和函数。

同时值得注意的是,每一个JSContext都从属于一个JSVM。咱们能够经过JSContext的只读属性 – virtualMachine得到当前JSContext绑定的JSVM。JSContext和JSVM是多对一的关系,一个JSContext只能绑定一个JSVM,可是一个JSVM能够同时持有多个JSContext。而上文中咱们提到,每一个JSVM同时只有整个一个线程来执行JS代码,因此综合来看,一次简单的经过JSCore运行JS代码,并在Native层获取返回值的过程大体以下:

JSValue

JSValue实例是一个指向JS值的引用指针。咱们可使用JSValue类,在OC和JS的基础数据类型之间相互转换。同时咱们也可使用这个类,去建立包装了Native自定义类的JS对象,或者是那些由Native方法或者Block提供实现JS方法的JS对象。

在JSContext一节中,咱们接触了大量的JSValue类型的变量。在JSContext一节中咱们了解到,咱们能够很简单的经过KVC操做JS全局对象,也能够直接得到JS代码执行结果的返回值(同时每个JS中的值都存在于一个执行环境之中,也就是说每一个JSValue都存在于一个JSContext之中,这也就是JSValue的做用域),都是由于JSCore帮咱们用JSValue在底层自动作了OC和JS的类型转换。

JSCore一共提供了以下10种类型互换:

 

同时还提供了对应的互换API(节选):

 

在讲类型转换前,咱们先了解一下JS这门语言的变量类型。根据ECMAScript(能够理解为JS的标准)的定义:JS中存在两种数据类型的值,一种是基本类型值,它指的是简单的数据段。第二种是引用类型值,指那些可能由多个值构成的对象。基本类型值包括”undefined”,”nul”,”Boolean”,”Number”,”String”(是的,String也是基础类型),除此以外都是引用类型。对于前五种基础类型的互换,应该没有太多要讲的。接下来会重点讲讲引用类型的互换:

NSDictionary <–> Object

在上节中,咱们把JSContext的globalObject转换成OC对象,发现是NSDictionary类型。要搞清楚这个转换,首先咱们对JS这门语言面向对象的特性进行一个简单的了解。在JS中,对象就是一个引用类型的实例。与咱们熟悉的OC、Java不同,对象并非一个类的实例,由于在JS中并不存在类的概念。ECMA把对象定义为:无序属性的集合,其属性能够包含基本值、对象或者函数。从这个定义咱们能够发现,JS中的对象就是无序的键值对,这和OC中的NSDictionary,Java中的HashMap何其类似。

 

在上面的实例代码中,笔者使用了相似的方式建立了JS中的对象(在JS中叫“对象字面量”表示法)与OC中的NSDictionary,相信能够更有助理解这两个转换。

NSBlock <–> Function Object

在上节的例子中,笔者在JSContext赋值了一个”globalFunc”的Block,并能够在JS代码中当成一个函数直接调用。我还可使用”typeof”关键字来判断globalFunc在JS中的类型:

 

经过这个例子,咱们也能发现传入的Block对象在JS中已经被转成了”function”类型。”Function Object”这个概念对于咱们写惯传统面向对象语言的开发者来讲,可能会比较晦涩。而实际上,JS这门语言,除了基本类型之外,就是引用类型。函数实际上也是一个”Function”类型的对象,每一个函数名实则是指向一个函数对象的引用。好比咱们能够这样在JS中定义一个函数:

 

同时咱们还能够这样定义一个函数(不推荐):

 

按照第二种写法,咱们就能很直观的理解到函数也是对象,它的构造函数就是Function,函数名只是指向这个对象的指针。而NSBlock是一个包裹了函数指针的类,JSCore把Function Object转成NSBlock对象,能够说是很合适的。

JSExport

实现JSExport协议能够开放OC类和它们的实例方法,类方法,以及属性给JS调用。

除了上一节提到的几种特殊类型的转换,咱们还剩下NSDate类型,与id、class类型的转换须要弄清楚。而NSDate类型无需赘述,因此咱们在这一节重点要弄清楚后二者的转换。

而一般状况下,咱们若是想在JS环境中使用OC中的类和对象,须要它们实现JSExport协议,来肯定暴露给JS环境中的属性和方法。好比咱们须要向JS环境中暴露一个Person的类与获取名字的方法:

 

而后,咱们能够把一个JSExportPerson的一个实例传入JSContext,而且能够直接执行fullName方法:

 

这就是一个很简单的使用JSExport的例子,但请注意,咱们只能调用在该对象在JSExport中开放出去的方法,若是并未开放出去,如上例中的”sayFullName”方法,直接调用则会报TypeError错误,由于该方法在JS环境中并未被定义。

讲完JSExport的具体使用方法,咱们来看看咱们最开始的问题。当一个OC对象传入JS环境以后,会转成一个JSWrapperObject。那问题来了,什么是JSWrapperObject?在JSCore的源码中,咱们能够找到一些线索。首先在JSCore的JSValue中,咱们能够发现这样一个方法:

 

这个API能够传入任意一个类型的OC对象,而后返回一个持有该OC对象的JSValue。那这个过程确定涉及到OC对象到JS对象的互换,因此咱们只要分析一下这个方法的源码(基于这个分支进行分析)。因为源码实现过长,咱们只须要关注核心代码,在JSContext中有一个”wrapperForObjCObject”方法,而实际上它又是调用了JSWrapperMap的”jsWrapperForObject”方法,这个方法就能够解答全部的疑惑:

 

在咱们建立”JSWrapperObject”的对象过程当中,咱们会经过JSWrapperMap来为每一个传入的对象建立对应的JSObjCClassInfo。这是一个很是重要的类,它有这个类对应JS对象的原型(Prototype)与构造函数(Constructor)。而后由JSObjCClassInfo去生成具体OC对象的JSWrapper对象,这个JSWrapper对象中就有一个JS对象所须要的全部信息(即Prototype和Constructor)以及对应OC对象的指针。以后,把这个jsWrapper对象写入JS环境中,便可在JS环境中使用这个对象了。这也就是”JSWrapperObject”的真面目。而咱们上文中提到,若是传入的是类,那么在JS环境中会生成constructor对象,那么这点也很容易从源码中看到,当检测到传入的是类的时候(类自己也是个对象),则会直接返回constructor属性,这也就是”constructor object”的真面目,实际上就是一个构造函数。

那如今还有两个问题,第一个问题是,OC对象有本身的继承关系,那么在JS环境中如何描述这个继承关系?第二个问题是,JSExport的方法和属性,又是如何让JS环境中调用的呢?

咱们先看第一个问题,继承关系要如何解决?在JS中,继承是经过原型链来实现,那什么是原型呢?原型对象是一个普通对象,并且就是构造函数的一个实例。全部经过该构造函数生成的对象都共享这一个对象,当查找某个对象的属性值,结果不存在时,这时就会去对象的原型对象继续找寻,是否存在该属性,这样就达到了一个封装的目的。咱们经过一个Person原型对象快速了解:

 

而原型链就是JS中实现继承的关键,它的本质就是重写构造函数的原型对象,连接另外一个构造函数的原型对象。这样查找某个对象的属性,会沿着这条原型链一直查找下去,从而达到继承的目的。咱们经过一个例子快速了解一下:

 

而咱们在生成对象的classinfo的时候(具体代码见”allocateConstructorAndPrototypeWithSuperClassInfo”),还会生成父类的classInfo。对每一个实现过JSExport的OC类,JSContext里都会提供一个prototype。好比NSObject类,在JS里面就会有对应的Object Prototype。对于其它的OC类,会建立对应的Prototype,这个prototype的内部属性[Prototype]会指向为这个OC类的父类建立的Prototype。这个JS原型链就能反应出对应OC类的继承关系,在上例中,Person.prototype被赋值为一个mammal的实例对象,即原型的连接过程。

讲完第一个问题,咱们再来看看第二个问题。那JSExport是如何暴露OC方法到JS环境的呢?这个问题的答案一样出如今咱们生成对象的classInfo的时候:

 

对于每一个声明在JSExport里的属性和方法,classInfo会在prototype和constructor里面存入对应的property和method。以后咱们就能够经过具体的methodName和PropertyName生成的setter和getter方法,来获取实际的SEL。最后就可让JSExport中的方法和属性获得正确的访问。因此简单点讲,JSExport就是负责把这些方法打个标,以methodName为key,SEL为value,存入一个map(prototype和constructor本质上就是一个Map)中去,以后就能够经过methodName拿到对应的SEL进行调用。这也就解释了上例中,咱们调用一个没有在JSExport中开放的方法会显示undefined,由于生成的对象里根本没有这个key。

总结

JSCore给iOS App提供了JS能够解释执行的运行环境与资源。对于咱们实际开发而言,最主要的就是JSContext和JSValue这两个类。JSContext提供互相调用的接口,JSValue为这个互相调用提供数据类型的桥接转换。让JS能够执行Native方法,并让Native回调JS,反之亦然。

利用JSCore,咱们能够作不少有想象空间的事。全部基于JSCore的Hybrid开发基本就是靠上图的原理来实现互相调用,区别只是具体的实现方式和用途不大相同。大道至简,只要正确理解这个基本流程,其它的全部方案不过是一些变通,均可以很快掌握。

一些引伸阅读

JSPatch的对象和方法没有实现JSExport协议,JS是如何调OC方法的?

JS调OC并非经过JSExport。经过JSExport实现的方式有诸多问题,咱们须要先写好Native的类,并实现JSExport协议,这个自己就不能知足“Patch”的需求。

因此JSPatch另辟蹊径,使用了OC的Runtime消息转发机制作这个事情,以下面这一个简单的JSPatch调用代码:

 
  1. require在全局做用域里生成UIView变量,来表示这个对象是一个OCClass。
  2. 经过正则把.alloc()改为._c(‘alloc’),来进行方法收口,最终会调用_methodFunc()把类名、对象、MethodName经过在Context早已定义好的Native方法,传给OC环境。
  3. 最终调用OC的CallSelector方法,底层经过从JS环境拿到的类名、方法名、对象以后,经过NSInvocation实现动态调用。

JSPatch的通讯并无经过JSExport协议,而是借助JSCore的Context与JSCore的类型转换和OC的消息转发机制来完成动态调用,实现思路真的很巧妙。

桥方法的实现是怎么经过JSCore交互的?

市面上常见的桥方法调用有两种:

  1. 经过UIWebView的delegate方法:shouldStartLoadWithRequest来处理桥接JS请求。JSRequest会带上methodName,经过WebViewBridge类调用该method。执行完以后,会使用WebView来执行JS的回调方法,固然实际上也是调用的WebView中的JSContext来执行JS,完成整个调用回调流程。
  2. 经过UIWebView的delegate方法:在webViewDidFinishLoadwebViewDidFinishLoad里经过KVC的方式获取UIWebView的JSContext,而后经过这个JSContext设置已经准备好的桥方法供JS环境调用。

参考资料

  1. 《JavaScript高级程序设计》
  2. Webkit Architecture
  3. 虚拟机随谈1:解释器…
  4. 戴铭:深刻剖析 WebKit
  5. JSCore-Wiki
  6. [知乎Tw93]iOS中的JSCore

https://tech.meituan.com/2018/08/23/deep-understanding-of-jscore.html

相关文章
相关标签/搜索