赞
踩
以前没怎么系统性的深入学习这方面,今天看了篇文章后还是觉得要好好整理归纳下的。顺便尝试能不能养成写笔记的习惯。
目录
在讲浏览器架构之前,先理解两个概念,进程
和线程
。
进程(process)是程序的一次执行过程,线程(thread)是CPU调度和分派的基本单位,也是跑在进程里面的,一个进程里面可能有一个或者多个线程,而一个线程只能隶属于一个进程。
因为浏览器是一个应用程序,而应用程序的一次执行,可以理解为计算机启动了一个进程。
进程启动后CPU会给该进程分配相应的内存空间,当我们的进程得到了内存之后,就可以使用线程
进行资源调度,进而完成我们应用程序的功能。同时应用程序可以采取多进程的方式来工作,因为进程和进程之间是互相独立的它们互不影响
,也就是说当其中一个进程挂掉了之后,是不会影响到其他进程的执行,只需要重启挂掉的进程就可以恢复运行。
在应用程序中,为了满足功能需要,启动的进程也会创建另外的新的进程来处理任务,这些新进程也拥有全新的独立内存空间(个人推测内存泄露应该就和这个有关)。
在Chrome中,主要的进程有4个:
浏览器进程 (Browser Process):负责浏览器的TAB的前进、后退、地址栏、书签栏的工作和处理浏览器的一些不可见的底层操作,比如网络请求和文件访问。
渲染进程 (Renderer Process):负责一个Tab内的显示相关的工作,也称渲染引擎。
插件进程 (Plugin Process):负责控制网页使用到的插件
GPU进程 (GPU Process):负责处理整个应用程序的GPU任务
进程关系:
1、在浏览器的地址栏里输入URL,浏览器进程 (Browser Process)会向该URL发送请求,获取这个URL的HTML内容并交给渲染进程 (Renderer Process);
2、渲染进程 (Renderer Process)解析HTML内容时,解析若遇到需要请求网络的资源将会返回给浏览器进程 (Browser Process)进行加载,同时通知Browser Process
需要Plugin Process
加载插件资源时要执行插件代码。解析完成后,Renderer Process
计算得到图像帧,并将这些图像帧交给GPU Process。
3、GPU Process
将其转化为图像显示屏幕。
一、更高的容错性。当今WEB应用中,HTML,CSS和JavaScript日益复杂,这些跑在渲染引擎的代码,频繁的出现BUG影响甚至导致渲染引擎崩溃,多进程架构使得每一个渲染引擎代码在各自的进程中相互不受影响,哪怕其中一个页面崩溃挂掉之后,其它页面还可以正常运行不爱影响。
二、更高的安全性和沙盒性(sanboxing)。渲染引擎会经常性的在网络上遇到不可信、甚至是恶意的代码,它们会利用这些漏洞在你的电脑上安装恶意的软件,针对这一问题,浏览器对不同进程限制了不同的权限,并为其提供沙盒运行环境,使其更安全更可靠。(vue文档中也对v-html描述有安全性问题,容易导致 XSS 攻击)
三,更高的响应速度。在单进程的架构中,各个任务相互竞争抢夺CPU资源,使得浏览器响应速度变慢,而多进程架构正好规避了这一缺点。
渲染进程 (Renderer Process)的作用是负责一个Tab内的显示相关的工作,这就意味着一个Tab就会有一个渲染进程,这些进程之间的内存无法进行共享,而不同的进程内存需要包含相同的内容。这里有点绕,就是说进程都是占独立内存,不过它们跑任务时会用到共用或常的内容(比如一些行为)。
为了节省内存,Chrome提供了四种进程模式(Process Models),不同的进程模式会对 tab 进程做不同的处理。
Process-per-site-instance (default) - 同一个 site-instance 使用一个进程
Process-per-site - 同一个站点(site)使用一个进程
Process-per-tab - 每个 tab 使用一个进程,所以每新建一个tab就会新建一个进程
Single process - 所有 tab 共用一个进程
这里需要给出 site 和 site-instance 的定义
site 指的是相同的 registered domain name(如:google.com ,bbc.co.uk)和scheme (如:https://)。比如a.baidu.com和b.baidu.com就可以理解为同一个 site(注意这里要和 Same-origin policy 区分开来,同源策略还涉及到子域名和端口)。
site-instance 指的是一组 connected pages from the same site,这里 connected 的定义是 can obtain references to each other in script code 怎么理解这段话呢。满足下面两中情况并且打开的新页面和旧页面属于上面定义的同一个 site,就属于同一个 site-instance
用户通过<a target="_blank">
这种方式点击打开的新页面
JS代码打开的新页面(比如 window.open
)
上文有提到,tab以外的大部分工作由浏览器进程Browser Process
负责,针对工作的不同,Browser Process 划分出不同的工作线程:
UI thread:控制浏览器上的按钮及输入框;
network thread:处理网络请求,从网上获取数据;
storage thread:控制文件等的访问;
当我们在浏览器的地址栏输入内容按下回车时,UI thread
会判断输入的内容是搜索关键词(search query)还是URL,如果是关键词就跳转至默认搜索引擎对应都搜索URL,如果是URL就开始请求URL。(这段其实不想抄的,感觉上过网的多多少少都知道)
索引开始,UI thread把对应的地址交给网络线程(Network thread),此时UI thread线程使该Tab前的图标展示为加载中状态,然后网络进程(Network thread)进行一系列操作(比如:DNS寻址,建立 TLS连接等)进行资源请求。
此时将会收到HTTP状态码,若为3xx若需要重写向的url跳转,则跳转并请求
1** | 信息,服务器收到请求,需要请求者继续执行操作 |
2** | 成功,操作被成功接收并处理 |
3** | 重定向,需要进一步的操作以完成请求 |
4** | 客户端错误,请求包含语法错误或无法完成请求 |
5** | 服务器错误,服务器在处理请求的过程中发生了错误 |
network thread
接收到服务器的响应后,开始解析HTTP响应报文,然后根据响应头中的Content-Type
字段来确定响应主体的媒体类型(MIME Type),如果媒体类型是一个HTML文件,则将响应数据交给渲染进程(renderer process)来进行下一步的工作,如果是 zip 文件或者其它文件,会把相关数据传输给下载管理器。
与此同时,浏览器会进行 Safe Browsing(安全浏览) 安全检查,如果域名或者请求内容匹配到已知的恶意站点,network thread 会展示一个警告页。对于Google来说,他有自己的开源API, 通过把目标网站POST到Google的服务区上,来查看返回值,已确定网站是否安全。
除此之外,网络线程还会做 CORB(Cross Origin Read Blocking)检查来确定那些敏感的跨站数据不会被发送至渲染进程。
各种检查完毕以后,network thread 确信浏览器可以导航到请求网页,network thread 会通知 UI thread 数据已经准备好,UI thread 会查找到一个 renderer process 进行网页的渲染。
浏览器为了对查找渲染进程这一步骤进行优化,考虑到网络请求获取响应需要时间,所以在第二步开始,浏览器已经预先查找和启动了一个渲染进程,如果中间步骤一切顺利,当 network thread 接收到数据时,渲染进程已经准备好了,但是如果遇到重定向,这个准备好的渲染进程也许就不可用了,这个时候会重新启动一个渲染进程。
到了这一步,数据和渲染进程都准备好了,Browser Process
会向 Renderer Process
发送IPC消息来确认导航,此时,浏览器进程将准备好的数据发送给渲染进程,渲染进程接收到数据之后,又发送IPC消息给浏览器进程,告诉浏览器进程导航已经提交了,页面开始加载。
这个时候导航栏会更新,安全指示符更新(地址前面的小锁),访问历史列表(history tab)更新,即可以通过前进后退来切换该页面。
当导航提交完成后,渲染进程开始加载资源及渲染页面(详细内容下文介绍),当页面渲染完成后(页面及内部的iframe都触发了onload事件),会向浏览器进程发送IPC消息,告知浏览器进程,这个时候UI thread会停止展示tab中的加载中图标。
根据上文总结如下:
1、在浏览器的地址栏里输入内容,浏览器进程 (Browser Process)下的UI thread会把对应的URL交给Browser Process下的网络进程(Netword thread)发送请求并读取响应,解析HTTP响应报文并根据其响应头里的Content-Type字段分析的媒体类型
若媒体类型是下载文件,会把相关数据传输给浏览器的下载管理器;
若媒体类型是一个html文件则浏览器会进行安全浏览(Safe Browsing)检查,Netword thread还会做CORB检查,各种检查后都没问题network thread会通知UI thread数据已经准备好,UI thread会查找到渲染进程 (Renderer Process)进行网页渲染。
2、渲染进程 (Renderer Process)解析HTML内容时,解析若遇到需要请求网络的资源将会返回给浏览器进程 (Browser Process)进行加载,同时通知Browser Process
需要Plugin Process
加载插件资源时要执行插件代码。解析完成后,会向浏览器进程发送IPC消息,告知浏览器进程,这个时候UI thread会停止展示tab中的加载中图标。
导航过程完成之后,浏览器进程把数据交给了渲染进程,渲染进程负责tab内的所有事情,核心目的就是将HTML/CSS/JS代码,转化为用户可进行交互的web页面。那么渲染进程是如何工作的呢?
渲染进程中,包含线程分别是:
一个主线程(main thread)
多个工作线程(work thread)
一个合成器线程(compositor thread)
多个光栅化线程(raster thread)
当渲染进程接受到导航的确认信息后,开始接受来自浏览器进程的数据,这个时候,主线程会解析数据转化为DOM(Document Object Model)对象。
在构建DOM的过程中,会解析到图片、CSS、JavaScript脚本等资源。这些资源是需要从网络或者缓存中获取的,主线程在构建DOM过程中如果遇到了这些资源,逐一发起请求去获取,而为了提升效率,浏览器也会运行预加载扫描(preload scanner)程序,如果HTML中存在img
、link
等标签,预加载扫描程序会把这些请求传递给Browser Process
的network thread进行资源下载。
构建DOM过程中,如果遇到<script>
标签,渲染引擎会停止对HTML的解析,而去加载执行JS代码,原因在于JS代码可能会改变DOM的结构(比如执行document.write()
等API)
不过开发者其实也有多种方式来告知浏览器应对如何应对某个资源,比如说如果在<script>
标签上添加了 async
或 defer
等属性(虽然笔者是没用过,没用MVVM框架之前,还是JQ的年代时,笔者是把共用的js文件放在head标签里按优先级排序,再把该页面会用到的js文件放在body后面),浏览器会异步的加载和执行JS代码,而不会阻塞渲染。
DOM树只是我们页面的结构,我们要知道页面长什么样子,我们还需要知道DOM的每一个节点的样式。主线程在解析页面时,遇到<style>
标签或者<link>
标签的CSS资源,会加载CSS代码,根据CSS代码确定每个DOM节点的计算样式(computed style)。
计算样式是主线程根据CSS样式选择器(CSS selectors)计算出的每个DOM元素应该具备的具体样式,即使你的页面没有设置任何自定义的样式,浏览器也会提供其默认样式(每个浏览器的默认样式都不一致,要reset下)。
DOM树和计算样式完成后,我们还需要知道每一个节点在页面上的位置,布局(Layout)其实就是找到所有元素的几何关系的过程。
主线程会遍历DOM 及相关元素的计算样式,构建出包含每个元素的页面坐标信息及盒子模型大小的布局树(Render Tree),遍历过程中会跳过隐藏的元素(display: none),另外伪元素虽然在DOM上不可见,但是在布局树上是可见的。
布局 layout 之后,我们知道了不同元素的结构,样式,几何关系,我们要绘制出一个页面,我们要需要知道每个元素的绘制先后顺序,在绘制阶段,主线程会遍历布局树(layout tree),生成一系列的绘画记录(paint records)。绘画记录可以看做是记录各元素绘制先后顺序的笔记。
这一块真要理的话,笔者自己都有点乱,水有点深,特别是光栅化内容(有兴趣的话去看下参考文献1和3)。笔者是没有兴趣再深入了,简单整理如下:
把结构、元素的样式、元素的几何关系、绘画顺序这些信息转化为显示器中的像素时,这个转化的过程叫做光栅化
(rasterizing)。
合成是一种将页面分成若干层,然后分别对它们进行光栅化,最后在一个单独的线程 - 合成线程(compositor thread)里面合并成一个页面的技术。
Chromium 使用的是异步分块光栅化的策略,除了一些特殊图层外(比如 Canvas,Video):
对于异步光栅化来说,为图层分配额外的像素缓冲区是必须的,而使用分块的方式比起分配一个完整大小的像素缓冲区有很多优势:
当页面渲染完毕以后,TAB内已经显示出了可交互的WEB页面,用户可以进行移动鼠标、点击页面等操作了,而当这些事件发生时候,浏览器是如何处理这些事件的呢?
以点击事件(click event)为例,让鼠标点击页面时候,首先接受到事件信息的是Browser Process
,随后便把事件的信息传递给了渲染进程,渲染进程会找到根据事件发生的坐标,找到目标对象(target),并且运行这个目标对象的点击事件绑定的监听函数(listener)。
由于执行 JS 是主线程的工作,当页面合成时,合成器线程会标记页面中绑定有事件处理器的区域为非快速滚动区域
(non-fast scrollable region),如果事件发生在这些存在标注的区域,合成器线程会把事件信息发送给主线程,等待主线程进行事件处理,如果事件不是发生在这些区域,合成器线程则会直接合成新的帧而不用等到主线程的响应。
而对于非快速滚动区域的标记,需要注意到全局事件的绑定,比如事件委托将目标元素的事件交给根元素body进行处理,示例如下:
- document.body.addEventListener('touchstart', event => {
- if (event.target === area) {
- event.preventDefault()
- }
- })
这一段代码给body元素绑定了事件监听器,那么整个页面都被编辑为一个非快速滚动区域,哪怕你的页面的某些区域没有绑定任何事件,然而每次用户触发事件时,合成器线程也需要和主线程通信并等待反馈,流畅的合成器独立处理合成帧的模式就失效了。
在这种情况下需要在事件监听时传递passtive
参数为 true,passtive
会告诉浏览器你既要绑定事件,又要让组合器线程直接跳过主线程的事件处理直接合成创建组合帧。示例如下:
- document.body.addEventListener('touchstart', event => {
- if (event.target === area) {
- event.preventDefault()
- }
- }, {passive: true});
当合成器线程接收到事件信息,判定到事件发生不在非快速滚动区域后,合成器线程会向主线程发送这个时间信息,主线程获取到事件信息的第一件事就是通过命中测试(hit test)去找到事件的目标对象。具体的命中测试流程是遍历在绘制阶段生成的绘画记录(paint records)来找到包含了事件发生坐标上的元素对象。
一般我们屏幕的帧率是每秒60帧,也就是60fps,但是某些事件触发的频率超过了这个数值,比如wheel,mousewheel,mousemove,pointermove,touchmove,这些连续性的事件一般每秒会触发60~120次,假如每一次触发事件都将事件发送到主线程处理,由于屏幕的刷新速率相对来说较低,这样使得主线程会触发过量的命中测试以及JS代码,使得性能有了没必要是损耗。
浏览器的多进程架构,根据不同的功能划分了不同的进程,进程内不同的使命划分了不同的线程,当用户开始浏览网页时候,浏览器进程进行处理输入、开始导航请求数据、请求响应数据,查找新建渲染进程,提交导航,之后渲染又进行了解析HTML构建DOM、构建过程加载子资源、下载并执行JS代码、样式计算、布局、绘制、合成,一步一步的构建出一个可交互的WEB页面,之后浏览器进程又接受页面的交互事件信息,并将其交给渲染进程,渲染进程内主进程进行命中测试,查找目标元素并执行绑定的事件,完成页面的交互。
https://mp.weixin.qq.com/s/QVfBgLXHKLQS-RkRi-cf4w
https://blog.csdn.net/st_andrews/article/details/78673453
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。