前言
浏览器是目前使用范围最广、使用人数最多的终端应用程序之一。深入去了解浏览器原理,去看看巨型软件应用优秀的架构设计。
浏览器内核的演变史
1994年网景浏览器应运而生,那时候的网景浏览器只能展示最简单的 HTML 静态页面,不支持动态的脚本(JavaScript)和样式(CSS)。在操作系统中,内核是最基本的功能,浏览器中的内核我们叫做渲染引擎,英文名Rendering Engine。
常见的浏览器内核有
- Blink:Google Chrome、Opera
- WebKit:Safari、iOS Safari
- Gecko:Mozilla Firefox
- Trident:Internet Explorer (已停止更新)
目前WebKit 内核占据了非常大的的市场,包括 Chrome、Safari、安卓浏览器等市面上的主流浏览器,都使用了 WebKit 内核。
浏览器内核(WebKit)架构
WebKit主要包含HTML Parser,CSS Parser,Layout,JavaScript Engine 几部分,如下图所示:
暂时无法在飞书文档外展示此内容
- HTML Parser:HTML 解析器,负责 HTML 文本的解析,将 HTML 解析为可编程结构 —— DOM (文档对象模型)树;
- CSS Parser:CSS 解析器是层叠样式的解析器,用来计算布局所需要的节点样式信息 —— CSSOM(样式)树;
- Layout:布局,在 得到 DOM 树和 CSSOM 树后,需要计算出 DOM 树中可见元素的几何位置,生成布局树 —— Layout Tree;
- JavaScript Engine:JavaScript 语言的解析引擎,执行页面的动态逻辑,并可以访问 DOM 和 CSSOM 数据接口;
- 操作系统支持 —— 移植:WebKit 代码中,因为其天生具有跨平台性质,所以部分平台相关的能力需要做跨平台兼容的移植。
Chromium 浏览器架构
在上图中,比较重要的是 Content 模块以及 Content 接口。
Content 模块和接口是浏览器对渲染过程的抽象,它们将浏览器的渲染、插件、沙箱等功能,进行了包装和抽象,提供一个接口层,方便上层的应用调用。
浏览器可视化界面构建在 Content 接口之上,用于接收用户交互和展示界面。
content shell 是一个简易版的浏览器,通常被第三方浏览器软件进行二次开发,它在 Andriod 系统上也应用广泛。
浏览器下的多进程与多线程模型
进程是应用程序运行时操作系统进行资源分配的最小容器,这些资源包括指令集、独立内存空间、IO、PCB 等等
进程虽然能帮助我们更方便地分配资源,也会引发一些问题:
-
进程切换上下文的开销比较大。由于虚拟内存的存在,我们需要从硬盘中频繁读写;
-
多进程应用通讯复杂度高。由于操作系统的保护策略,系统资源跨进程是无法共享的。如果需要跨进程共享资源就要采用 IPC 通讯 ,但是成本相对高。
线程则是 CPU 调度的基本单位。
优点:显而易见,切换成本很低,只有少量 CPU 寄存器、堆栈等内容,线程的创建、销毁本身也有性能成本,但这个成本相对较低,而且通常可以通过线程池优化。
缺点:
-
线程可以共享进程内的所有资源,但需要考虑资源竞态问题;
-
线程间的指令时序不可预测,无法保证代码按照预期的顺序执行;
-
单个进程崩溃可能会影响其他线程。
那么在目前常见的 Chrome 浏览器里,采用的是多进程还是单进程多线程模型呢?
在我们的电脑中,打开任务管理器,就可以看到浏览器的后台进程占用情况。通常后台都存在多个进程
其实在一些旧的浏览器中,采用的是单进程多线程的模型,如 IE 浏览器;但是以 Chromium 浏览器为例的现代浏览器,采用的都是多进程架构。
那么为什么现代浏览器采用的是多进程架构呢? 我们需要先分析一下,浏览器如果是单进程多线程会引发哪些问题。
- 稳定性的问题。因为一个浏览器程序,是可以同时启动多个 Tab 的,浏览器多进程化的最大的好处就是,单个 Tab 的卡死、崩溃不会影响其它 Tab。
- 加载速度的问题。 由于整个程序只存在一个进程,浏览器的 JS 代码和插件逻辑和页面渲染是运行在同一个进程中的,如果存在一些计算量很大的操作,这些计算量大的线程会抢占大量资源,从而导致其他的渲染逻辑无法正常执行。这会严重影响页面的加载速度,甚至造成崩溃。在多进程架构下,将插件提取为单独的进程,不会存在插件卡顿和崩溃影响整个浏览器的情况。
- 安全性的考虑。 由于 JS 脚本和插件的存在,很容易利用浏览器的系统漏洞,进而获得整个计算机的权限,从而造成安全问题。而多进程架构很容易就可以实现沙箱控制。
我们在浏览器程序内新建一个 Tab 时,就会启动一个新的渲染进程,Chrome 支持四种不同的进程模型模型:
- Process-per-site-instance。这种进程模型会为每一个同一个域的实例都会创建一个 Renderer 进程。
- Process-per-site。这种进程模型会为不同一个域创建独立的进程,同一域的不同实例共享同一个进程。
- Process-per-tab。这种进程模型会为每个标签页创建一个 Renderer 进程。
- Single process。这种进程模型不为页面创建任何独立的进程,所有渲染工作都在 browser 进程中(这种模式是实验性质的,不推荐使用)。
Chromium 默认采用 Process-per-site-instance 方式,不过我们可以在浏览器启动时传递一个命令行开关,用来指定浏览器的进程模型。
采用多进程模型就可以解决上述几个问题,但是多进程模型也不是银弹,它同样也会引发一些问题。
比如性能的问题,由于基础的指令无法共享,多进程会带来很大程度的资源浪费,由于每个 Tab 和插件都是一个独立的进程,所以在打开多个 Tab 或者插件的情况下,我们会看到系统的内存会疯狂飙升。
Chrome 有哪些进程
- 浏览器进程:主要负责用户交互、子进程管理和文件储存等功能;
- 网络进程:浏览器主进程和渲染进程通过他来向操作系统申请端口以及与操作系统的协议栈进程通信;
- 渲染进程:主要职责是把从网络下载的 HTML、JavaScript、CSS、图片等资源解析为可以显示和交互的页面;
- 插件进程:主要负责单个插件功能的运行;
- GPU 进程:主要负责 3D 效果的实现以及 UI 的绘制。
着重了解渲染进程(renderer)内的主要线程
- GUI 线程:负责渲染浏览器中的页面,并解析 HTML,CSS;
- JS 线程:负责处理 JavaScript 脚本程序;
- 事件触发线程:归属于浏览器而不是 JS 引擎,用来控制事件循环;
- 定时触发器线程:浏览器的定时任务,如 setInterval 与 setTimeout 事件,也包括浏览器内部的一些定时任务。
- IO 线程:用来和其他进程进行 IPC 通信,接受发送消息;
- 异步 http 请求线程:处理所有的异步请求,如果有回调函数,就放入异步事件队列,由事件触发线程处理;
- WebWorker 线程:每声明一个 WebWorker 就会新建一个 WebWorker 线程处理;
- 合成线程:在 GUI 渲染后执行,将 GUI 渲染线程生成的产物转换为位图。