动态化技术如果不知道它,你就白学了!

关注公众号:wingjay

第 17 篇

人和人的差距,是下班后的 4 小时拉开的。

读者朋友,你好,我是 wingjay。

之前的文章《2019年移动开发,我的求变之路》里,我提到动态化方向是 2019 年移动领域一个重要的方向,也是本公众号着重关注的技术点之一。而目前来看,动态化领域的一大主流技术就是基于 JavaScript 与 Native 进行通信,借助 Js 本身的动态性来实现业务逻辑与视图的动态化。而这背后,离不开一个强大的后盾:JavaScriptCore。

熟悉 Java 或 Android 的读者都知道,Java 代码是运行在虚拟机(如JVM / Davilk)上的,而 JsCore 就相当于是 Js 的虚拟机。它正是这套动态化方案的核心所在。

WebView 大家都清楚,它内部包含两大模块:WebCore 和 JsCore,前者用来解析渲染 Html 和 CSS,后者则是用来解析执行 Js 代码。而 RN 和 Weex 这类技术方案和 WebView 本质区别在于:前者抛弃了繁重的 WebCore 和 Html 语法,而是利用 React/Vue 自己实现了一套Native渲染方式。但两者的共同点,就在于它们都依赖 JsCore 来执行 Js。

换句话说,没有 JsCore,就不会诞生 RN、Weex 这类优秀的动态化技术。而今天,我们就顺着动态化这条路线,一起来学习下 JsCore 的原理。今天的文章来自美团技术团队:唐笛,文章很硬,大家可以收藏起来慢慢品味。

文章所有删减以适合 Android 读者阅读,文末可查看原文。

正文

背景

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

从浏览器谈起

JSCore作为浏览器引擎WebKit中重要组成部分,这个JS引擎已经存在多年。如果想去追本溯源,探究JSCore的奥秘,那么就应该从JS这门语言的诞生,以及它最重要的宿主-Safari浏览器开始谈起。

JavaScript历史简介

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

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

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

WebKit

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

WebKit由多个重要模块组成,通过下图我们可以对WebKit有个整体的了解:

å¾ç1

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

WebCore

在上面的WebKit组成图中,我们可以发现只有WebCore是红色的。这是因为时至今日,WebKit已经有很多的分支以及各大厂家也进行了很多优化改造,唯独WebCore这个部分是所有WebKit共享的。WebCore是WebKit中代码最多的部分,也是整个WebKit中最核心的渲染引擎。那首先我们来看看整个WebKit的渲染流程:

å¾ç2

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

JSCore

概述

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

å¾ç3

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

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

词法分析:Lexer

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

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

sum = 3 + 2; 

将其标记化之后可以得到下表的内容:

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

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

语法分析:Parser

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

var sum = 2 + 3;
var a = sum + 5;

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

å¾ç4

之后,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寄存器不足的问题。

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

i = a + b;
//转成三地址指令:
add i,a,b; //把a寄存器中的值和b寄存器中的值相加,存入i寄存器

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

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

单线程机制

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

事件驱动机制

之前讲到,JS的诞生就是为了让浏览器也拥有一些交互,逻辑处理能力。而JS与浏览器之间的交互是通过事件来实现的,比如浏览器检测到发生了用户点击,会传递一个点击事件通知JS线程去处理这个事件。

那通过这一特性,我们可以让JS也进行异步编程,简单来讲就是遇到耗时任务时,JS可以把这个任务丢给一个由JS宿主提供的工作线程(WebWorker)去处理。等工作线程处理完之后,会发送一个message让JS线程知道这个任务已经被执行完了,并在JS线程上去执行相应的事件处理程序。(但是需要注意,由于工作线程和JS线程并不在一个运行环境,所以它们并不共享一个作用域,故工作线程也不能操作window和DOM。)

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

å¾ç5

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

JSCore结构

实际上,即使同为JSCore,它们之间也存在很多区别。因为随着JS这门语言的发展,JS的宿主越来越多,有各种各样的浏览器,甚至是常见于服务端的Node.js(基于V8运行)。随时使用场景的不同,以及WebKit团队自身不停的优化,JSCore逐渐分化出不同的版本。除了老版本的JSCore,还有2008年宣布的运行在Safari、WKWebView中的Nitro(SquirrelFish)等等。而在本文中,我们主要介绍移动端系统自带的JSCore Framework。

笔者认为很有必要了解的概念只有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++)之间的接口等等。

å¾ç7

从功能上来看,一个高级语言虚拟机主要分为两部分,一个是解释器部分,用来运行高级语言编译生成的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)开始维护的一条引用链,一旦引用链无法触达某对象节点,这个对象就会被回收掉。如下图所示:

å¾ç8

总结

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

å¾ç11

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

----

公众号:wingjay

人和人的差距,是下班后的 4 小时拉开的。

长期输出有价值 Android 技术内容,更重要的是,原创文章末尾会有随机抽奖哟

----

在上一篇文章《说一件重要的事。》中可查看上一个“一文一题”。

更多文章:

2019年移动开发,我的求变之路

Flutter 跨平台实践及原理探秘

支付宝的 Hybrid 架构是如何建设起来的?

Android 架构之长连接技术

猜你喜欢

转载自blog.csdn.net/lanxian837820149/article/details/89202721
今日推荐