启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行的主线程,我们把这样的一个运行环境叫进程。
线程是依附于进程的,而进程中使用多线程并行处理能提升运算效率。进程和线程之间的关系如下:
进程中的任意一线程执行出错,都会导致整个进程的崩溃
线程之间共享进程中的数据
当一个进程关闭之后,操作系统会回收进程所占用的内存
即使其中任意线程因为操作不当导致内存泄漏,进程退出时,这些内存也会被争取回收。比如之前的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个。
多进程架构浏览器解决了单进程浏览器存在的三大问题:
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>
方式打开新页面window.open
) 打开新页面浏览器是 Process-per-site-instance
作为默认进程模式的,这也就是为什么我们通过 JS 或者 a 标签进行跳转打开的页面崩溃的时候,我们原来的 Tab 页面也会崩溃的原因,因为他们使用的是同一个进程。
在网络中,一个大文件通常会被拆分为很多谁包来进行传输,而数据包在传输过程中又有很大概率丢失或者出错,那么如何保证页面文件能被完整的送达浏览器呢?
数据包在互联网上传输,就要符合网际协议(Internet Protocol
,简称 IP
)标准。计算机的地址就称为IP地址,访问任何网站实际上只是你的计算机向另外一台计算机请求信息。
数据包从主机A发送到主机B的路线如下:
最终,“内容”数据包从主机A到达了主机B的上层。
IP是非常底层的协议,只负责把数据包传达到对方电脑,但是对方电脑并不知道数据包交给哪个程序。
因为数据包最终是要送达上层应用程序,所以需要基于IP上开发能和应用打交道的协议,最常见的就是UDP协议,UDP中最重要的信息就是端口号,每个访问网络的程序都需要绑定一个端口号,通过端口号UDP把指定的数据包发送给指定程序。
和IP头一样,端口号会被装进UDP头里面,UPD头再和原始数据包合并组成新的UDP数据包,为了支持UDP协议,在网络层和上层之间增加了传输层。
增加了UDP协议后数据包从主机A发送到主机B的路线就变成:
最终,“内容”数据就到达了主机B的上层应用程序这里。
在使用UDP发送数据时,有各种因素会导致数据包出错,虽然UDP可以校验数据是否正确,但是对于错误的数据包它只是丢弃并不能重发,而且发送之后也无法知道是否能达到目的地,它的优势就是速度快,所以一般会应用在关注速度但不那么严格要求数据完整性的领域(比如在线视频、互动游戏等)。
对于浏览器请求,或者邮件这类要求数据传输可靠性的应用,使用UDP传输存在两个问题:
基于这两个问题,引入了TCP协议(面向连接的、可靠的、基于字节流的传输层通信协议)。它的特点刚好弥补了UDP的缺点:
一个完整的TCP连接生命周期包含“建立连接”,“传输数据”,“断开连接”。
建立连接:通过“三次握手”客户端和服务端总共发送三个数据包以确认连接的建立
传输数据:接收端在接收到数据包之后,需要发送确认数据包给发送端,如果在规定时间内发送端没有收到这个确认反馈信息,则判断为数据包丢失,并触发发送端的重发机制。一个大的文件在传输过程中拆分成很多小的数据包,这些数据包到达接收端后,接收端会按照TCP头中的序号为其排序来组成完整数据
断开连接:数据传输完毕后终止连接,“四次握手”来保证连接断开
HTTP是建立在TCP连接基础之上,一种允许浏览器向服务器获取资源的协议,是Web的基础。
浏览器使用HTTP协议作为应用层协议,用来封装请求的文本信息,并使用TCP/IP作为传输层协议将它发送到网络上,所以在HTTP工作开始之前,浏览器需要通过TCP与服务器建立连接,也就是说HTTP的内容是通过TCP的传输数据阶段来实现的。
构建网络请求
查找缓存
当浏览器发现请求的资源已经在浏览器缓存,它就会拦截请求返回该资源的副本并结束请求。如果缓存查找失败就进入网络请求过程
浏览器请求DNS返回域名对应的IP(浏览器还提供了DNS数据缓存服务),拿到IP就需要获取端口号(未特意指明端口号的默认80端口)
准备好IP和端口号,三次握手后,和服务器建立了连接
一旦建立了TCP连接,浏览器就可以和服务器进行通信,而HTTP中的数据正是在这个通信过程中传输的
首先浏览器会向服务器发送请求行,然后还要以请求头形式发送其他一些基本信息(比如浏览器基本信息等)
服务器处理结束准备返回数据给浏览器,首先会返回响应行,随后向浏览器发送响应头(比如服务器基本信息,返回数据类型,服务器要在客户端保存的Cookie等),发送完响应头后服务器就可以继续发送响应体数据
通常情况下,一旦服务器向客户端返回了请求数据,它就要关闭TCP连接,不过如果浏览器或者服务器在其头信息中加入了
Connection: Keep-Alive
那么TCP连接在发送后将仍保持打开状态,这样浏览器就可以继续通过同一个TCP连接发送请求,省去了下次请求时建立连接的时间
如果第二次页面打开很快,主要原因是第一次加载页面过程中,缓存了一些耗时的数据。DNS缓存和页面资源缓存,重点看下浏览器资源缓存流程:
Cache-Control
字段来设置是否缓存该资源Etag
,第二次发送同一请求时,客户端会同时发送一个 If-None-Match
,其值就是 Etag
的值,服务端会比对这个客户端发送过来的 Etag
是否与服务器的相同来判断是否有更新,如果相同客户端继续使用本地缓存返回 304
状态码,不相同就返回最新资源Set-Cookie
字段中Set-Cookie
会把这个字段信息保存到本地首先,用户在地址栏输入内容,浏览器会判断是搜索关键字还是请求URL
按下回车后,意味着页面即将被替换成新的页面(不过,在这之前,浏览器给当前页面一次执行 beforeunload事件的机会,允许当前页面退出之前执行操作,或取消导航事件不执行后续工作)。
浏览器标签图标显示加载动画状态,表示进入了页面资源请求过程,此时,浏览器进程会通过进程间通信(IPC)将URL请求发送至网络进程,网络进程发起真正的URL请求流程。
Chrome的默认策略(process-per-site-instance)是,每个标签对应一个渲染进程,但如果从一个页面打开了另一个页面,而新页面和当前页面属于同一站点(相同协议+相同跟域名)的话,那么新页面会复用父页面的渲染进程。
渲染进程准备好之后,还不能立即进入文档解析状态,因为此时的文档数据还在网络进程中,并没有提交给渲染进程,所以下一步就进入了提交文档阶段。
所谓提交文档,就是指浏览器进程将网络进程接收到的HTML数据提交给渲染进程
到这里,一个完整的导航流程走完了,它涵盖了从用户发送请求到提交文档给渲染进程的中间所有阶段,它是网络加载流程和渲染流程之间的一座桥梁,之后就是进入渲染阶段
一旦文档被提交,渲染进程便开始页面解析和子资源加载,一旦页面生成完成,渲染进程会发送一个消息给浏览器进程,浏览器接收到消息后会停止标签图标上的加载动画。
由于渲染机制过于复杂,所有渲染模块在执行过程中会被划分为很多子阶段,输入的HTML经过这些子阶段最后输出像素,我们将这样的一个处理流程叫做渲染流水线
按照渲染的时间顺序,流水线可分为:构建DOM树、样式计算、布局阶段、分层、绘制、分块、光栅化和合成,每个子阶段都有其输入内容、处理过程和输出内容
因为浏览器无法直接理解和使用HTML,所以需要将HTML转换为浏览器能够理解的结构——DOM树
CSS样式的来源有三种
浏览器也是无法直接理解纯文本的CSS样式,所以当渲染引擎会将CSS文本转换为浏览器可以理解的结构——styleSheets。它包含了以上三种来源的样式,且具备查询和修改功能,为后面的样式操作提供基础
转换为 styleSheets之后,接下来就要对其属性值进行标准化操作
计算出DOM树中每个节点的具体样式,这就涉及到CSS的继承规则和层叠规则
最终输出的内容是每个DOM节点的样式,并被保存在 ComputedStyle 结构中
现在,有了DOM树和DOM树中元素的样式,接下来需要计算DOM树中可见元素的几何位置,这个计算过程叫布局,它包含了两个任务:
display:none
属性的元素等因为页面中有很多复杂的效果,如3D变换、页面滚动、使用z-index做z轴排序等,渲染引擎还需要为特定的节点生成专用的图层,并生成一颗对应的图层树——LayoutTree
并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层。最终每一个节点都会直接或间接的从属于一个层。
完成图层树的构建后,渲染引擎会对图层中每个图层进行绘制,它会将一个图层的绘制拆分成很多小的绘制指令,然后将这些绘制指令顺序组成一个待绘制列表,图层绘制阶段输出的内容就是这些待绘制列表
实际的绘制操作是由渲染引擎中的合成进程来完成的,绘制列表准备好之后,主线程会把该绘制列表提交给合成进程。
通常一个页面可能很大,用户只能看到视口部分,所以要绘制出所有图层内容的话就会产生太大的开销,基于这个原因,合成进程会将图层划分为图块(tile)通常图块大小为256256或512512,然后合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化就是指将图块转换为位图,图块是栅格化执行的最小单位
栅格化过程都会使用GPU来加速生成,渲染进程把生成图块的指令发送给GPU,生成的位图被保存在GPU内存中,这就涉及到了渲染进程和GPU进程的跨进程操作。
一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。
浏览器进程里面有一个叫viz的组件,用来接收合成线程发过来的DrawQuad命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。
到这里,经过这一系列的阶段,编写好的HTML、CSS、JavaScript等文件,经过浏览器就会显示出漂亮的页面了
重排需要更新完整的渲染流水线(从构建DOM树开始),开销是最大的,哪些操作会引起重排呢
如果只是修改了某个元素的背景色,那么布局阶段将不会被重新执行,直接进入绘制以及之后一系列子阶段
修改一个既不要布局也不要绘制的属性例如修改opacity透明度,transform变换,渲染引擎将跳过布局和绘制,只执行后续的合成操作