浏览器背后的故事(未完待续)

346 阅读18分钟

浏览器的进程和线程

什么是进程和线程

进程:进程是拥有资源和独立运行的最小单位,也是程序执行的最小单位。

  • 啥意思呢这是?举个例子,爸爸打算今天给你蒸猪肉大葱馅儿的包子,爸爸准备了面,肉,葱,调料,爸爸还在网上搜索了一篇如何包包子的食谱。
  • 食谱就是程序。
  • 爸爸就是处理器
  • 面粉,肉,大葱等等原料就是输入数据
  • 进程就是爸爸看食谱,准备原料以及包好包子上锅蒸等所有动作的总和。

线程:线程(thread)是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。

  • 啥意思呢这是?我们再举个例子,有一串作业需要写。
  • 如果你一个人按顺序写,这就是单线程。
  • 你找了好几个人同时帮你写,给大家布置任务,这叫创建线程。
  • 你负责调度他们,你叫主线程,给你写作业的这些人,是子线程,你们合起来叫多线程
  • 这些人写的时候,每个人用的纸和笔都是共享的,这叫多线程资源共享。
  • 这些人在都需要橡皮,可是橡皮只有一个,这叫冲突。
  • 解决冲突的办法有很多,比如排队等候、等上一个人用完后喊你,这叫线程同步。
  • 你跟大家说可以开始写了,不然他们就等着啥也不干,这叫启动线程。
  • 如果小王写的作业非常重要,你可能会在旁边看着他写一会儿,这叫线程参与。

进程和线程的关系

进程中,任何一个线程的的出错都会导致进程的崩溃 线程之间共享进程中的数据 当一个进程关闭之后,操作系统会回收进程所占用的内存 进程之间的内容相互隔离

单进程浏览器

单进程浏览器是指浏览器的所有功能运行在同一个进程里。很容易理解,任何一个线程出错,都会导致整个浏览器的崩溃。一个页面的执行时间长,可能会导致所有页面都点不了。内存无法全部回收也会导致内存泄漏。并且很容易被脚本攻击。

多进程浏览器

最新的 Chrome浏览器包括:1个浏览器主进程、1个GPU进程、1个网络进程、多个渲染进程和多个插件进程。其实就是当你启动浏览器的时候,此时至少有1 个浏览器主进程、1个GPU进程、1个网络进程,1个渲染进程的运行,当你打开两个tab页的时候,就是1个浏览器主进程、1个GPU进程、1个网络进程,2个渲染进程。如果打开的页面有运行插件的话,还需要再加上1个插件进程。

  • 浏览器进程。主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。

  • 渲染进程。核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。

  • GPU 进程。其实,Chrome 刚开始发布的时候是没有 GPU 进程的。而 GPU 的使用初衷是为了实现 3D CSS 的效果,只是随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制,这使得 GPU 成为浏览器普遍的需求。最后,Chrome 在其多进程架构上也引入了 GPU 进程。

  • 网络进程。主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。

  • 插件进程。主要是负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。

解析网址会发生什么

解析url

输入URL后,浏览器会解析出协议、主机、端口、路径等信息,并构造一个HTTP请求。

浏览器缓存

浏览器发送请求前,根据请求头的expires和cache-control判断是否命中(包括是否过期)强缓存策略,如果命中,直接从缓存获取资源,并不会发送请求。如果没有命中,则进入下一步。 没有命中强缓存规则,浏览器会发送请求,根据请求头的last-modified和etag判断是否命中协商缓存,如果命中,直接从缓存获取资源。如果没有命中,则进入下一步。 如果前两步都没有命中,则直接从服务端获取资源。 我们经常在浏览器控制台能看到Service Worker,Memory Cache,Disk Cache,Push Cache这几种,其实他们是将缓存存起来的不同方式,对应不同位置。

  • Service Worker Service Worker是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用Service Worker的话,传输协议必须为HTTPS。因为Service Worker中涉及到请求拦截,所以必须使用HTTPS协议来保障安全。Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。

Service Worker 实现缓存功能一般分为三个步骤:首先需要先注册Service Worker,然后监听到install事件以后就可以缓存需要的文件,那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。

当Service Worker没有命中缓存的时候,我们需要去调用fetch函数获取数据。也就是说,如果我们没有在Service Worker命中缓存的话,会根据缓存查找优先级去查找数据。但是不管我们是从Memory Cache中还是从网络请求中获取的数据,浏览器都会显示我们是从Service Worker中获取的内容。

  • Memory Cache Memory Cache也就是内存中的缓存,主要包含的是当前中页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。 一旦我们关闭Tab页面,内存中的缓存也就被释放了。那么既然内存缓存这么高效,我们是不是能让数据都存放在内存中呢?这是不可能的。计算机中的内存一定比硬盘容量小得多,操作系统需要精打细算内存的使用,所以能让我们使用的内存必然不多。

需要注意的事情是,内存缓存在缓存资源时并不关心返回资源的HTTP缓存头Cache-Control是什么值,同时资源的匹配也并非仅仅是对URL做匹配,还可能会对Content-Type,CORS等其他特征做校验。

  • Disk Cache Disk Cache也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之Memory Cache胜在容量和存储时效性上。

  • Push Cache Push Cache(推送缓存)是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂,在Chrome浏览器中只有5分钟左右,同时它也并非严格执行HTTP头中的缓存指令。

所有的资源都能被推送,并且能够被缓存,但是Edge和Safari浏览器支持相对比较差;可以推送no-cache和no-store的资源;一旦连接被关闭,Push Cache 就被释放;多个页面可以使用同一个HTTP/2的连接,也就可以使用同一个Push Cache。这主要还是依赖浏览器的实现而定,出于对性能的考虑,有的浏览器会对相同域名但不同的tab标签使用同一个HTTP连接;Push Cache 中的缓存只能被使用一次;浏览器可以拒绝接受已经存在的资源推送;你可以给其他域名推送资源。

DNS解析

DNS管理主机名和IP地址间的对应关系。

域名解析的过程实际是将域名还原为IP地址的过程。首先浏览器先检查本地hosts文件是否有这个网址映射关系,如果有就调用这个IP地址映射,完成域名解析。如果没找到则会查找本地DNS解析器缓存,如果查找到则返回。如果还是没有找到则会查找本地DNS服务器,如果查找到则返回。最后迭代查询,按根域服务器 ->顶级域(.cn)->第二层域(hb.cn) ->子域( www.hb.cn )的顺序找到IP地址。

TCP传输

TCP建立连接

  • 第一次握手: 建立连接时,客户端发送syn包(syn=j)到服务器,并进入SYN_SENT状态,等待服务器确认;
  • 第二次握手: 服务器收到syn包,必须确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
  • 第三次握手: 客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。
  • 完成三次握手,客户端与服务器开始传送数据。

传输数据

建立好连接后,会发起HTTP请求,每个发过去的包都会经历下图的过程。

解析响应数据

服务器接收并解析请求,将请求转发到服务程序,服务程序读取完整请求并准备HTTP响应,服务器将响应报文又是一顿封包,再次经历了上图的过程,通过TCP发还给浏览器。浏览器接收到请求。

浏览器渲染

  • 构建dom树 因为浏览器无法直接理解和使用HTML,所以需要将HTML转换为浏览器能够理解的结构——DOM树

  • 样式计算 1.把 CSS 转换为浏览器能够理解的结构,当渲染引擎接收到 CSS 文本时,会执行一个转换操作,将 CSS 文本转换为浏览器可以理解的结构——styleSheets。 2.转换样式表中的属性值,使其标准化。 3.计算出 DOM 树中每个节点的具体样式。样式计算阶段的目的是为了计算出 DOM 节点中每个元素的具体样式,在计算过程中需要遵守 CSS 的继承和层叠两个规则。 CSS 样式来源主要有三种:通过 link 引用的外部 CSS 文件

  • 布局阶段 1.创建布局树,在显示之前,我们还要额外地构建一棵只包含可见元素布局树。 遍历 DOM 树中的所有可见节点,并把这些节点加到布局树中; 而不可见的节点会被布局树忽略掉。 2.布局计算

  • 分层 浏览器的页面实际上被分成了很多图层,这些图层叠加后合成了最终的页面 渲染引擎为特定的节点创建新的图层条件是什么? 1.拥有层叠上下文属性的元素会被提升为单独的一层。 2.需要剪裁(clip)的地方也会被创建为图层。


<style>
      div {
            width: 200;
            height: 200;
            overflow:auto;
            background: gray;
        } 
</style>
<body>
    <div >
        <p>所以元素有了层叠上下文的属性或者需要被剪裁,那么就会被提升成为单独一层,你可以参看下图:</p>
        <p>从上图我们可以看到,document层上有AB层,而B层之上又有两个图层。这些图层组织在一起也是一颗树状结构。</p>
        <p>图层树是基于布局树来创建的,为了找出哪些元素需要在哪些层中,渲染引擎会遍历布局树来创建层树(Update LayerTree)。</p> 
    </div>
</body>

在这里我们把 div 的大小限定为 200 * 200 像素,而 div 里面的文字内容比较多,文字所显示的区域肯定会超出 200 * 200 的面积,这时候就产生了剪裁,渲染引擎会把裁剪文字内容的一部分用于显示在 div 区域,下图是运行时的执行结果:

出现这种裁剪情况的时候,渲染引擎会为文字部分单独创建一个层,如果出现滚动条,滚动条也会被提升为单独的层。你可以参考下图:

  • 图层绘制 渲染引擎实现图层的绘制:会把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表。

  • 栅格化(raster)操作 当图层的绘制列表准备好之后,主线程会把该绘制列表提交(commit)给合成线程,那么接下来合成线程是怎么工作的呢? 通常一个页面可能很大,但是用户只能看到其中的一部分,我们把用户可以看到的这个部分叫做视口(viewport)。在有些情况下,有的图层可以很大,比如有的页面你使用滚动条要滚动好久才能滚动到底部,但是通过视口,用户只能看到页面的很小一部分,所以在这种情况下,要绘制出所有图层内容的话,就会产生太大的开销,而且也没有必要。基于这个原因,合成线程会将图层划分为图块(tile),这些图块的大小通常是 256x256 或者 512x512。

然后合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图。而图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的,运行方式如下图所示:

通常,栅格化过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫快速栅格化,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中。GPU 操作是运行在 GPU 进程中,如果栅格化操作使用了 GPU,那么最终生成位图的操作是在 GPU 中完成的,这就涉及到了跨进程操作。渲染进程把生成图块的指令发送给 GPU,然后在 GPU 中执行生成图块的位图,并保存在 GPU 的内存中。参考下图:

  • 合成和显示

一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。到这里,经过这一系列的阶段,编写好的 HTML、CSS、JavaScript 等文件,经过浏览器就会显示出漂亮的页面了。

  • 渲染流水线大总结

    结合上图,一个完整的渲染流程大致可总结为如下:

    • 渲染进程将 HTML 内容转换为能够读懂的 DOM 树结构。
    • 渲染引擎将 CSS 样式表转化为浏览器可以理解的 styleSheets,计算出 DOM 节点的样式。
    • 创建布局树,并计算元素的布局信息。
    • 对布局树进行分层,并生成分层树。
    • 为每个图层生成绘制列表,并将其提交到合成线程。
    • 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。
    • 合成线程发送绘制图块命令 DrawQuad 给浏览器进程。
    • 浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上。

重排:更新了元素的几何属性 从上图可以看出,如果你通过 JavaScript 或者 CSS 修改元素的几何位置属性,例如改变元素的宽度、高度等,那么浏览器会触发重新布局,解析之后的一系列子阶段,这个过程就叫重排。无疑,重排需要更新完整的渲染流水线,所以开销也是最大的。

重绘:更新元素的绘制属性 如果修改了元素的背景颜色,那么布局阶段将不会被执行,因为并没有引起几何位置的变换,所以就直接进入了绘制阶段,然后执行之后的一系列子阶段,这个过程就叫重绘。相较于重排操作,重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些。

直接合成阶段:更改一个既不要布局也不要绘制的属性 在上图中,我们使用了 CSS 的 transform 来实现动画效果,这可以避开重排和重绘阶段,直接在非主线程上执行合成动画操作。这样的效率是最高的,因为是在非主线程上合成,并没有占用主线程的资源,另外也避开了布局和绘制两个子阶段,所以相对于重绘和重排,合成能大大提升绘制效率。

关闭TCP连接

串起来!

  • 打开浏览器,此时各个进程间运行如下图

  • 浏览器进程接收到用户输入的URL请求。

  • 浏览器进程通过进程间通信将该URL转发给网络进程,在网络进程中发起真正的URL请求。

  • 网络进程接收到url请求后检查本地缓存是否缓存了该请求资源,如果有则将该资源返回给浏览器进程。

  • 如果没有,网络进程向web服务器发起http请求(网络请求),请求流程如下:

    • 进行DNS解析,获取服务器ip地址,端口
    • 利用ip地址和服务器建立tcp连接
    • 构建请求头信息
    • 发送请求头信息
    • 服务器响应后,网络进程接收响应头和响应信息,并解析响应内容 详细的请求部分如图所示 详细的浏览器缓存如图所示
  • 网络进程解析响应流程;

    • 检查状态码,如果是301/302重定向,从Location自动中读取地址,重新开始,如果是200,则继续处理请求。
    • 200响应处理:检查响应类型Content-Type,如果是字节流类型,则将该请求提交给下载管理器,该导航流程结束,不再进行后续的渲染,如果是html则将数据转发给浏览器进程,并通知浏览器进程准备渲染。
  • 浏览器进程检查当前url是否和之前打开的渲染进程根域名是否相同,如果相同,则复用原来的进程,如果不同,则开启新的渲染进程。

  • 渲染进程准备好后,浏览器进程接收到网络进程的响应头数据之后,发送“提交导航”消息到渲染进程。提交导航就是指浏览器进程将网络进程接收到的HTML数据提交给渲染进程。

  • 渲染进程接收到“提交导航”的消息后,和网络进程建立数据管道,开始准备接收HTML数据。

  • 等文档数据传输完成之后,渲染进程会返回“确认提交”的消息给浏览器进程,这是告诉浏览器进程:“已经准备好接受和解析页面数据了”。

  • 浏览器进程接收到渲染进程“提交文档”的消息之后,便开始移除之前旧的文档,然后更新浏览器进程中的页面状态,包括了安全状态、地址栏的URL、前进后退的历史状态,并更新Web页面。

  • 一旦文档被提交,渲染进程便开始页面解析和子资源加载了

这其中,用户发出 URL 请求到页面开始解析的这个过程,就叫做导航。