1.认识浏览器
1.1 认识浏览器内核
常说的浏览器内核指的就是浏览器的排版引擎(layout engine),或者也称作浏览器引擎、页面渲染引擎(rendering engine)或者样板引擎。
排版引擎(比如Blink)和JavaScript引擎(比如V8)都是运行在浏览器渲染进程中的。
| 内核 | 浏览器 | css前缀 | 备注 |
|---|---|---|---|
| Trident | IE4-IE11 | -ms | 最新的Edge已转向Blink |
| Gecko | 火狐浏览器 | -moz | |
| Webkit | safari、旧版谷歌 | -webkit | |
| Blink | Google Chrome | -webkit | |
| Presto | opera | -o | 现在的opera转向了Blink |
1.2 浏览器常见进程的功能
- 浏览器主进程:主要负责界面显示、用户交互、子进程管理(其他页面(进程)的创建与销毁)、Renderer进程中的内存中的Bitmap,绘制到用户界面,同时提供存储等功能。
- GPU进程:初衷是为了实现3D CSS效果(transform、opacity、filter等),后来UI界面都选择采用GPU来进行绘制。
- 插件进程:负责插件的运行,因插件容易崩溃,所以需要通过插件进程来隔离。
- Renderer进程:页面渲染、脚本执行、事件处理。把HTML/CSS/JavaScript转换为用户可以与之交互的网页。默认情况下,Chrome会为每个Tab标签创建一个渲染进程,出于安全考虑,渲染进程都是运行在安全沙箱下(HTML/CSS/JS资源都是外来的,可能不安全)。
- 网络进程:网络资源的管理,如下载
多进程架构的优势:
- 避免单页面崩溃
- 避免第三方插件崩溃
- 充分利用多核
- 方便使用安全沙箱隔离插件等进程
三个词概括:稳定性、流畅性、安全性
缺点:
-
更高的资源占用
每个进程都会包含公共基础结构的副本(如 JavaScript 运行环境)
-
更复杂的体系架构
浏览器各模块之间耦合性高、扩展性差,导致现在的架构已经很难适应新的需求了。
更多内容详见:浅谈Chrome架构发展史
1.3 浏览器的进程模式
之前说到,Renderer Process的作用是负责一个Tab内的显示相关的工作,这就意味着,一个Tab,就会有一个Renderer Process,这些进程之间的内存无法共享,却常常包含相同的副本。为了节约内存,Chrome提供了四种进程模式,每种进程模式会对Tab做不同的处理:
Process-per-site-instance(default):同一个site-instance使用一个进程
Process-per-site:同一个site使用一个进程
Process-per-tab:每个tab使用一个进程
Single Process: 所有tab使用一个进程序
process-per-site: 相同的registered domain name(根域名)+scheme(协议)
process-per-site-instance: 打开的新页面和旧页面属于相同的site(同一站点) 且满足下列条件之一:
- 用户通过鼠标左键点击
<a target="_bland"></a>方式打开新页面 - 通过JS代码(如window.open)打开新页面
因为浏览器默认是site-instance,所以通过JS或者是a标签进行跳转打开的页面一旦崩溃,我们原来的页面也会崩溃,因为使用的是同一个渲染进程。
2.浏览器的导航流程
该问题又有一个别名:从输入URL到页面展示发生了什么?属于经典的面试题了。
上述进程的浏览器主进程(Brower Process)中,又有很多负责不同工作的线程:
- UI线程(UI Thread):界面线程,能够响应操作系统的特定消息,包括界面消息、鼠标键盘消息、自定义消息等,是在普通的worker线程基础上加上消息循环来实现的。
- 网络线程(NetWork Thread):处理网络请求,从上获取数据
- 存储线程(Storage Thread):控制文件等的访问
2.1 合成完整的URL
用户在浏览器地址栏输入地址回车后,UI Thread会判断输入的内容是关键字还是URL,如果是关键字,根据默认的搜索引擎拼接成搜索关键字的URL,如果是URL地址,就给它加上协议(HTTP/HTTPS)和端口(80默认),得到合法的URL。
2.2 URL请求流程
浏览器主进程通过进程间通信(IPC)把URL请求发送到网络进程。网络进程首先检查本地缓存,如果缓存命中,则直接返回,导航结束。如果没有命中缓存,就会发起网络请求了。此时UI Thread会把Tab前的图标展示为加载中状态。
请求前的第一步是要进行请求DNS解析,以获得域名对应的IP地址。更多有关DNS解析的信息,请看DNS解析流程。
如果请求的协议是HTTPS,还需要建立TLS连接。更多内容请关注浏览器中的HTTP。
接下来就是根据IP地址和服务器建立TCP连接。详见浅谈传输层协议。
连接建立好之后,浏览器端会构建请求行、请求头等信息,并把和域名相关的Cookie等数据附加到请求头中,然后把构建好的HTTP请求报文发送给服务器。
服务器端收到请求信息后,会根据请求信息生成相应数据(响应行、响应头和响应体等信息),再发送给浏览器端。
浏览器端的网络进程接受到相应数据后,会去解析HTTP报文。这个过程是同步的。
2.3 处理响应数据
网络进程在解析响应数据的过程中,如果遇到了重定向信息(读取请求行的时候状态码为301或302)。就会从Location字段中读取新的URL地址重新发起请求,步骤同上。
如果响应的状态码为2xx,则浏览器继续处理响应数据。
网络进程在解析响应头的时候,会根据Content-type字段判断响应体的类型。如果值为application/octet-stream就表明数据是字节流类型。浏览器就会按照下载类型来处理这个数据。导航到此结束。
如果为text/html,浏览器就会把它当做HTML文件来解析,这时候浏览器就会去准备渲染进程了。
与此同时,浏览器还会进行安全检查,如果域名或者请求内容匹配到已知的恶意网站,NetWork Thread会展示一个警告页。除此之外,Network Thread还会做 CORB(Cross Origin Read Blocking)检查来确定那些敏感的跨站数据不会被发送至Renderer Process
2.4 提交文档
渲染进程准备好了之后,浏览器主进程会通过进程间通信(IPC)发送"提交文档"的消息给渲染进程,渲染进程收到后会和网络进程建立起数据通信的管道。这个过程也是同步的,网络进程一边接收数据,一边发送给渲染进程。
等文档数据全部传输完成之后,渲染进程会发送“确认提交”的消息给浏览器进程。
浏览器收到这个消息之后,UI Thread回去更新浏览器界面状态,包括了安全状态、地址栏的URL、前进后退的历史状态,然后更新页面(白屏)。
到这里,一个完整的"导航"流程算是结束了。
3. 浏览器的渲染流水线
渲染进程包含:
- 主线程(V8和Blink引擎以及GC都是运行在主线程上)
- 多个工作线程
- 一个合成器线程
- 多个光栅化线程
当渲染进程接收到文档后,就开始DOM解析等一系列渲染流水线工作了。这个过程也是同步的。
3.1 构建DOM树
浏览器无法直接识别和使用HTML,所以需要把HTML转换为浏览器能够理解的结构——DOM树。
DOM树---> HTMLParser ---> Document,DOM树的生成细节,不是本文的重点,感兴趣的同学可以关注后续的文章。
有了DOM树之后,可以根据DOM树提供的接口使用JavaScript对DOM节点进行修改。
3.2 样式计算
样式计算的目的是为了计算出DOM树中每个节点的具体样式。大致可以分为三步:
-
把CSS转换为浏览器认识的结构 CSSOM(可通过document.stylesheets访问)
-
标准化样式表中的属性值(比如em,blue,bold)
这些数值不容易被渲染引擎理解,需要转换为标准化的属性值:px、rgb、font-weight:700;
-
计算出每个节点具体的样式
根据CSS的继承和层叠规则(一般浏览器带有默认熟悉UserAgent),计算出每个节点的具体样式。最终输出的每个节点的样式保存在ComputedStyle中,可以打开 Chrome 的“开发者工具”,“element”标签,然择“Computed”子标签查看:
3.3 布局阶段
3.3.1 合成布局树
首先会根据DOM树和CSSOM把节点加入布局树中。但是一些特殊的节点不会被加入布局树中。比如设置了display:none;的节点或者head标签下的所有内容等。更多详见:如何生成一帧图像
现在我们有了一棵布局树。接下来就要计算布局树节点的位置坐标了。
执行布局计算操作的时候,会把布局运算的结果重新写回布局树中,所以布局树既是输入内容也是输出内容,这是布局阶段一个不合理的地方,因为在布局阶段并没有清晰地将输入内容和输出内容区分开来。针对这个问题,Chrome 团队正在重构布局代码,下一代布局系统叫 LayoutNG,试图更清晰地分离输入和输出,从而让新设计的布局算法更加简单。
3.3.2 分层
页面中的一些复杂效果通常要进行分层操作。比如一些复杂的3D变换、页面滚动或者使用z-index做z轴排序等。为了更方便地实现这些效果。渲染引擎需要为特定的节点生成专用的图层,并生成一棵图层树。并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。最终每一个节点都会直接或者间接地从属于一个层。
满足下列条件之一,渲染引擎就会为特定的节点创建新的层:
-
拥有层叠上下文属性的元素会被单独提升为一层(定位、透明、CSS滤镜等)
更多层叠上下文的知识,请参考层叠上下文
-
需要剪裁(clip)的地方
比如div文字超出了盒子大小,这时候就发生了剪裁,文字内容便会被单独提升为一层。
3.3.3 图层绘制
图层树构建完毕之后,渲染引擎就会对图层树中的每个图层进行绘制。它会把一个图层的绘制拆分为很多小的绘制指令,然后再按照顺序组合为一个待绘制列表。如下所示:
3.3.4 栅格化操作
绘制列表只是用来记录绘制顺序和绘制指令的列表,实际的绘制操作是由渲染引擎中的合成器线程来进行的。当图层的待绘制列表准备好之后,主线程会把绘制列表提交给合成线程。
因为有的图层很大,远超视口范围。所以合成线程会先把图层划分为图块。
然后合成线程会根据视口附近的图块来优先绘制位图。实际生成位图的操作是由栅格化线程池来执行的。合成线程内部维护了一个栅格化的线程池,所有图块的栅格化都是在线程池中进行的。所谓的栅格化,就是把图块转换为位图的操作。
通常栅格化过程都会使用GPU加速,所以使用 GPU 生成位图的过程叫快速栅格化,或者GPU栅格化、光栅化等。最终生成的位图保存在GPU内存中。
3.3.5 合成和显示
一旦所有的图块都被光栅化,合成线程就会生成一个绘制图块的命令——"DrawQuard",然后把该命令提交给浏览器进程。
浏览器进程中有一个叫做Viz的组件,用于接收合成线程发送过来的DrawQuard命令。然后根据DrawQuard命令,将页面绘制到内存中,最后再显示到屏幕上。
关于从位图到屏幕的细节,可以关注如何生成一帧图像
最后用两张图总结下渲染流程: