01 邂逅浏览器

3,044 阅读11分钟

1.认识浏览器

1.1 认识浏览器内核

常说的浏览器内核指的就是浏览器的排版引擎(layout engine),或者也称作浏览器引擎、页面渲染引擎(rendering engine)或者样板引擎。

排版引擎(比如Blink)和JavaScript引擎(比如V8)都是运行在浏览器渲染进程中的。

内核浏览器css前缀备注
TridentIE4-IE11-ms最新的Edge已转向Blink
Gecko火狐浏览器-moz
Webkitsafari、旧版谷歌-webkit
BlinkGoogle Chrome-webkit
Prestoopera-o现在的opera转向了Blink

1.2 浏览器常见进程的功能

  • 浏览器主进程:主要负责界面显示、用户交互、子进程管理(其他页面(进程)的创建与销毁)、Renderer进程中的内存中的Bitmap,绘制到用户界面,同时提供存储等功能。
  • GPU进程:初衷是为了实现3D CSS效果(transform、opacity、filter等),后来UI界面都选择采用GPU来进行绘制。
  • 插件进程:负责插件的运行,因插件容易崩溃,所以需要通过插件进程来隔离。
  • Renderer进程:页面渲染、脚本执行、事件处理。把HTML/CSS/JavaScript转换为用户可以与之交互的网页。默认情况下,Chrome会为每个Tab标签创建一个渲染进程,出于安全考虑,渲染进程都是运行在安全沙箱下(HTML/CSS/JS资源都是外来的,可能不安全)。
  • 网络进程:网络资源的管理,如下载

多进程架构的优势:

  1. 避免单页面崩溃
  2. 避免第三方插件崩溃
  3. 充分利用多核
  4. 方便使用安全沙箱隔离插件等进程

三个词概括:稳定性、流畅性、安全性

缺点:

  1. 更高的资源占用

    每个进程都会包含公共基础结构的副本(如 JavaScript 运行环境)

  2. 更复杂的体系架构

    浏览器各模块之间耦合性高、扩展性差,导致现在的架构已经很难适应新的需求了。

更多内容详见:浅谈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”子标签查看: 01-2.png 01-1.png

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 图层绘制

图层树构建完毕之后,渲染引擎就会对图层树中的每个图层进行绘制。它会把一个图层的绘制拆分为很多小的绘制指令,然后再按照顺序组合为一个待绘制列表。如下所示: 01-3.png

3.3.4 栅格化操作

绘制列表只是用来记录绘制顺序和绘制指令的列表,实际的绘制操作是由渲染引擎中的合成器线程来进行的。当图层的待绘制列表准备好之后,主线程会把绘制列表提交给合成线程。

因为有的图层很大,远超视口范围。所以合成线程会先把图层划分为图块

然后合成线程会根据视口附近的图块来优先绘制位图。实际生成位图的操作是由栅格化线程池来执行的。合成线程内部维护了一个栅格化的线程池,所有图块的栅格化都是在线程池中进行的。所谓的栅格化,就是把图块转换为位图的操作。

通常栅格化过程都会使用GPU加速,所以使用 GPU 生成位图的过程叫快速栅格化,或者GPU栅格化、光栅化等。最终生成的位图保存在GPU内存中。 01-4.png

3.3.5 合成和显示

一旦所有的图块都被光栅化,合成线程就会生成一个绘制图块的命令——"DrawQuard",然后把该命令提交给浏览器进程。

浏览器进程中有一个叫做Viz的组件,用于接收合成线程发送过来的DrawQuard命令。然后根据DrawQuard命令,将页面绘制到内存中,最后再显示到屏幕上。

关于从位图到屏幕的细节,可以关注如何生成一帧图像

最后用两张图总结下渲染流程: 01-6.png 01-5.png