简述Chromium, CEF, Webkit, JavaScriptCore, V8, Blink

浏览器内核分为两部分:渲染引擎(render engin)、js引擎(js engin)

  • 渲染引擎:负责对网页语法的解释(HTML、javaScript、引入css等),并渲染(显示)网页

  • js引擎:javaScript的解释、编译、执行

主流内核:Trident(IE)、Gecko(FireFox)、Webkit(Safari)、Presto(opera前内核、已废弃)、blink(Chrome)

  1. webkit浏览器渲染引擎的进度史

  1. webkit浏览器内核部分内容介绍

HTML解释器:解释HTML文本的解释器,主要作用是将HTML文本解释成DOM树,DOM是一种文档表示方法。

CSS解释器:级联样式表的解释器,它的作用是为DOM中的各个元素对象计算出样式信息,从而为计算最后网页的布局提供基础设施。

布局:在DOM创建之后,webkit需要将其中的元素对象同样式信息结合起来,计算它们的大小位置等布局信息,形成一个能够表示这所有信息的内部比偶表示模型。

JavaScript引擎:使用JavaScript代码可以修改网页的内容,也能修改CSS的信息,JavaScript引擎能解释JavaScript代码并通过DOM接口和CSSOM视图模式来修改网页内容和样式信息,从而改变渲染结果。

绘图:使用图形库将布局计算后的各个网页的节点绘制成图像结果。

V8(在这里详细了解)

Webkit (refer: Webkit技术内幕)

苹果公司基于KDE(Linux桌面系统)的KHTML开源,包括Webcore和JavaScriptCore两个引擎。苹果比较了Gecko和KHTML之后,选择后者的原因,是KHTML拥有清晰的源码结构和极快的渲染速度。2008年,谷歌公司发布的chrome浏览器,采用的chromium内核是基于Webkit而来应用:safari, mail, app store等应用webkit布局:当webkit创建RenderObject对象之后,每个对象是不知道自己的位置、大小等信息的,webkit根据盒模型来计算他们的位置、大小等信息的过程称为布局计算/排版。

布局计算分类:第一类是对整个RenderObject树进行的计算;第二类是对RenderObject树中某个子树的计算,常见于文本元素或者overflow:auto块的计算。布局计算:布局计算是一个递归的过程,这是因为一个节点的大小通常需要先计算它的子节点的位置、大小等信息。

补充: 为什么说transform实现动画较直接设置几何属性性能较好?

1.webkit渲染过程:style -> Layout(reflow发生在这) -> Paint(repaint发生在这) -> Composite,transform是位于Composite(渲染层合并),而width、left、margin等则是位于Layout(布局)层,这必定导致reflow。

2.现代浏览器针对transform等开启GPU加速。

style -> Layout(reflow发生在这) -> Paint(repaint发生在这) -> Composite(transform发生在这个时候)

(refer: CSS Animation性能优化 从重绘重排角度讲解transform的动画性能

JavaScriptCore 引擎一开始它是基于KJS开发的,是基于抽象语法树的解释器,性能较差。2008年苹果重写了编译器和字节码解释器,叫SquirrelFish

(refer:JavaScriptCore深度解析

Chromium/BlinkChromium基于webkit,却把Webkit的代码梳理得可读性更高,多进程框架Chromium加载网页的过程,需要Browser进程和Render进程协作完成。加载网页的过程由Browser进程发起,向服务器请求网页内容的过程也是由Browser进程完成。Render进程负责对下载回来的网页内容进行解析,解析之后得到一个DOM Tree。有了这个DOM Tree之后,Render进程就可以对网页进行渲染了(refer: Chromium网页加载介绍)Blink:Chromium项目中研发的渲染引擎,基于并脱离Webkit更强大的渲染和布局:

    追求多线程布局

    样式的重新计算

    不为隐藏的iframe创建渲染器

    为插件设置为display:none时,修复插件卸载等旧错误

    异步卸载iframe,速度更快

(refer: blink的开发者FAQ

  1. CEF(Chromium Embeded Framework)

一个将浏览器功能(页面渲染、js执行)嵌入到其他应用程序的框架,支持windows, Linux, Mac平台

CEF的历史:

    CEF有两种版本的Chromium Embedded Framework:CEF 1和CEF 3

    在Chromium Content API出现后,CEF 2的开发被放弃。

    CEF 1是基于Chromium WebKit API的单进程实现。它不再积极发展或支持。

    CEF 3是基于Chromium Content API的多进程实现,其性能类似于Google Chrome。

    应用:

    1)做一个浏览器

    1. 跨平台的桌面底层方案electron.js

    2. 客户端(如:桌面端app应用)

    好处:开发web和native混合的应用非常方便

    小结

    V8引擎是谷歌公司自主研发的js引擎

    Webkit基于KHTML,包含JavaScriptCore

    Chromium基于Webkit,衍生出Blink CEF是包含Chromium浏览器的应用框架

    一切鼻祖:KHTML KJS

    深入理解JSCore

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

    从浏览器谈起

    在iOS 7之后,JSCore作为一个系统级Framework被苹果提供给开发者。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(它被运用在iOS操作系统以及Safari上)。

    WebKit

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

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

    简单点讲,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。

    WebCore

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

    首先浏览器通过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树,并与之交互。同样的,我们也通过一张图看下它的工作流程:

    可以看到,相比静态编译语言生成语法树之后,还需要进行链接,装载生成可执行文件等操作,解释型语言在流程上要简化很多。这张流程图右边画框的部分就是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语句:

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

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

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

    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),类似于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有了多线程异步能力。

    参考

    1. https://juejin.im/post/5c0492a36fb9a049e82b435a
    2. https://cloud.tencent.com/developer/article/1367129

    (文中有些细节仅做参考,有些问题,我后面有空修改下)



    请遵守《互联网环境法规》文明发言,欢迎讨论问题
扫码反馈

扫一扫,反馈当前页面

咨询反馈
扫码关注
返回顶部