Webkit、Chrome、Chromium的关系
-
苹果公司参与了由KDE开源社区发起的
网页渲染引擎KHTML
的开源项目开发 -
后来产生冲突,苹果宣布从KHTML的源代码树中复制代码出来,成立
封闭的项目WebKit
。 -
2005年,苹果决定将
WebKit项目开源
。 -
2008年,Google公司以苹果开源项目WebKit作为内核,创建了一个新的项目
Chromium
。 -
在Chromium项目的基础上,Google发布了自己的浏览器产品
Chrome
。Chromium本身就是一个浏览器,而不是Chrome浏览器的内核,Chrome浏览器一般选择Chromium的稳定版本作为它的基础
-
2013年4月,Google和苹果公司有了一些分歧,Google宣布了从WebKit复制出来并独立运作的
Blink
项目。
Webkit渲染引擎
图1-1 渲染引擎模块及其依赖的模块
-
HTML解释器:解释HTML文本的解释器,主要作用是将HTML文本解释成DOM(文档对象模型)树。
-
CSS解释器:级联样式表的解释器,它的作用是将样式表解释称CSSOM并为DOM中的各个元素对象计算出样式信息。
-
布局:在DOM创建之后,Webkit需要将其中的元素对象同样式信息结合起来,计算它们的大小位置等布局信息,形成一个能够表示这所有信息的内部表示模型。
-
绘图:使用图形库将布局计算后的各个网页的节点绘制成图像结果。
-
JavaScript引擎:JavaScript引擎能够解释JavaScript代码并通过DOM接口和CSSOM接口来修改网页内容和样式信息,从而改变渲染的结果。
async属性:脚本异步加载完成后立刻同步执行
defer属性:脚本异步加载后延后同步执行
WebKit的渲染过程
图1-2 渲染引擎的一般渲染过程及各阶段依赖的其他模块
渲染过程分成三个阶段
-
第一个阶段是从网页的URL到构建完DOM树
-
第二个阶段是从DOM树到构建完WebKit的绘图上下文
-
第三个阶段是从绘图上下文到生成最终的图像
阶段一:构建 DOM 树
图1-3 从网页URL到DOM树
具体的过程如下:
- 当用户输入网页URL的时候,WebKit调用其资源加载器加载该URL对应的网页。
- 加载器依赖网络模块建立连接,发送请求并接收答复。
- WebKit接收到各种网页或者资源的数据,其中某些资源可能是同步或异步获取的。
- 网页被交给HTML解释器转变成一系列的词语(Token)。
- 解释器根据词语构建节点(Node),形成DOM树。
- 如果节点是JavaScript代码的话,调用JavaScript引擎解释并执行。
- JavaScript代码可能会修改DOM树的结构。
阶段二:绘制上下文
图1-4 从CSS和DOM树到绘图上下文
具体的过程如下:
- CSS文件被CSS解释器解释成内部表示结构(CSSOM)。
- DOM树上附加解释后的样式信息,这就是RenderObject树。
- RenderObject节点在创建的同时,WebKit会根据网页的层次结构创建RenderLayer树,同时构建一个虚拟的绘图上下文。
上述图中的四个内部表示结构(DOM、CSSDOM、RenderObject、RenderLayer)一直存在,直到网页被销毁,因为它们对于网页的渲染起了非常大的作用。
阶段三:绘制最终图像
图1-5 从绘图上下文到最终的图像
具体的过程如下:
- 绘图上下文是一个与平台无关的抽象类,它将每个绘图操作桥接到不同的绘图具体实现类。
- 绘图实现类也可能有简单的实现,也可能有复杂的实现。在Chromium中,它的实现相当复杂,涉及到软件渲染和GPU加速机制,这在后面会涉及。
- 绘图实现类将2D图形库或者3D图形库绘制的结果保存下来,交给浏览器来同浏览器界面一起显示。
Chromium浏览器
主要是将页面资源转换成一个可视化、可听化的图像
Chromium浏览器的架构
Content模块:是渲染网页内容的模块,结合了渲染功能和沙箱模型、跨进程的GPU硬件加速机制等。
Content API:将下面的渲染机制、安全机制和插件机制等隐藏起来,提供一个接口层。
Chromium浏览器:具有浏览器完整的功能。
Content Shell:使用Content API来包装的一层简单的“壳”,是一个简单的“浏览器”,一般用来用来测试Content模块很多功能的正确性,例如渲染、硬件加速等。
Android WebView:利用Chromium的实现来替换原来Android系统默认的WebView。
Chromium的模型
Chromium的多进程模型
图3-5 Chromium的多进程模型
- Browser进程:浏览器的主进程,负责浏览器界面的显示、各个页面的管理,是所有其他类型进程的祖先,负责它们的创建和销毁等工作,它有且仅有一个。
- 网页的渲染进程:负责页面的渲染工作,Blink/WebKit的渲染工作主要在这个进程中完成,可能有多个。
- NPAPI插件进程:用来拓展浏览器功能,该进程是为NPAPI类型的插件而创建的。
- 插件:表示一些动态库根据定义的一些标准接口可以跟浏览器进行交互,NPAPI接口标准只是其中的一种。
- 当有多个网页需要使用同一种类型的插件的时候,例如很多网页需要使用Flash插件,Flash插件的进程会为每个使用者创建一个实例,所以插件进程是被共享的。
- GPU进程:最多只有一个,当且仅当GPU硬件加速打开的时候才会被创建,主要用于对3D图形加速调用的实现。
- Pepper插件进程:同NPAPI插件进程,不同的是为Pepper插件而创建的进程。
- 其他类型的进程:图中还有一些其他类型的进程没有描述出来,比如名为“Sandbox”的准备进程。
多进程的好处
- Browser进程和页面的渲染是分开的,保证了页面的渲染导致的崩溃不会导致浏览器主界面的崩溃。
- 每个网页是独立的进程,这保证了页面之间相互不影响。
- 插件进程也是独立的,插件本身的问题不会影响浏览器主界面和网页。
- 方便使用沙盒模型隔离插件等进程,提高浏览器稳定性
Chromium的多线程模型
网页的加载和渲染过程:
-
Browser进程收到用户的请求,首先由UI线程处理,而且将相应的任务转给IO线程,它随即将该任务传递给Renderer进程。
Browser进程除了 主线程、IO线程(进程间通信),中间还有很多其他的线程,用来处理视频、存储、网络、文件、音频、浏览历史等。
-
Renderer进程的IO线程经过简单解释后交给渲染线程。渲染线程接受请求,加载网页并渲染网页,这其中可能需要Browser进程获取资源和需要GPU进程来帮助渲染。最后Renderer进程将结果由IO线程传递给Browser进程。
Render进程除了 主线程、IO线程(进程间通信),中间还有很多其他的线程,比如GUI渲染线程、JavaScript引擎线程、事件触发线程、定时器线程、异步http请求线程等。
-
最后,Browser进程接收到结果并将结果绘制出来。
Chromium渲染过程
资源加载机制
DNS预取和TCP预连接
- **DNS预取:**当用户正在浏览当前网页的时候,Chromium提取网页中的超链接,将域名抽取出来,利用比较少的CPU和网络带宽来解析这些域名或IP地址
- TCP预连接: 当用户在地址栏中输入地址,如候选项同输入的地址很匹配,则在用户敲下回车键获取该网页之前,Chromium就已经开始尝试建立TCP连接了
资源缓存机制
构建DOM树
HTML网页和它的DOM树表示
在渲染引擎内部,有一个叫 HTML 解析器(HTMLParser) 的模块,它的职责就是负责将 HTML 字节流转换为 DOM 结构。所以这里我们需要先要搞清楚 HTML 解析器是怎么工作的。
从资源的字节流到DOM树
构建CSSOM树
样式的来源:
-
网页开发者编写的内部样式信息或者外部样式文
-
网页的读者设置的样式信息
比如缩放、开发者工具修改样式等
-
浏览器的内在默认样式
1. 解释过程
CSS解释器是指从CSS字符串经过CSS解释器处理后变成渲染引擎的内部规则表示的过程。当Webkit需要解释CSS内容的时候,调用CSSParser来负责,最后Webkit将创建好的结构设置到StyleSheetContents对象中。
2. 样式匹配
- StyleResolver类负责获取样式信息,并返回RenderStyle对象,RenderStyle对象包含了匹配完的结果样式信息
- StyleResolver类首先全网为每个元素匹配不同来源的规则,比如浏览器(用户代理)规则集合、用户规则集合和HTML网页中包含的自定义规则集合
- 对于自定义规则集合,它先查找ID规则,检查有无匹配的规则,之后依次检查类型规则、标签规则等。如果某个规则匹配上该元素,WebKit把这些规则保存到匹配结果,并进行排序。
- 对于元素需要的样式属性,WebKit选择从高优先级规则中选取,并将样式属性值返回。
元素匹配结果
3. JavaScript获取和设置样式
-
CSSOM(CSS对象模型)提供了接口让JavaScript获得和修改CSS代码设置的样式信息。在Webkit中,这需要JavaScript引擎和渲染引擎共同来完成。
-
CSSOM不会阻塞DOM的生成,但会阻塞JS的执行,JS又影响DOM的生成,从而影响页面渲染进度。
当在JS中访问了CSSDOM中某个元素的样式,需要等待这个样式被下载完成才能继续往下执行JS脚本。
大致的过程是,JavaScript引擎调用设置属性值的公共处理函数,然后该函数调用属性值解析函数。而后Webkit将解析后的信息设置到元素的style属性的样式webkitTransform中,然后设置标记表明该元素需要重新计算样式,并触发重新布局。最后就是Webkit的重新绘制。
4. CSSOM树生成过程
-
解析CSS文件(如果是link的)
-
将字节转换成字符。
-
确定tokens(标签)
-
将tokens转换成节点
-
最后根据节点构建CSSOM树。
源代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body {
font-size: 16px;
}
p {
font-weight: bold;
}
span {
color: red;
}
p span {
display: none;
}
img {
float: right;
}
</style>
</head>
<body>
<p>
<span>xx</span>
</p>
<span>xx</span>
<img src="" />
</body>
</html>
CSSOM
RenderObject树
WebKit会为可视节点建立相应的RenderObject对象,后面将它们的内容绘制到最终的网页结果中。
head、script、meta标签和display:none等非可视节点不会生成RenderObject对象
DOM树节点和RenderObject树的对应关系
- WebKit检查该DOM节点是否需要创建RenderObject对象。
- 如果需要,WebKit建立或者获取一个创建RenderObject对象的NodeRenderingContext对象,NodeRenderingContext对象会分析需要创建的RenderObject对象的父亲节点、兄弟节点等,设置这些信息后完成插入树的动作。
DOM、CSSOM、Render树的关系
RenderLayer树
RenderObject树和RenderLayer树的关系
- RenderLayer树是基于RenderObject树建立起来的一棵新树。
- RenderLayer节点和RenderObject节点是一对多的关系。
以下的RenderObject节点需要建立新的RenderLayer节点:
- DOM树的Document节点对应的RenderView节点。
- DOM树中的Document的子女节点,也就是HTML节点对应的RenderBlock节点。
- 显式的指定CSS位置的RenderObject节点。
- 有透明效果的RenderObject节点。
- 节点有溢出(Overflow)、alpha或者反射等效果的RenderObject节点。
- 使用Canvas 2D和3D(WebGL)技术的RenderObject节点。
- Video节点对应的RenderObject节点。
WebKit中的实际内部表示和布局信息
源码
WebKit中的实际内部表示和布局信息
绘图操作
WebKit的机制操作将内部模型转换成可视的结果分为两个阶段:1.每层的内容进行绘图工作 2. 之后将这些绘图的结果合成为一个图像。
流程
-
图层绘制:图层构建完成后会对每个图层进程绘制,把图层绘制拆分成很多的小的指令,按照指令顺序生成绘制列表,绘制列表是用来记录绘制顺序和绘制指令的
-
栅格化:绘制操作是由合成线程来完成的,当绘制列表完成主线程会把绘制列表提交到合成线程。
合成线程将图层分为图块(tile),合成线程会按照视口(viewport),也就是用户可以看到的区域优先来生成位图,所以**栅格化就是将图块生成位图,图块是栅格化的最小单位,**通常栅格化会使用GPU加速,并存储在GPU内存中
-
显示:图块都被光栅化了之后合成线程就会通知浏览器进程将内容绘制到屏幕上。
渲染方式:
软件渲染
:对于软件渲染机制,WebKit需要使用CPU来绘制每层的内容。会创建一个位图,每一层的内容都绘制上去,所以没有合成阶段。硬件加速渲染(GPU)
:对于硬件加速这种合成化渲染方式来说,会为每一层提供后端存储。- 混合模式
如果需要更新某个层的一个区域,因为软件渲染没有为每一层提供后端存储,因而它需要将和这个区域有重叠部分的所有层次的相关区域依次从后向前重新绘制一遍,硬件加速渲染只需要重新绘制更新发生的层次。
Webkit 软件渲染技术
使用场景
- 用来渲染基本的HTML元素
chromium渲染过程
- Browser进程发起的请求如窗口变化、路径改变等 或者页面自身的逻辑变更如果DOM操作等。
- Renderer进程的RenderWidget类接收到更新请求时,调用TransportDIB类来创建一个共享内存区域(以下称为共享内存位图)。
- Renderer进程遍历RenderObject树,使用Skia Canvas来绘制每个RenderObject节点及其子女,并把内容绘制并存储到共享内存位图。
- 首先Webkit计算重绘的区域是否和RenderLayer对象有重叠,如果有,Webkit要求绘制该层中的所有RenderObject对象。
- Renderer进程通知Browser进程绘制完成。
- Browser进程的RenderWidgetHost类把位图复制到BackingStore对象的相应区域中,释放共享内存位图
- Browser进程调用Paint函数来把结果绘制到窗口中。
- Browser进程发消息发给Renderer进程,表示已经使用完该共享内存,可以进行回收利用等操作。随后Renderer进程释放共享内存位图。
Webkit 硬件加速
使用场景
- 用来绘制含3D图形、HTML5新功能的元素并且性能特别好
流程
- WebKit决定将哪些RenderLayer对象组合在一起,形成一个有后端存储的新层,这一新层不久后会用于之后的合成(Compositing),这里称之为合成层(Compositing Layer)。
- 每个新层都有一个或者多个后端存储,这里的后端存储可能是GPU的内存。对于一个RenderLayer对象,如果它没有后端存储的新层,那么就使用它的父亲所使用的合成层。
- 将每个合成层包含的这些RenderLayer内容绘制在合成层的后端存储中
- 由合成器(Compositor)将多个合成层合成起来,形成网页的最终可视化结果
哪些RenderLayer对象可以是合成层呢?
-
RenderLayer具有CSS 3D属性或者CSS透视效果。
-
RenderLayer包含的RenderObject节点表示的是使用硬件加速的视频解码技术的HTML5“video”元素。
-
RenderLayer包含的RenderObject节点表示的是使用硬件加速的Canvas 2D元素或者WebGL技术。
-
RenderLayer使用了CSS透明效果的动画或者CSS变换的动画。
-
RenderLayer使用了硬件加速的CSS Filters技术。
-
RenderLayer使用了剪裁(Clip)或者反射(Reflection)属性,并且它的后代中包括一个合成层。
-
RenderLayer有一个Z坐标比自己小的兄弟节点,且该节点是一个合成层。
-
后端(存储RenderLayerBacking)对象
V8引擎
-
parse模块:将js源代码解析成AST抽象语法树
-
字节码:在虚拟机上执行的代码,而不是在最终的物理机器上执行的二进制代码。可以理解为机器码的抽象。
-
lgnition:将AST抽象语法树解释成字节码,并转为机器码执行,同时收集turbofan所需要的优化信息。
-
turtofan:可以将字节码编译成CPU直接运行的机器码,如果一个函数被多次调用,那么会标记成热点函数,由turbofan直接编译成优化后的机器码来提升性能
-
反优化
function add(a, b) { const params1 = a + 42; const params2 = b; return params1 + params2; } var result1 = add(1, 2); var result2 = add(3, 4); var result3 = add(5, 6); 该add函数会被标记为热点函数, turbofan 会对其编译优化,并且缓存起来,下次执行add函数时会直接执行优化后的机器码
该add函数会被标记为热点函数, turbofan 会对其编译优化,并且缓存起来,下次执行add函数时会直接执行优化后的机器码
function add(a, b) { const params1 = a + 42; const params2 = b; return params1 + params2; } var result1 = add('a', 'b');
执行到add(‘a’, ‘b’)时,会进行反优化,lgnition解释器重新生成字节码,解释执行。
在早期,V8 团队采取了非常激进的策略,直接将 JavaScript 代码编译成机器代码。但是二进制机器码占用空间大,导致性能不太好,从而转向占用空间较小的字节码。
V8垃圾回收机制
在V8中,将内存分为了新生代(年轻分代)和老生代(年老分代)。它们特点如下:
- 新生代:对象的存活时间较短。新生对象或只经过一次垃圾回收的对象。
- 老生代:对象存活时间较长。经历过一次或多次垃圾回收的对象。
新生代
新生代中的对象主要通过 Scavenge 算法进行垃圾回收。Scavenge 的具体实现,主要采用了Cheney算法。
- 垃圾回收过程
- 将堆内存一分为二,一个处于使用状态(from) 一个处于闲置状态(to)。
- 分配对象时,先是在From空间中进行分配,当开始进行垃圾回收时,会From空间中存活的对象复制到To空间中,而非存活对象将被释放。
- 完成复制后,From空间和To空间的角色发生对换。
- 当一个对象经过多次复制仍然存活时,这种生命周期较长的对象会被移动到老生代中,也称为晋升
- 对象晋升的条件
- 对象是否经过Scavenge回收。
- 当要从From空间复制一个对象到To空间时,将使用超过to空间25%的内存时,则这个对象直接晋升到老生代空间中。
老生代
-
垃圾回收过程
阶段一:标记清楚法
-
采用标记清楚法(Mark-Sweep),遍历堆中的所有对象,并标记活着的对象
-
在清理阶段,只清理没有被标记的对象,但会存在一些内存碎片。
阶段二:标记整理法
-
将活着的对象往一端移动,移动完成后,直接清理掉边界外的内存。