前言
本文章将从以下三点来讲述webkit的工作流程及一些实现原理,帮助你更好地理解浏览器工作流程:
浏览器内核
网页渲染
JavaScript引擎
下面,让我们开启webKit探索之旅吧~
一、浏览器内核
-
浏览器特性
随着B/S架构的流行,浏览器变得越来越重要。而一个浏览器往往需要以下这些功能:
-
用户代理(User Agent)
User Agent用于表明浏览器身份,因为一个网页在不同的浏览器往往有不同的展示,所以需要根据浏览器身份发送不同的网页内容。浏览器控制台输入 navigator.userAgent 就会得到以下字符串
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36"
此次测试(使用Chrome)浏览器不仅包含Chrome 还加入了Mozilla ,AppleWebKit , Safari的额外代理,表示浏览器也能兼容其定制内容的页面,这样就能拿到最新功能的页面了。
-
浏览器内核
在浏览器,将网络资源转换为可视化界面的模块就称为浏览器内核(也叫渲染引擎)。
目前主流内核:
浏览器 | Chrome | IE | 火狐 | Safari |
---|---|---|---|---|
内核 | Blink(基于WebKit) | Trident | Gecko | WebKit |
渲染引擎主要由以下组成:
渲染引擎在基础模块的基础上主要包含:
渲染过程:
上图大体概括了浏览器渲染的大体过程,这也很好解释了为什么不在DOM里面夹杂JavaScript代码,容易造成线程切换。
webKit的渲染过程大致可分成三大阶段(三阶段后面又细分具体过程):
从URL到构建完DOM树
- 用户输入网页URL时,webKit调用资源加载器加载对应资源
- 加载器依赖网络模块建立连接,发送请求
- 收到的网页被HTML解释器转变成DOM树
- 如果遇到JavaScript代码,调用js引擎解释执行
- DOM树构建完触发DOMContent事件,DOM树构建完成及网页依赖资源都加载完成后触发onload事件。
从DOM树构建完webKit的绘图上下文
- css文件被解释成内部结构,附加在DOM树形成renderObject渲染树。
- 渲染树创建,webKit同时会构建renderLayer层次树和一个虚拟的绘图上下文。
从绘图上下文到生成图像
- 绘图实现类结合图像库将绘制的结果返回给浏览器
-
webKit
webKit是苹果公司开源的一个项目,被很多浏览器采用为内核,Chrome的Blink就是后面从webKit分支出去发展的。其整体架构如下图:
而webKit2则是在webKit基础上一组支持新架构的接口层。该接口与网页渲染工作代码不在同一个进程,实现了chromium多进程的优点。webKit2接口使用不需要接触背后的多进程机制。
如上图,网页渲染在web进程与webKit2所在的UI进程不是同一进程。
-
chromium
chromium浏览器使用的也是基于webKit的Blink引擎,它相当于chrome的创新版,一些新技术都会先在chromium上实验。在chromium,webKit只是它的一部分。其中content模块和接口是对chromium渲染网页功能的抽象,它在webKit的上层渲染网页,以便可以使用沙箱模型和跨进程GPU等机制。相当于封装内层,提供content接口层让人调用。
-
多进程模型
多进程模型的优势:
上图是chromium的多进程模型,其中连线代表IPC进程间通信,chromium浏览器主要进程类型有:
- 浏览器的主进程,负责页面的显示和各页面的管理,是所有其他类型线程的祖先,负责它们的创建与销毁,有且仅有一个。
- 网页的渲染线程,可能有多个,不一定和网页数量相同
- 进程为创建NPAPI类型的插件创建,每种类型插件只会创建一次,插件进程共享
- 最多只有一个,GPU硬件加速时才创建
- 同NPAPI插件进程 ,为创建Pepper类型的插件创建。
- 分场景使用,例如Linux的”Zygote“进程,”Sandbox“准备进程。
多进程模型下网页的渲染:
Renderer创建方式
Chromium允许用户配置 Renderer 进程的创建方式,有以下四种方式:
- Process-per-site-instance:每个页面都创建一个独立的渲染线程
- Process-per-site:同一个域的页面共享同一线程
- Process-per-tab(Chromium默认):每个标签页都创建一个独立的渲染线程
- Single process:不为页面创建任何独立线程,渲染在Browser进程进行。主要在Android WebView使用。
下图为WebKit由内到外的交互:
-
webKit2与chromium的区别
首先,两者都是多进程架构的模型,两者的根本目的都要实现UI和渲染的分离,区别在于设计理念:
二、网页加载与渲染
-
webKit资源加载机制
资源缓存
当webKit请求资源时,先从资源池查找是否存在相应的资源(通过URL,不同的URL被认为不同的资源。),如果有,直接取出使用,否则创建一个新的CachedResource子类的对象并真正发送请求给服务器,当webKit收到资源后将其设置到该资源类的对象中去,以便内存缓存后下次使用。
资源加载器
webKit共有三种类型的加载器:
- 针对每种资源类型的特定加载器,仅加载某一种资源:例如ImageLoader
- 资源缓存机制的资源加载器,所有的特定加载器都可以共享它所查找出来的缓存资源--CacheResourceLoader类。
- 通用的资源加载器--resourceLoader类,webKit使用该类只负责获取资源的数据,属于CachedResource类,但不是继承CacheResourceLoader类。
通常资源的加载是异步执行的,这样不会阻碍WebKit的渲染过程。webKit能够并发下载资源以及下载JavaScript代码。
-
DOM树
DOM结构的基本要素就是“节点”,整个文档(Document)也是一个节点,称文档节点。除了文档节点还有元素节点,属性节点,注释节点等等。
Document继承Node,具有一些属性和方法。
-
HTML解释器
HTML解释器的工作就是讲网页资源由字节流解释成DOM树结构。
大体概括为:
-
事件机制
事件在工作分为两主体,一是事件,二是事件目标。Node节点继承EventTarget类。下图接口用来注册和移除监听。
当渲染引擎收到事件,它会检查哪个元素是直接的事件目标,事件会经过自顶向下捕获和自底向上冒泡的两个过程。
-
RenderObject树
在DOM树构建完成后,webKit要为DOM树节点构建RenderObject树,一个RenderObject对应绘制DOM节点所需要的各种信息。那么问题来了,DOM树哪些节点需要创建RenderObject呢?大概有以下三类:
- document节点
- 可视节点,如body,div 。(ps: 不可视节点比如head script等)
- 匿名的RenderObject 不对应任何DOM,例如RenderBlock
-
webKit布局
当webKit创建RenderObject对象后,根据框模型计算各对象的位置,大小等信息的过程称为布局计算。
Frame类用于表示网页的框结构,每个框都有一个frameView类表示框的视图结构。其中layout和needslayout用来计算布局和是否需要重新布局。布局计算先递归子女节点的位置和大小,最后得出自己节点布局。当可视区域发生变化后,webKit都需要重新计算布局。
-
渲染方式
网页渲染的方式主要有三种:
软件渲染:CPU完成,处理2D方面的操作,适合简单网页
RenderObject图像的绘制又分为以下三阶段:
- 绘制层中背景和边框
- 绘制浮动内容
- 绘制前景,即内容部分等
硬件加速渲染:GPU完成,适合3D绘图,但消耗内存资源
混合渲染:既有CPU也有GPU
CPU绘制使用缓存机制可以减少重绘开销,每个renderLayer对象对应图像的一个图层,浏览器把所有图层合成图像就叫做合成化渲染。
三、JavaScript引擎
-
解释性语言
JavaScript属于解释性语言,它的语言特性让我们在编译时无法确定变量的类型(不能做偏移信息查找,偏移信息共享等编译优化),同时在运算时计算和决定类型会带来性能损失,这也是它比静态语言慢的原因。但现在JavaScript引擎做了很多优化,加入JIT等等,已经十分接近静态语言的性能了。
JavaScript与静态语言编译优化的区别:
-
JIT(just in time)
JavaScript为了提高运行速度加入了JIT技术,当解释器将源代码解释成内部表示时,JavaScript执行环境不仅解释这些内部表示而且将使用率较高的一些字节码转成本地代码(汇编)让CPU直接执行,而不是解释执行。这在Java虚拟机中也有应用。
-
JavaScript引擎
JavaScript引擎就是能够将JavaScript代码处理并执行的运行环境。
js引擎先将源代码转为抽象语法树,抽象语法树再转为中间表示(字节码表示),然后JIT转成本地代码运行。当然V8引擎可以直接从抽象语法树到本地代码,不经过中间表示,更加提高效率。
一个JavaScript引擎通常包括:
-
js引擎与渲染引擎
JavaScript引擎需要能够访问渲染引擎构建的DOM树,这往往通过桥接的接口。通过桥接接口这对性能来说是很大的损失,所以应避免JavaScript频繁地访问DOM。
-
V8引擎
V8是谷歌的一个开源项目,是高性能JavaScript引擎的实现。它支持window,linux,mac等操作系统,也支持X64,arm,IA32等硬件架构。其中node就是基于V8的引擎。在v8中,数据表示分为数据实际内容和数据的句柄两部分,内容是变长的,类型也不一样,而句柄定长,包含指向数据的指针。
JavaScript对象在V8的内部表示有三个成员:
隐藏类的例子:
-
内存管理
对于V8的内存划分,Zone类先申请一块内存,然后管理和分配一些小内存。小内存被分配后,不能被Zone回收,只能一次性回收分配的所有小内存。这有个缺陷,如果Zone分配了大量内存,但又不能够释放就会导致内存不足。
V8用堆来管理JavaScript数据,为了方便垃圾回收,v8堆主要分成三部分:
- 年轻代:主要为新对象分配内存空间
- 年老代:较少地做垃圾回收
- 大对象:需要较多内存的大对象
对于垃圾回收,主要采用标记清除法(标记清除也分多种,如下所示)。
-
V8为什么快?
针对上下文的快照(Snapshot)技术
快照技术即将内置对象和函数加载之后的内存保存并序列化,缩短启动时间。打开snapshot=on即可开启快照机制(但快照代码没法被优化编译器优化)。上下文(Contexts)则是JS应用程序的运行环境,避免应用程序的修改相互影响,例如一个页面js修改内置对象方法,不应该影响到另外页面。chrome浏览器每个process只有一个V8引擎实例,浏览器中的每个窗口、iframe都对应一个上下文。
Built-in代码
利用JS自表达内置对象、方法。
AST的内存管理
针对AST建立过程中多结点内存申请和一次性回收的特点,V8使用了内存段链表管理,并结合scopelock模式,实现少数申请(Segment,8KB~1MB)、多次分配AST结点、一次回收各个Segment的管理方式,既能避免内存碎片,又可以避免遍历AST结点逐个回收内存。
ComplieCache避免重复编译
对于一段JS代码,在开始进行词法分析前,会从编译缓存区CompilationCache查找该段代码是否已经被编译过,如果是,则直接取出编译过的机器代码,并返回,这样降低CPU的使用率。
属性快速访问
V8没有使用词典结构或红黑树实现的map来管理属性,而是在每个对象附加一个指针,指向隐藏类hidden class(如果第一次创建该类型对象,则新建hidden class);当对象每添加一个属性时,将新建一个class(记录了每个属性的位移/位置),而原来的class指向新class,即建立起一个hidden class的转换链表。
一次性编译生成机器语言
V8一次性把AST编译为机器语言,没有中间表示(通常先编译为字节码)。