浏览器的多进程架构
可以通过 Chrome 浏览器中的 选项->更多工具->任务管理器
,打开 Chrome 的任务管理器窗口,来查看 Chrome 打开一个页面,需要启动多少进程:
可以看到,在 Chrome (版本 101.0.4951.67(正式版本))中只打开了一个标签页,启动了 6 个进程。
进程和线程
在计算机中,并行处理就是同一时刻处理多个任务,如果使用单线程处理的任务采取多线程方式,通过使用并行处理能大大提升性能。
多线程可以处理并行任务,但是线程不能单独存在,线程是由进程来启动和管理的,进程是由系统来启动和管理的。
一个进程就是一个程序的运行实例。在启动程序时,操作系统会为程序创建一块内存,用于存放代码、运行过程中产生的数据和一个执行任务的主线程,把这样的一个运行环境称为进程。
可以看出,线程是依附于进程的,通过在进程中使用多线程并行处理能提升处理效率。
进程和线程之间的关系有以下特点:
- 进程中的任一线程执行出错,会导致这个进程的崩溃
- 线程之间共享进程的内存,所以线程之间的数据共享
当一个进程关闭之后,操作系统会回收进程占用的内存
- 当一个进程退出时,操作系统会回收该进程所申请的所有资源
- 即使由于其中线程操作不当,导致内存泄漏,当进程退出时,这些内存也会被正确回收
进程之间的内容相互隔离
- 进程隔离是为保护操作系统中的数据安全,防止数据被其他进程获取
- 如果进程之间需要进行数据通信,需要使用进程间通信(IPC)的机制
单进程浏览器时代
单进程浏览器是指浏览器的所有功能模块都是运行在同一个进程里,这些模块包含网络、插件、JavaScript 运行环境、渲染引擎和页面等。
将很多功能模块都运行在一个进程中,导致了单进程浏览器不稳定、不流畅和不安全:
不稳定
- 早期浏览器要借助插件实现 Web 视频等功能,但是插件式极其容易出问题的模块,因为其运行在浏览器进程中,所以一个插件的意外崩溃会导致整个浏览器进程崩溃
- 渲染引擎模块也是不稳定的,一些复杂的 JavaScript 代码就有可能引起渲染引擎模块的崩溃,同样,也会导致整个浏览器进程崩溃
不流畅
- 所有页面的渲染模块、JavaScript 执行环境和插件都是运行在同一个线程中,这意味着同一时刻只能有一个模块可以执行,当某一模块独占线程后,其他页面就没有机会执行,会导致整个浏览器失去响应,变卡顿
- 页面的内存泄露也会导致单进程浏览器变卡顿,通常运行完复杂页面,页面关闭后可能有内存不能完全回收的情况,这会导致使用时间越长,内存占用越高,浏览器越慢
不安全
- 插件可以使用 C/C++ 等代码编写,通过插件可以获取操作系统的任意资源,当页面运行一个插件时插件完全可以操作系统,引发安全性问题
- 页面脚本也可以通过浏览器的漏洞获取系统权限
多进程浏览器
早期多进程架构
进程隔离解决不稳定问题
- 由于进程相互隔离,当一个页面或者插件崩溃时,影响的只是当前的页面进程或者插件进程,不会影响浏览器和其他页面
按页面划分进程,解决不流畅和内存泄露问题
- JavaScript 运行在渲染进程中,即使 JavaScript 阻塞了渲染进程,影响的也只是当前的渲染页面,不会影响浏览器和其他页面
- 当关闭一个页面时,对应的整个渲染进程也会被关闭,所占资源会被回收,解决了浏览器页面的内存泄露问题
使用安全沙箱解决安全问题
- 安全沙箱是一种提供给 Web 浏览器的安全机制,它可以防止恶意代码被运行
当前的多进程架构
浏览器进程
- 界面显示
- 用户交互
- 子进程管理
- 存储等
渲染进程
- 将 HTML、CSS、JavaScript 转换为用户可以与之交互的网页
- 排版引擎 Blink 和 JavaScript 引擎 V8 都运行在渲染进程中
- 默认情况下 Chrome 会为每个 Tab 页创建一个渲染进程
- 处于安全考虑,渲染进程都是运行在沙箱模式下
GPU 进程
- GPU 使用的初衷是为了实现 3D CSS 的效果,不过后来网页、Chrome 的 UI 界面都选择采用 GPU 绘制,这使得 GPU 成了浏览器普遍的需求
网络进程
- 负责页面的网络资源加载
插件进程
- 负责插件运行,因为插件容易崩溃,所以需要通过插件进程来隔离,保证插件崩溃不会影响浏览器和其他页面
虽然多进程模型提升了浏览器的稳定性、流畅性和安全性,但是它也带来了一些问题:
更高的资源占用
- 每个进程都会包含公共基础结构的副本(如 JavaScript 运行环境),会消耗更多的内存资源
更复杂的体系架构
- 各模块之间耦合性高,扩展性差等
对于上面这两个问题,Chrome 团队一直在寻求一种弹性方案,既可以解决资源占用高的问题,也可以解决复杂的体系架构的问题。
未来面向服务的架构
在 2016 年,Chrome 官方团队使用“ 面向服务的架构”(Services Oriented Architecture,简称SOA)的思想设计了新的 Chrome 架构。即 Chrome 整体架构会朝向现代操作系统所采用的“面向服务的架构” 方向发展,原来的各种模块会被重构成独立的服务(Service),每个服务(Service)都可以在独立的进程中运行,访问服务(Service)必须使用定义好的接口,通过 IPC 来通信,从而 构建一个更内聚、松耦合、易于维护和扩展的系统,更好实现 Chrome 简单、稳定、高速、安全的目标。
同时 Chrome 还提供灵活的弹性架构,在强大性能设备上会以多进程的方式运行基础服务,但是如果在资源受限的设备上,Chrome 会将很多服务整合到一个进程中,从而节省内存占用。
目前 Chrome 正处在老的架构向服务化架构过渡阶段,这将是一个漫长的迭代过程。
Web 中的 TCP/IP
互联网是一套理念和协议组成的体系架构。其中,协议是一套众所周知的规则和标准,如果各方都同意使用,那么它们之间的通信将变得毫无障碍。
互联网中的数据是通过数据包来传输的,如果发送的数据很大,那么该数据就会被拆分为很多小数据包来传输。
IP:把数据包送达目的主机
数据包要在互联网上进行传输,就要符合网际协议(Internet Protocol,简称IP)标准。
计算机的地址就称为 IP 地址,访问任何网站实际上只是你的计算机向另外一台计算机请求信息。
可以将网络简单分为三层结构:
数据包从 主机 A 到 主机 B 的传输过程如下:
- 业务层 将含有 “Cellinlab” 的数据包交给 网络层
网络层 再将 IP 头附加到数据包上,组成新的 IP 数据包,并交给 物理层
- IP 头是 IP 数据包开头的信息,包含 IP 版本、源 IP 地址、目标 IP 地址、生存时间等信息
- 物理层 通过物理网络将数据包传送给 主机 B
- 数据包被传输到 主机 B 的 网络层,主机 B 拆开数据包的 IP 头信息,并将拆开的数据部分交给 业务层
- 最终,含有 “Cellinlab” 的数据包到达 主机 B 的 业务层
UDP:把数据包送达应用程序
IP 是非常底层的协议,只负责把数据包传送到对方电脑,但是对方电脑并不知道把数据包交给哪个程序?因此,需要基于 IP 之上开发能和应用打交道的协议,最常见的是“用户数据包协议(User Datagram Protocol)”,简称 UDP。
将前面的三层结构进行拆分,在业务层和网络层之间加上传输层:
重新梳理下 数据包从 主机 A 到 主机 B 的传输过程:
- 业务层 将含有 “Cellinlab” 的数据包交给 传输层
传输层 在数据包前面加上 UDP 头,组成新的 UDP 数据包,再将新的 UDP 数据包交给 网络层
- UDP 头中除了目的端口,还有源端口号等信息
- 网络层 再将 IP 头附加到数据包上,组成新的 IP 数据包,并交给 物理层
- 数据包被传输到 主机 B 的网络层,在这里,主机 B 拆开 IP 头信息,并将拆开来的数据部分交给 传输层
- 传输层 将数据中的 UDP 头拆开,根据 UDP 头中的目的端口号,找到对应的程序,并将数据包交给对应的程序
- 最终,含有 “Cellinlab” 的数据包到达 主机 B 的 业务层
在使用 UDP 发送数据时,有各种因素会导致数据包出错,虽然 UDP 可以校验数据是否正确,但是对于错误的数据包,UDP 并不提供重发机制,只是丢弃当前的包,而且 UDP 在发送之后也无法知道是否能达到目的地。
虽说 UDP 不能保证数据可靠性,但是传输速度却非常快,所以 UDP 会应用在一些关注速度、但不那么严格要求数据完整性的领域,如在线视频、互动游戏等。
TCP:把数据包完整地送达应用程序
在要求数据传输可靠性的应用中,UDP 会存在一些问题:
- 数据包传输过程中易丢失
- 大文件会被拆包传输,小包不会同时到达,UDP 不知道组合组包还原
为了解决上面的问题,引入了 TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。
TCP 相对 UDP 有了以下特点:
- 对于数据包丢失的情况,TCP 提供重传机制
- TCP 引入了数据包排序机制,用来保证把乱序的数据包组合成一个完成的文件
在 TCP 下的数据包传输流程:
TCP 是如何保证重传机制和数据包的排序的:
一个完整的 TCP 连接的生命周期包括了“建立连接”“传输数据”和“断开连接”三个阶段:
建立连接阶段
- 通过“三次握手”,发送端和接收端建立了连接
- TCP 提供面向连接的通信传输,数据通信开始之前先做好两端之间的准备工作
传输数据阶段
- 接收端需要对每个数据包进行确认操作,收到数据包之后需要发送确认数据包给发送端
- 在发送端发送了一个数据包之后,在协定的时间内没有收到接收方的确认消息,会判断为数据包丢失,并触发发送端重发机制
- 大文件会被拆分为小数据包,每个小数据包到达后,接收端会按照 TCP 头中的序号排序,然后拼接成完整的数据
断开连接阶段
- 通过“四次挥手”,来保证双方都能断开连接
TCP 为了保证数据传输的可靠性,牺牲了数据包的传输速度,因为“三次握手”和“数据包校验机制”等把传输过程中的数据包的数量提高了一倍。
HTTP 请求流程
HTTP 协议,正是建立在 TCP 连接基础之上的。HTTP 是一种允许浏览器向服务器获取资源的协议,是 Web 的基础,通常由浏览器发起请求,用来获取不同类型的文件,例如 HTML 文件、CSS 文件、JavaScript 文件、图片、视频等。此外,HTTP 也是浏览器使用最广的协议。
浏览器端发起 HTTP 请求
在浏览器地址栏输入 https://cellinlab.xyz/index.html
之后,浏览器会完成下面的操作:
构建请求
- 浏览器构建请求信息,准备发起网络请求
GET /index.html HTTP/1.1
查找缓存
在真正发起网络请求之前,浏览器会在浏览器缓存中查询是否有要请求的文件
- 浏览器缓存是一种在本地保存资源副本,以供下次请求时直接使用的技术
当浏览器发现有缓存副本时,会拦截请求,并返回该资源的缓存副本,直接结束请求,不会再去服务器重新下载
- 浏览器缓存副本,可以缓解服务器端压力,提升性能
- 对于网站来说,缓存是实现快速资源加载的重要组件部分
- 如果缓存没有命中,就会进入网络请求过程
准备 IP 地址和端口
- 浏览器使用 HTTP 协议作为应用层协议,用来封装请求的文本信息
- 使用 TCP/IP 作为传输层协议,将封装的请求文本发送到网络
- 在 HTTP 工作开始之前,浏览器需要通过 TCP 与服务器建立连接
- 浏览器会请求 DNS 返回域名对应的 IP 地址,浏览器也提供 DNS 数据缓存
等待 TCP 队列
- Chrome 有个机制,同一个域名同时最多只能建立 6 个 TCP 连接,如果一次发 10 个请求到同一域名,其中 4 个会进入排队状态
- 如果当前请求数量小于 6,会直接进入下一步
- 建立 TCP 连接
发送 HTTP 请求
- 一旦建立了 TCP 连接,浏览器就可以和服务器通信了
服务器端处理 HTTP 请求
返回请求
- 一旦服务器处理结束,就能返回数据给浏览器
- 服务器会通过请求行的状态码来告诉浏览器它的处理结果
断开连接
- 一般情况,服务器向客户端返回了请求数据,就要关闭 TCP 连接
- 也可以通过在头信息中标记,来保持 TCP 连接
Connection: keep-alive
- 保持 TCP 连接可以节省下次请求时建立连接的时间,提升资源加载速度
重定向
- 如果服务器返回了重定向,浏览器会自动重新发起请求
从输入 URL 到 页面展示
用户输入地址
在地址栏输入后,判断是搜索内容还是请求的 URL
- 如果是搜索内容,使用浏览器默认搜索引擎拼接出新的带关键词的 URL
- 如果是 URL,工具规则给 URL 加上协议,拼接出完整的 URL
- 回车后,进入加载状态
URL 请求
- 浏览器进程通过进程间通信(IPC),把 URL 请求发送至网络进程,网络进程收到 URL 请求后,发起真正的 URL 请求流程
具体请求流程
网络进程查找本地缓存是否缓存了该资源,命中缓存就直接返回资源,没有命中就进入网络请求流程
- 请求前会进行 DNS 解析,获取 IP 地址
- 如果请求协议是 HTTPS 还需要建立 TLS 连接
利用 IP 地址和服务器建立 TCP 连接
- 连接建立后,浏览器端会构建请求行、请求头等信息,并把和该域名相关的 Cookie 等附加到请求头,发送给服务器
服务器收到请求后,进行相应的处理,生成响应数据,并发给网络进程
- 重定向,如果服务器返回了重定向,浏览器会自动重新发起请求
- 响应数据类型处理,会根据
Content-Type
来判断如何显示响应体的内容
准备渲染流程
- 默认情况下,Chrome 会为每个页面分配一个渲染进程
在某些情况下,浏览器会让多个页面直接运行在同一个渲染进程中
- Chrome 的默认策略是,每个标签对应一个渲染进程
- 如果从一个页面打开了另一个新页面,而新页面和当前页面属于同一站点,那么新页面会复用父页面的渲染进程,这个策略叫
process-per-site-instance
- 渲染进程准备好之后,还不能立即进入文档解析状态,因为此时的文档数据还在网络进程中,还没有提交给渲染进程
提交文档
- “提交文档”的消息是由浏览器进程发出的,渲染进程接收到“提交文档”的消息后,会和网络进程建立传输数据的“管道”
- 等文档数据传输完成后,渲染进程会返回“确认提交”的消息给浏览器进程
- 浏览器进程在收到“确认提交”的消息后,会更新浏览器界面状态,包括安全状态、地址栏 URL、前进后退的历史状态,并更新 Web 页面
渲染页面
- 一旦文档被提交,渲染进程就开始页面解析和子资源加载
- 页面一旦生成完成,渲染进程就会发送消息给浏览器进程,浏览器进程收到后停止标签图标加载动画
渲染流程
按照渲染的时间顺序,渲染流水线可分为如下几个子阶段:构建 DOM 树、样式计算、布局阶段、分层、绘制、分块、光栅化和合成。
构建 DOM 树
因为浏览器无法直接理解和使用 HTML,所以需要将 HTML 转换为浏览器能够理解的结构——DOM 树。
构建 DOM 树的输入内容是一个非常简单的 HTML 文件,然后经由 HTML 解析器解析,最终输出树状结构的 DOM。
DOM 和 HTML 内容几乎是一样的,但是和 HTML 不同的是,DOM 是保存在内存中树状结构,可以通过 JavaScript 来查询或修改其内容。
样式计算
样式计算的目的是为了计算出 DOM 节点中每个元素的具体样式,大致可以分三个步骤:
把 CSS 转换为浏览器能够理解的结构
CSS 主要有三个来源:
- 通过
<link>
标签引用的外部样式 - 通过
<style>
标签指定的内联样式 - 元素的
style
属性中指定的样式
- 通过
浏览器也是无法直接理解这些纯文本的 CSS 样式,所以当渲染引擎接收到 CSS 文本时,会执行一个转换操作,将 CSS 文本转换为浏览器可以理解的结构 —— styleSheets
- styleSheets 具备查询和修改功能,为后面的样式操作提供基础
document.styleSheets; // 查看 styleSheets
转换样式表中的属性值,使其标准化
- CSS 文本中有很多属性值,如
2em
、blue
、blod
等,这些类型不容易被渲染引擎理解,需要将所有值转换为渲染引擎容易理解的、标准化的计算值
- CSS 文本中有很多属性值,如
计算出 DOM 树中每个节点的具体样式
CSS 继承是指每个 DOM 节点都包含父节点的样式
body { font-size: 20px; } p { color: blue; } span { display: none; } div { font-weight: bold; color: red; } div p { color: green; }
- 可以在 Chrome "开发者工具",选择 "Elements",查看 DOM 树,再选择 "Styles",查看样式表
- User Agent 样式,是浏览器提供的一组默认样式
- 样式层叠,定义如何合并来自多个源的属性值的算法
- 最终计算出的每个 DOM 节点的样式,会被保存在 ComputedStyle 的结构内,可以在 “开发者工具” 中 “Computed” 子标签中查看
布局阶段
布局是计算 DOM 树中可见元素的几何位置,主要有两个任务:
创建布局树
- DOM 树中还含有很多不可见的元素,比如
<script>
标签,还有使用了display: none
的元素,这些元素不会被渲染,在显示之前,需要额外构建一棵只包含可见元素布局树
构建布局树大致流程:
- 遍历 DOM 树中的所有可见节点,并把这些节点加到布局中
- 不可见元素会被布局树忽略掉
- DOM 树中还含有很多不可见的元素,比如
布局计算
- 计算布局树节点的坐标位置
分层
页面中有很多复杂的效果,如一些复杂的 3D 变换、页面滚动或者 z-index
做 z 轴排序等。为了更方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(Layer Tree)。
可以通过 “开发者工具” 中 “Layers” 子标签直观查看页面分层情况。
可以看出,渲染引擎给页面分了很多图层,这些图层按照一定顺序叠加在一起,就形成了最终的页面。
通常情况下,并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。
渲染引擎为特定的节点创建新的层,一般需要满足以下任意条件:
拥有层叠上下文属性的元素会被提升为单独的一层
- 页面是个二维平面,但是层叠上下文能够让 HTML 元素具有三维概念,HTML 元素按照自身属性的优先级分布在垂直于二维平面的 z 轴上
- 明确定位属性的元素、定义透明属性的元素、使用 CSS 滤镜的元素等,都会拥有层叠上下文
需要裁剪(clip)的地方会被创建为图层
- 出现裁剪情况的时候,渲染引擎会为文字部分单独创建一个层,如果出现滚动条,滚动条也会被提升为单独的层
图层绘制
在完成图层树的构建之后,渲染引擎会对图层树中的每个图层进行绘制。渲染引擎在实习图层绘制时,会把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表。
可以在 “开发者工具-Layers” 中选择 document,观察绘制列表。
栅格化
绘制列表只是用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。
当图层的绘制列表准备好之后,主线程会把该绘制列表提交(commit)给合成线程。
通常一个页面可能很大,但是用户只能看到其中的一部分,把用户可以看到的这个部分叫做视口(viewport)。
在有些情况下,有的图层可以很大,比如有的页面使用滚动条要滚动好久才能滚动到底部,但是通过视口,用户只能看到页面的很小一部分,所以在这种情况下,要绘制出所有图层内容的话,就会产生太大的开销,而且也没有必要。
为了优化性能,合成线程会将图层划分为图块(tile),这些图块的大小通常是 256x256 或者 512x512。
然后合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。而图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的,运行方式如下:
通常,栅格化过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫快速栅格化,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中。
合成和显示
一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。
浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。
整个渲染流程
整个渲染流程,从 HTML 到 DOM、样式计算、布局、图层、绘制、光栅化、合成和显示。
大致可总结为如下:
- 渲染进程将 HTML 内容转换为能够读懂的 DOM 树结构。
- 渲染引擎将 CSS 样式表转化为浏览器可以理解的 styleSheets,计算出 DOM 节点的样式。
- 创建布局树,并计算元素的布局信息。
- 对布局树进行分层,并生成分层树。
- 为每个图层生成绘制列表,并将其提交到合成线程。
- 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。
- 合成线程发送绘制图块命令 DrawQuad 给浏览器进程。
- 浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上。