前言
现代浏览器十分复杂,颇有运行在操作系统之上的"操作系统"的意思,我们将尽可能用简单容易理解的例子来简单概括它主要的工作逻辑。
目录:
- 进程与线程概述;
- 浏览器架构;
- 浏览器视角下的输入;
- 页面如何渲染;
- 如何进行交互;
Part 1. 进程与线程概述
计算机的核心是 CPU,它承担了几乎所有的计算任务。
你可以把 CPU 想象成是一个工厂,时刻在运行着。
假设这个工厂的电力有限,同一时刻只能供一个车间使用。这也就意味着,一个车间正在使用,其他车间都将不会被使用。
进程就好比车间,是工厂将要执行的任务。潜台词就是说,单个 CPU 任意时刻总是只能运行一个任务。
一个车间可以有很多的工人,它们协同完成同一个任务。
线程就是车间里的工人。
假设工人都是很耗电的机器人,靠着分得工厂给的电力进行任务,每一次给的电力刚好够完成本次的任务,而工厂同一时刻又只能给一个机器人供电。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V53fOee0-1613461453925)(https://cdn.jsdelivr.net/gh/wmyskxz/BlogImage02/2021-2-12/1613111336416-2021-02-12%2014.22.01.gif)]
这几乎就是单核 CPU 的工作方式了:同一时刻只能做一个工作。
但你仍然感觉到许多不同的任务正在 “同时” 运行着,这是因为当切换任务的速度足够快时,你将感知不到 CPU 同一时刻只能做一个工作的特性:
我们的 CPU 就这样飞速地奔腾着。
每当我们打开一个应用,就会启动一个进程。程序也会创建一个或多个线程来帮助它完成工作。
操作系统会为进程提供一个可使用的 “一块” 内存,就像开工厂占地一样,所有应用程序的状态信息都会保存在该私有内存空间中。程序关闭时,相应进程会消失,操作系统也会释放内存。
进程可以请求操作系统启动另一个进程来执行不同的任务。此时内存不同区域会分给新进程。
如果两个进程需要对话,他们可以通过 进程间通信(IPC) 来进行。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z7dxVUFV-1613461453928)(https://cdn.jsdelivr.net/gh/wmyskxz/BlogImage02/2021-2-14/1613273462146-2021-02-14%2011.28.02.gif)]
许多应用程序就是这样设计的,如果一个工作进程失去响应,该进程就可以在不停止应用程序的情况下靠着其他进程重新启动。
Part 2. 浏览器架构
那么如何通过进程和线程构建 web 浏览器呢?
虽然对于如何构建 web 浏览器没有明确的标准,但现在拥有一个导航栏、输入框、标签页这样类似的设计却是不同浏览器之间默契的共同选择。
浏览器的架构也总体分为两类:
现在已经很难看到单进程的架构方式了,因为单进程的浏览器需要处理的事情太多(网络、渲染、管理插件等),极不稳定和安全。因此市面上主流的浏览器都已经升级为多进程的方式。
就拿 Chrome 举例来说,就采取了下方的架构方式:
- 最顶层是浏览器进程,负责协调处理其他进程模块的任务。
- UI 进程负责控制地址栏、标签页等;
- 渲染进程控制标签页内网站的展示。
- 插件进程控制站点使用的任意插件,比如:Flash。
- GPU 进程单独处理来自不同应用发送的绘制请求。
- …
多进程的好处显而易见。比如当你打开了三个标签页,其中一个崩溃了,你可以关掉它而不会影响其他两个标签页:
并且由于进程的数据是私有的,所以一定程度上能够保证安全性。
但缺点也显而易见。我们上面用车间来类比进程,用工人来类比线程,显然「建一座车间」比「招聘一个工人」消耗的资源要大得多——哪怕车间只有一个工人——这里比较明显的是对内存的消耗。
为了避免过大的内存消耗,Chrome 把一些服务做了聚合:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EdOOtNjl-1613461453933)(https://cdn.jsdelivr.net/gh/wmyskxz/BlogImage02/2021-2-15/1613381859976-2021-02-15%2017.37.07.gif)]
这样就能一定程度上减少内存的开销。
Part 3. 浏览器视角下的输入
当在浏览器中键入一个 URL 地址,浏览器会做什么处理呢?
第一步:处理输入
我们已经习惯了一个链接打开就对应一个外部网站,但它还可能是浏览器本身的设置页(如 chrome://settings/
),或是本地硬盘的地址(如 Mac 下的 \
):
所以我们的第一步就是要判断这个输入到底是个啥:
第二步:开始导航
随着用户输入完毕按下 Enter 键,UI 线程知道要启用网络去调取网站的信息。网络线程会负责联系目标主机并获取到信息:
网络线程获取信息的过程,发生了很多事,比如 DNS 域名解析、TLS 建立连接等,如果不熟悉可以看看之前的系列文章。
第三步:读取响应
总之网络线程为我们取到了来自网站的响应,大概长这样:
响应分为 header 和 payload 两个部分。header 类似于一本书的版权、作者介绍等相关信息,而 payload 才是真实的数据内容。
浏览器需要根据响应头里的 Content-Type
来区分对应内容的类型,例如 text/html
时浏览器会对内容进行 HTML 解析,image/png
则调用图片渲染器。
然而完全信任网站响应的 Content-Type
是不行的,因为一旦 Content-Type
未指定或者是一个错误的值的时候,就会发生未知的错误。
所以当收到响应主体(payload)时,网络线程会在必要时检查数据的前几个字节,以确保数据内容与 header 里标识的数据类型(Content-Type)一致。如果不一致,那么就需要进行 MIME 类型嗅探来猜测该数据的类型。
当响应是一个 HTML 文件时,此时也会进行安全检查(SafeBrowsing 检查)。如果域名和相应数据似乎匹配到了一个已知的恶意网站,那么网络线程会显示一个警告页面。
除此之外,还会发生 Cross Origin Read Blocking(CORB)检查,以确保敏感的跨域数据不被传给渲染进程。
第四步:查找渲染进程
一旦所有的检查执行完毕并且网络线程确信浏览器会导航到请求的站点,网络线程会告诉 UI 线程所有的数据准备完毕。UI 线程会寻找渲染进程去开始渲染 web 页面。
由于网络请求会花费几百毫秒才获取回响应,因此可以应用一个优化措施。
当第 2 步 UI 线程正发送一个 URL 请求给网络线程时,它已经知道它们会导航到哪个站点。在网络请求的同时,UI 线程并行地尝试主动寻找或开启一个渲染进程。
这样,如果一切按预期进行,渲染进程在网络线程接受到数据时就已经处于待命状态。
第五步:提交导航
现在数据和渲染进程已经就绪,浏览器进程会发送一个 IPC(进程间通信)到渲染进程去提交导航。
这时地址栏会更新、标签页的历史记录也会更新,前进/后退按钮会走向刚导航过的站点。渲染进程根据 HTML 内容开始解析并渲染页面。最终您将看到网站设计者设计的网站。
Part 4. 页面如何渲染
渲染进程涉及 Web 性能的许多方面,流程非常复杂,我们只做必要的理解。如果您想要深入了解,可以在 web.dev
找到相关资源。
渲染进程内部包含主线程、工作线程、合成线程和光栅线程。
在详细说明之前,请先想象一个这样的场景:您站在一副简单绘画的面前,如何通过打电话来让您的朋友知道这幅画究竟长什么样子呢?
如果您真打算这么做,这里参考 HTML 解析的过程给您提供一些建议。
首先,图中的元素以及具体元素的属性分开描述(如:图里有一个圆是元素,圆有多大具体在什么位置等是属性):
这样做的好处是可阅读性变高了,有哪些元素,以及元素哪些属性一目了然,也利于分别维护和修改。(类似于书的目录和对应内容一样)
另外是你可以提炼一些通用的属性来减少描述:
然后,最好是分层进行描述,因为图画是有层次的,光有元素大小、位置等信息是不够的:
元素实际上就是我们通常说的 HTML 文件,HTML 文件中包含了描述元素属性的 CSS 样式文件。每个浏览器对应常见的样式都会有默认的样式。
浏览器实际上要知道绘制些什么元素,每个元素属性如何是要分成三步的:1)通过 HTML 绘制元素树(俗称 DOM 树);2)通过 CSS 文件绘制样式树(俗称 CSSOM 树);3)综合两颗树绘制渲染树(俗称 Render Tree);
现在浏览器知道文档的结构、每个元素的样式、页面的几何形状和绘制顺序,它是如何绘制页面的?把这些信息转换为屏幕上的像素,我们称为光栅化。
处理这种情况的一种简单的方法是,先在光栅化视窗内的画面,如果用户滚动页面,则移动光栅框,并光栅化填充缺少的部分。这就是 Chrome 首次发布时处理光栅化的方式。
但是,现代浏览器会运行一个更复杂的过程,我们称为合成。
合成是一种将页面的各个部分分层,分别光栅化,并在称为合成线程的单独线程中合成为页面的技术。如果发生滚动,由于图层已经光栅化,因此它所要做的只是合成一个新帧。动画也可以以相同的方式(移动图层和合成新帧)实现。
另外需要说明的是如何进行描述是有相当的技巧的。例如「正中心有一个 半径为 2 的圆」和「正中心有一个 直径为页面宽度 50% 的圆」是完全不同的:
如何进行组织描述,这需要网站建设者的经验。
Part 5. 如何进行交互
在浏览器眼中,用户的一切行为都是输入。不单单是滚动鼠标滑轮,或是点击屏幕、按下按键等。
对于浏览器进程来说只存在事件和对应坐标,只有渲染进程知道页面究竟长啥样,以及究竟该如何处理事件。浏览器进程只负责把事件和坐标发送给渲染进程。
我们也可以编写自己的逻辑文件(js 文件)来监听某一事件进行对应的处理。然后再统一由渲染进程进行合成。为了浏览流畅,浏览器需要保证渲染进程的渲染速度与屏幕刷新率一致(大概每秒 60 帧)。
另外为了降低主线程中传递过量的调用,Chrome 也会把一些连续的事件进行合并。
浏览器进程监听并发送事件给渲染进程进行渲染,这大概就是浏览器交互的基本方式。
后记
浏览器的复杂远不是一篇文章能解释清楚的,本篇文章也只是想让大家理解浏览器的基本过程和原理。尽可能使用动图的形式清晰地表达,希望大家能用餐愉快。
本文大量借鉴了 Chrome 官方 developer 分享的系列文章(下2),如果有想更加深入了解的小伙伴也可以阅读更加硬核的浏览器工作原理揭秘文章(下4)
至此,我们对浏览器已经有了相当的了解了。后续也会继续跟大家一起学习计算机网络的基础知识,也会尝试着跟着后端学习路线图的脚步跟着大家一起学习进阶。
这里是我没有三颗心脏,欢迎关注公众号 wmyskxz,2021,与您在 Be Better 的路上共同成长!
参考资料
- 进程与线程的简单解释 - http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html
- 转载:现代浏览器内部揭秘 - https://hasaki.xyz/blog/2020-01-20-%E8%BD%AC%E8%BD%BD%E7%8E%B0%E4%BB%A3%E6%B5%8F%E8%A7%88%E5%99%A8%E5%86%85%E9%83%A8%E6%8F%AD%E7%A7%98/
- 深入浅出浏览器渲染原理 - https://blog.fundebug.com/2019/01/03/understand-browser-rendering/
- 浏览器工作原理幕后揭秘 - https://www.html5rocks.com/zh/tutorials/internals/howbrowserswork/
(完)