Chrome浏览器进程工作原理和机制
- Chrome架构:一个页面四个进程
- 进程和线程
- 单进程浏览器
- 多进程浏览器
- 多进程浏览器解决的问题
- Chrome的进程模式
- TCP协议:如何保证页面文件被完整送达浏览器
- IP:把数据包送达目的主机
- UDP:把数据包送达应用程序
- TCP:把数据完整地送达应用程序
- HTTP请求流程
- 浏览器发起HTTP请求流程
- 服务器处理HTTP请求流程
- 数据缓存
- 保持登录状态
- 从输入URL到页面展示
- 用户输入
- URL请求过程
- 准备渲染进程
- 提交文档
- 渲染阶段
- 构建DOM树
- 样式计算
- 布局阶段
- 分层
- 图层绘制
- 栅格化(raster)操作
- 合成和显示
- 重排(Reflow),重绘(Repaint),合成(Composite)
- 重排
- 重绘
- 合成
Chrome架构:一个页面四个进程
进程和线程
启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行的主线程,我们把这样的一个运行环境叫进程。
线程是依附于进程的,而进程中使用多线程并行处理能提升运算效率。进程和线程之间的关系如下:
-
进程中的任意一线程执行出错,都会导致整个进程的崩溃
-
线程之间共享进程中的数据
-
当一个进程关闭之后,操作系统会回收进程所占用的内存
即使其中任意线程因为操作不当导致内存泄漏,进程退出时,这些内存也会被争取回收。比如之前的IE浏览器,支持很多插件,插件很容易导致内存泄漏,这就意味着只要浏览器开着,内存占用就有可能会越来越多,但是当关闭浏览器进程时,这些内存就会被系统回收掉
-
进程之间内容相互隔离
每一个进程只能访问自己占有的数据,如果进程之间需要进行数据通信,就需要使用进程间通信机制(IPC)
单进程浏览器
单进程浏览器时代所有功能模块都运行在一个进程中(包括网络、插件、JavaScript运行环境、渲染引擎和页面等)。它的缺点主要暴露为
-
不稳定
这些早期单进程浏览器要实现 Web 视频、Web游戏等强大功能时需要安装对应的插件,而因为这些都是运行在一个进程中,所以只要其中一个插件意外崩溃了整个浏览器就崩溃了。同理,当一些复杂 JavaScript 代码引起渲染引擎模块崩溃了也会导致整个浏览器崩溃。
-
不流畅
当执行一个无限循环的JavaScript脚本时,它会独占整个线程,这样导致其他运行在该线程中的模块没有机会被执行,导致整个浏览器失去响应变卡顿。
-
不安全
插件可以获取到操作系统的任意资源,如果是恶意插件,就可能会释放病毒、窃取账号密码,引发安全性问题。
多进程浏览器
目前多进程浏览器包含
-
一个浏览器主进程(
Browser Process
)负责浏览器 Tab 页的前进、后退、地址栏、书签栏等工作,以及界面显示、用户交互、子进程管理,同时提供存储等功能。
-
多个渲染进程(
Renderer Process
)负责一个 Tab 页内的显示相关的工作,将
html
、css
和JavaScript
转换为用户可以交互的网页,排版引擎blink和JavaScript引擎v8都运行在该进程中。默认情况下,Chrome会为每个tab标签创建一个渲染进程,因为渲染进程所有的内容都是通过网络获取的,会存在一些恶意代码利用浏览器漏洞对系统进行攻击,所以为安全考虑,渲染进程都是运行在沙箱模式下 -
多个插件进程(
Plugin Process
)负责插件的运行,因为插件易崩溃,通过进程来隔离,保证插件进程崩溃不会对浏览器和页面造成影响。如
flash
插件等 -
一个网络进程(
Network Process
)负责网络资源加载
-
一个GPU进程(
GPU Process
)初衷是为了实现 css3 的3D效果,随后成为了浏览器普遍需求,UI界面都选择采用 GPU来绘制
打开一个页面至少需要一个网络进程,一个浏览器进程,一个GPU进程以及一个渲染进程,共4个。
多进程浏览器解决的问题
多进程架构浏览器解决了单进程浏览器存在的三大问题:
- 由于进程是相互隔离的,当一个页面或者插件崩溃,影响到的仅仅是当前页面进程或插件进程,不会影响到浏览器和其他页面
- JavaScript 运行在渲染进程,JavaScript 代码阻塞影响到的也知识点当前的渲染页面,不会影响到浏览器和其他页面。对于内存泄漏,当关闭一个网页时整个渲染进程也会被关闭,该进程所占用的内存都会被系统回收,轻松解决浏览器页面内存泄漏问题。
- 多进程架构可以使用安全沙箱,沙箱里面的程序可以运行但是不能在硬盘上写入任何数据,也不能在敏感位置读取任何数据。Chrome把插件进程和渲染进程锁在沙箱里,这样即使执行了恶意程序,程序也不能突破沙箱获取系统权限。
Chrome的进程模式
Chrome 提供了四种进程模式,不同的进程模式会对 Tab 页进程做不同的处理
Process-per-site-instance
(也是默认模式):同一个site-instance
使用一个进程Process-per-site
:同一个site
使用一个进程Process-per-tab
:每个 Tab 使用一个进程Single Process
:所有 Tab 页共用一个进程
site
就是相同的 registered domain name (根域名,比如 google.com
,baidu.com
) 和 scheme (协议,比如 http://
,https://
)
site-instance
指的是打开的新页面和旧页面属于相同的 site
且满足下面的条件之一
- 用户通过点击
<a target="_bland"></a>
方式打开新页面 - 通过 JS 代码 (如,
window.open
) 打开新页面
浏览器是 Process-per-site-instance
作为默认进程模式的,这也就是为什么我们通过 JS 或者 a 标签进行跳转打开的页面崩溃的时候,我们原来的 Tab 页面也会崩溃的原因,因为他们使用的是同一个进程。
TCP协议:如何保证页面文件被完整送达浏览器
在网络中,一个大文件通常会被拆分为很多谁包来进行传输,而数据包在传输过程中又有很大概率丢失或者出错,那么如何保证页面文件能被完整的送达浏览器呢?
IP:把数据包送达目的主机
数据包在互联网上传输,就要符合网际协议(Internet Protocol
,简称 IP
)标准。计算机的地址就称为IP地址,访问任何网站实际上只是你的计算机向另外一台计算机请求信息。
数据包从主机A发送到主机B的路线如下:
- 主机A上层将含有“内容”的数据包交给网络层
- 网络层再将IP头附加到数据包上,组成新的 IP数据包,并交给底层
- 底层通过物理网络将数据包传输给主机B的网络层
- 在这里主机B拆开数据包的IP头信息,并将拆开的“内容”数据部分交给上层
最终,“内容”数据包从主机A到达了主机B的上层。
IP是非常底层的协议,只负责把数据包传达到对方电脑,但是对方电脑并不知道数据包交给哪个程序。
UDP:把数据包送达应用程序
因为数据包最终是要送达上层应用程序,所以需要基于IP上开发能和应用打交道的协议,最常见的就是UDP协议,UDP中最重要的信息就是端口号,每个访问网络的程序都需要绑定一个端口号,通过端口号UDP把指定的数据包发送给指定程序。
和IP头一样,端口号会被装进UDP头里面,UPD头再和原始数据包合并组成新的UDP数据包,为了支持UDP协议,在网络层和上层之间增加了传输层。
增加了UDP协议后数据包从主机A发送到主机B的路线就变成:
- 主机A将含有“内容”的数据包交给传输层
- 传输层会在数据包前加UDP头组成UDP数据包,并将其交给网络层
- 网络层再将IP头附加到数据包中,组成IP数据包,并交给底层
- 底层通过物理网络将数据包传输给主机B的网络层
- 在这里主机B拆开IP头信息,并将拆开的UDP数据包交给传输层
- 传输层拆开UDP头信息,根据UDP所提供的端口号,将“内容”数据部分交给上层应用程序
最终,“内容”数据就到达了主机B的上层应用程序这里。
在使用UDP发送数据时,有各种因素会导致数据包出错,虽然UDP可以校验数据是否正确,但是对于错误的数据包它只是丢弃并不能重发,而且发送之后也无法知道是否能达到目的地,它的优势就是速度快,所以一般会应用在关注速度但不那么严格要求数据完整性的领域(比如在线视频、互动游戏等)。
TCP:把数据完整地送达应用程序
对于浏览器请求,或者邮件这类要求数据传输可靠性的应用,使用UDP传输存在两个问题:
- 数据包在传输过程中容易丢失
- 大文件会被拆分成很多小的数据包传输,UDP协议并不知道如何组装这些小的数据包从而将其还原
基于这两个问题,引入了TCP协议(面向连接的、可靠的、基于字节流的传输层通信协议)。它的特点刚好弥补了UDP的缺点:
- 对于数据包丢失,提供重传机制
- TCP引入了数据包排序机制,用来还原文件
一个完整的TCP连接生命周期包含“建立连接”,“传输数据”,“断开连接”。
建立连接:通过“三次握手”客户端和服务端总共发送三个数据包以确认连接的建立
传输数据:接收端在接收到数据包之后,需要发送确认数据包给发送端,如果在规定时间内发送端没有收到这个确认反馈信息,则判断为数据包丢失,并触发发送端的重发机制。一个大的文件在传输过程中拆分成很多小的数据包,这些数据包到达接收端后,接收端会按照TCP头中的序号为其排序来组成完整数据
断开连接:数据传输完毕后终止连接,“四次握手”来保证连接断开
HTTP请求流程
HTTP是建立在TCP连接基础之上,一种允许浏览器向服务器获取资源的协议,是Web的基础。
浏览器使用HTTP协议作为应用层协议,用来封装请求的文本信息,并使用TCP/IP作为传输层协议将它发送到网络上,所以在HTTP工作开始之前,浏览器需要通过TCP与服务器建立连接,也就是说HTTP的内容是通过TCP的传输数据阶段来实现的。
浏览器发起HTTP请求流程
-
构建网络请求
-
查找缓存
当浏览器发现请求的资源已经在浏览器缓存,它就会拦截请求返回该资源的副本并结束请求。如果缓存查找失败就进入网络请求过程
-
浏览器请求DNS返回域名对应的IP(浏览器还提供了DNS数据缓存服务),拿到IP就需要获取端口号(未特意指明端口号的默认80端口)
-
准备好IP和端口号,三次握手后,和服务器建立了连接
-
一旦建立了TCP连接,浏览器就可以和服务器进行通信,而HTTP中的数据正是在这个通信过程中传输的
首先浏览器会向服务器发送请求行,然后还要以请求头形式发送其他一些基本信息(比如浏览器基本信息等)
服务器处理HTTP请求流程
服务器处理结束准备返回数据给浏览器,首先会返回响应行,随后向浏览器发送响应头(比如服务器基本信息,返回数据类型,服务器要在客户端保存的Cookie等),发送完响应头后服务器就可以继续发送响应体数据
通常情况下,一旦服务器向客户端返回了请求数据,它就要关闭TCP连接,不过如果浏览器或者服务器在其头信息中加入了
Connection: Keep-Alive
那么TCP连接在发送后将仍保持打开状态,这样浏览器就可以继续通过同一个TCP连接发送请求,省去了下次请求时建立连接的时间
数据缓存
如果第二次页面打开很快,主要原因是第一次加载页面过程中,缓存了一些耗时的数据。DNS缓存和页面资源缓存,重点看下浏览器资源缓存流程:
- 服务器返回HTTP响应头给浏览器,浏览器是通过响应头中的
Cache-Control
字段来设置是否缓存该资源 - 第一次请求时,服务器会返回一个
Etag
,第二次发送同一请求时,客户端会同时发送一个If-None-Match
,其值就是Etag
的值,服务端会比对这个客户端发送过来的Etag
是否与服务器的相同来判断是否有更新,如果相同客户端继续使用本地缓存返回304
状态码,不相同就返回最新资源
保持登录状态
- 服务器接收到浏览器提交的登录信息,会生成一段表示用户身份的字符串,并将该字符串写到响应头的
Set-Cookie
字段中 - 浏览器接收到服务器响应头后,解析响应头,遇到
Set-Cookie
会把这个字段信息保存到本地 - 用户再次访问时,浏览器发起HTTP请求,请求前浏览器会读取之前保存的
Cookie
数据,并将其写到请求头的Cookie
字段中 - 服务器接收到请求头数据后查找里面的
Cookie
字段信息,然后去服务器查询后台,判断该用户是已登录状态,返回用户页面数据
从输入URL到页面展示
用户输入
首先,用户在地址栏输入内容,浏览器会判断是搜索关键字还是请求URL
- 如果是搜索关键字就会使用浏览器默认搜索引擎合成带有搜索关键字的URL
- 如果是请求URL浏览器会根据规则将其合成一个完整的URL
按下回车后,意味着页面即将被替换成新的页面(不过,在这之前,浏览器给当前页面一次执行 beforeunload
事件的机会,允许当前页面退出之前执行操作,或取消导航事件不执行后续工作)。
URL请求过程
浏览器标签图标显示加载动画状态,表示进入了页面资源请求过程,此时,浏览器进程会通过进程间通信(IPC
)将URL请求发送至网络进程,网络进程发起真正的URL请求流程。
- 网络进程查找本地缓存,有缓存取缓存,没缓存进入网络请求
- 请求前进行DNS解析,获取服务器IP地址(如果协议是https还需要建立TLS连接)
- 利用IP地址建立TCP连接
- 连接成功后,浏览器构建请求行、请求头,并将该域名相关的
Cookie
数据附加到请求头中,向服务器发送构建的请求信息 - 服务器接收到请求信息,生成响应数据,并发给网络进程
- 网络进程接收到响应信息,对其解析
- 解析相应头发现状态码是301或302,读取响应头中
Location
字段发起新的重定向请求,一切从头开始,如果是200,浏览器继续处理该请求 - 浏览器根据响应头中
Content-Type
类型来决定如何显示响应体的内容。如果是下载类型该请求会被提交给浏览器的下载管理器,同时该URL请求的导航流程就此结束。如果是HTML
类型,那么浏览器则会继续进行导航流程,准备渲染进程
准备渲染进程
Chrome的默认策略(process-per-site-instance
)是,每个标签对应一个渲染进程,但如果从一个页面打开了另一个页面,而新页面和当前页面属于同一站点(相同协议+相同跟域名)的话,那么新页面会复用父页面的渲染进程(参考上面的 浏览器进程模式
)。
渲染进程准备好之后,还不能立即进入文档解析状态,因为此时的文档数据还在网络进程中,并没有提交给渲染进程,所以下一步就进入了提交文档阶段。
提交文档
所谓提交文档,就是指浏览器进程将网络进程接收到的HTML数据提交给渲染进程
- 当浏览器进程接收到网络进程的响应头数据之后,便向渲染进程发起“提交文档”的消息
- 渲染进程接收到“提交文档”消息后,会和网络进程建立传输数据的“管道”
- 等文档数据传输完成后,渲染进程会返回“确认提交”消息给浏览器进程
- 浏览器进程接收到“确认提交”消息后,会更新浏览器界面状态(安全状态、地址栏URL、前进后退历史状态等)并更新Web页面
到这里,一个完整的导航流程走完了,它涵盖了从用户发送请求到提交文档给渲染进程的中间所有阶段,它是网络加载流程和渲染流程之间的一座桥梁,之后就是进入渲染阶段
渲染阶段
一旦文档被提交,渲染进程便开始页面解析和子资源加载,一旦页面生成完成,渲染进程会发送一个消息给浏览器进程,浏览器接收到消息后会停止标签图标上的加载动画。
由于渲染机制过于复杂,所有渲染模块在执行过程中会被划分为很多子阶段,输入的HTML经过这些子阶段最后输出像素,我们将这样的一个处理流程叫做渲染流水线
按照渲染的时间顺序,流水线可分为:构建DOM树、样式计算、布局阶段、分层、绘制、分块、光栅化和合成,每个子阶段都有其输入内容、处理过程和输出内容
构建DOM树
因为浏览器无法直接理解和使用HTML,所以需要将HTML转换为浏览器能够理解的结构——DOM树
样式计算
CSS样式的来源有三种
浏览器也是无法直接理解纯文本的CSS样式,所以当渲染引擎会将CSS文本转换为浏览器可以理解的结构——styleSheets。它包含了以上三种来源的样式,且具备查询和修改功能,为后面的样式操作提供基础
接下来就要对其属性值进行标准化操作
计算出DOM树中每个节点的具体样式,这就涉及到CSS的继承规则和层叠规则
- CSS继承就是每个DOM节点都包含有父节点的样式
- 层叠是CSS的一个基本特征,定义如何合并来自多个源的属性值的算法
最终输出的内容是每个DOM节点的样式,并被保存在 ComputedStyle
结构中
布局阶段
现在,有了DOM树和DOM树中元素的样式,接下来需要计算DOM树中可见元素的几何位置,这个计算过程叫布局,它包含了两个任务:
- 创建布局树:一颗只包含可见元素的布局树,不包含head标签还有使用了
display:none
属性的元素等 - 计算布局树节点的坐标位置
分层
因为页面中有很多复杂的效果,如3D变换、页面滚动、使用 z-index
做z轴排序等,渲染引擎还需要为特定的节点生成专用的图层,并生成一颗对应的图层树——LayoutTree
并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。最终每一个节点都会直接或间接的从属于一个层。
图层绘制
完成图层树的构建后,渲染引擎会对图层中每个图层进行绘制,它会将一个图层的绘制拆分成很多小的绘制指令,然后将这些绘制指令顺序组成一个待绘制列表,图层绘制阶段输出的内容就是这些待绘制列表
栅格化(raster)操作
实际的绘制操作是由渲染引擎中的合成进程来完成的,绘制列表准备好之后,主线程会把该绘制列表提交给合成进程。
通常一个页面可能很大,用户只能看到视口部分,所以要绘制出所有图层内容的话就会产生太大的开销,基于这个原因,合成进程会将图层划分为图块(tile
)通常图块大小为256*256
或512*512
,然后合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化就是指将图块转换为位图,将以上这些信息转化为显示器中的像素,图块是栅格化执行的最小单位
栅格化过程都会使用GPU来加速生成,渲染进程把生成图块的指令发送给GPU,生成的位图被保存在GPU内存中,这就涉及到了渲染进程和GPU进程的跨进程操作。
合成和显示
一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——DrawQuad
,然后将该命令提交给浏览器进程。
浏览器进程里面有一个叫viz
的组件,用来接收合成线程发过来的DrawQuad
命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。
到这里,经过这一系列的阶段,编写好的HTML
、CSS
、JavaScript
等文件,经过浏览器就会显示出漂亮的页面了
重排(Reflow),重绘(Repaint),合成(Composite)
重排
重排需要更新完整的渲染流水线(从构建DOM树开始),开销是最大的,哪些操作会引起重排呢
- 添加、删除某个节点
- 元素的几何属性的变化,包括
margin
、padding
、height
、width
、border
等 - 浏览器窗口发生变化
resize
- 获取某些属性:虽然浏览器引擎对重排做了优化,比如它会等到有足够数量的变化发生,或者等到一定的时间,或者等一个线程结束再一起处理,只发生一次重排。但当获取一些属性时,浏览器为取得正确的值也会触发重排,这样就使得浏览器的优化失效了,这些属性包括
offsetTop
、offsetWidth
、scrollLeft
、scrollTop
、clientTop
等,所以多次使用这些值时应进行缓存
重绘
如果只是修改了某个元素的背景色,那么布局阶段将不会被重新执行,直接进入绘制以及之后一系列子阶段
合成
修改一个既不要布局也不要绘制的属性例如修改opacity
透明度,transform
变换,渲染引擎将跳过布局和绘制,只执行后续的合成操作