TL;NR
在 Blink 上开发绝非易事。对于新接触 Blink的开发者来说,要实现一个高效的渲染引擎,需要了解大量Blink特有的概念和编码约定。对于经验丰富的开发者来说亦非易事,因为 Blink 非常庞大,对性能、内存和安全性极为敏感。
本文从全局概述了 "Blink 是如何工作的",希望有助于开发者快速熟悉Blink的架构:
- 本文并非Blink详细架构与编码规范的开发手册(这些内容可能会变化或过时)。相反地,本文简明扼要地介绍了短期内不会变更的Blink基本原理,并提供了可以进一步阅读的资源(如果你想了解更多的话)
- 本文不解释特定功能(e.g. ServiceWorkers, editing),相反地,本文解释了代码中广泛使用的基本功能(e.g. 内容管理, V8 APIs).
关于Blink开发的更多内容, 参考这里 Chromium wiki page
继续阅读
没错, 这里说的那个浏览器, 它就是 Chrome 。 以及包括 Edge 在内的绝大多数基于 Chromium 开发的 web浏览器,
我们在登录网站后, 浏览器会提示我们是否保存密钥,我们点下 “是”, 密钥就被浏览器记录。下次打开网站时它甚至会贴心地帮我们自动填充好用户名和密码。我们可以在 chrome://settings/passwords
里查看已保存的密码, 它们会随浏览器账户同步到任何使该浏览器的设备上。当我们要查看某个密码时, 浏览器会要求我们先输入设备密钥。


我一直认为它足够安全。直到有一天我研究了 Chromium 的源码。
密码保存的核心代码位于 components\password_manager\core\browser\login_database.cc
。这个文件会对用户输入的密码信息进行保存, 包括网站 URL, 用户名在表单中的元素, 用户名, 密码元素, 密码值等。Chromium 在将密码信息保存在本地时, 对密码值做了加密。但对于其它信息,则完全没有提供任何保密措施。
继续阅读
译注:本文翻译自 ChromeDeveloper 博客, 原文发表于2018年底, 部分特性可能与目前 Chrome 有所不同。建议有条件的读者直接阅读原文。
本章是深入理解现代浏览器系列四章中的第后一章, 研究如何处理我们的代码并显示网页。之前的几章中, 我们研究了渲染进程,并了解了合成器。本章我们将研究当用户输入时,合成器如何实现与用户的流畅交互。
当说到"输入事件(input events)" , 你可能只会想到在文本框输入字符或点击鼠标,但以浏览器的视角来看,输入意味着用户的任何手势(gesture)。鼠标滚动是,触摸(touch)是, 鼠标悬停也是。
当用户触摸屏幕时,浏览器进程是首先接收到该手势的进程。但是, 因为标签内的内容是由渲染器进程处理的,browser 进程只知道该手势的位置。所以浏览器进程将事件类型(如 touchstart
) 和其坐标发送给渲染器进程。渲染器进程通过查找事件目标及运行其附加的事件监听器来适当地处理处理事件。

通过浏览器进程路由到渲染器进程的输入事件
继续阅读
译注:本文翻译自 ChromeDeveloper 博客, 原文发表于2018年底, 部分特性可能与目前 Chrome 有所不同。建议有条件的读者直接阅读原文。
本章是该系列4章中的第3章。之前我们研究了多进程架构和导航流程。在此章中, 我们将渲染进程内部发生了什么
渲染进程涉及到web性能的方方面面。由于渲染进程内部做了太多工作, 本章只做一个总体概述,如果你想深挖,the Performance section of Web Fundamentals 里有更多的资源。
渲染进程处理 web 内容
渲染进程负责处理标签内的一切。在渲染进程中, 主线程处理你发送给用户的大部分代码。如果你使用 web worker 或 service worker, 有时部分 JavaScript 代码由 worker 线程处理。合成器(compositor) 和栅格(raster)线程也在渲染进程内运行,以高效流畅地渲染页面。
渲染进程的核心工作是将 HTML,CSS 和 JavaScript转换为可与用户交互的网页。

渲染进程中的主线程、worker 线程等
解析
DOM的构建
当渲染进程接收到导航的提交消息, 并开始接收 HTML 数据时, 主线程开始解析文本字符串(HTML)并将其转换为 DOM(Document Object Model).
DOM 是浏览器对页面的内部表示,也是 Web 开发人员可以通过 JavaScript 与之交互的数据结构和 API。
将 HTML 文档转换为 DOM 的解析行为由 HTML 标准定义。你可能已经注意到了, 向浏览器塞入 HTML 从来不会引发错误。比如缺少 </p>
tag 的HTML是有效的 。像 Hi! <b>I'm <i>Chrome</b>!</i>
(b tag 在 i tag 之前关闭)这样的错误标记会被当作 Hi! <b>I'm <i>Chrome</i></b><i>!</i>
。 这是因为 HTML 规范旨在优雅地处理这些错误。如果你感到好奇, 你可以阅读 "An introduction to error handling and strange cases in the parser" 关于 HTML 的部分。
继续阅读
译注:本文翻译自 ChromeDeveloper 博客, 原文发表于2018年底, 部分特性可能与目前 Chrome 有所不同。建议有条件的读者直接阅读原文。
本文是深入理解现代浏览器4章中的第2章。此前我们研究了不同进程和线程是如何处理浏览器的不同部件的, 本章中, 我们来深挖每个进程和线程间是如何通信以显示网站的。
我们来看一个简单浏览器网站的用例:在浏览器中输入URL, 然后浏览器从网络上获取数据并显示页面。本章中, 我们聚焦于用户请求站点并准备呈现页面这一部分--即我们说的导航(Navigation)。
从浏览器进程开始说起

browser 进程
如我们之前所说,标签以外的所有内容都由 browser 进程处理。browser 进程拥有很多线程, 像绘制按钮和输入域的 UI 线程,和网络栈交换信息以从网上接收数据的 network 线程,控制文件访问的 storage 线程等等。当你在地址栏输入URL时, browser 进程的 UI线程会处理你的输入。
简单的导航
Step 1: 处理输入
当用户开始在地址栏中输入时,UI线程首先会问"这是一搜索查询还是URL"。 在Chrome 中, 地址栏也是搜索输入域,所以UI线程需要解析并确认是使用搜索引擎还是请求站点。

UI 线程询问输入的是搜索还是 URL
Step 2: 开始导航
当用户点击回车,UI 线程启动一个网络调用并获取站点内容。标签的左上角会显示加载动画,并且 network 线程这通过适当的协议来处理请求, 如 DNS查找与TSL连接建立。

UI 线程告诉 network线程, 要导航到 mysite.com
此时, network 线程可能收到像HTTP301 这样的服务重定向头。这种情况下, network 线程和UI线程交流, 服务请求重定向, 之后开始另一个URL请求。
继续阅读
译注:本文翻译自 ChromeDeveloper 博客, 原文发表于2018年底, 部分特性可能与目前 Chrome 有所不同。建议有条件的读者直接阅读原文。
本系列有4章, 我们将从高层架构到管道渲染的细节来深入理解 Chrome 浏览器。如果你想知道浏览器如何将你的代码变成功能性的网站, 或者你不确实为何被建议使用某种技术来提高网页性能, 那么本系列会适合你。
在本章中, 我们将了解核心计算的术语及Chrome 的多进程架构。
计算机的核心是 CPU 和 GPU
为了理解浏览器运行的环境, 我们需要理解一些计算机部分及它们的作用。
CPU

首先是 中央处理单元(CPU) , CPU可以比作你计算机的大脑。一个CPU核心,就如图中的一个办公室职员, 可以一一处理很多不同的任务。它可以处理从数学到文艺的所有的事件, 甚至知道如何答复客户。过去大部分CPU是单芯的。而核就像生活在同一个CPU中的另一个芯片。在现代硬件中,你通常拥有多核CPU,它为你的手机和笔记本提供更多算力。
GPU

图像处理单元(GPU) 是计算机的另一个部件。和CPU不同的是,GPU擅长处理简单任务, 不过它可以同时跨越多核。顾名思义, 它最初是为处理图像而开发。这就是为何在图形上下文中, "使用GPU" 或 "GPU支持" 往往与快速渲染和平滑交互相关联。现如今, 随着GPU加速运算的发展,越来越多仅在GPU上完成的运算成为可能。
当你在电脑或手机上启动程序时, CPU和GPU都为程序提供算力。通常程序使用操作系统提供的机制在CPU和GPU中运行。

在进程和线程中执行程序

在深入研究浏览器架构之前要掌握的概念是进程和线程。进程可以看作是应用程序的执行程序。而线程存在于进程内部执行其进程程序任意部分。
继续阅读
如下两个结构体 A B ,他们的实例的大小相同吗?
|
struct A{ char c_1; int i; char c2; }; struct B{ char c_1; char c_2; int i; }; |
两个 char
共占两个字节, 一个 int
占4个字节, 所以两个结构体都是 6个字节。但事实并非如此,在大部分计算机中,他们都不会只占 6 个字节。这涉及到 内存对齐
需知,计算机访问内存的方式,并不是一个字节一个字节访问的,而是以字长(word size)
为单位来进行访问的。32位计算机的字长为 4 字节, 64位计算机的字节为 8 字节。而所谓内存对齐, 就是将变量调整至字长的倍数的位置存放,以方便计算机访问
。
上面两个结构体在内存中的布局最有可能是这个样子的:(这里说 最有可能 是因为具体的布局要视计算机而定)

我们使用一段代码来验证:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
|
void showmem(unsigned char* p, uint32_t sz){ int len = sizeof(int*); cout<<"\t"; for(int i = 0; i<len; i++){ cout<<i<<"\t"; } cout<<endl; for(int i = 0; i<=len; i++){ cout<<"-------|"; } cout<<endl; unsigned char* t = p; uint32_t i = 0; char msg[8]; while(i < sz){ sprintf(msg, "%02d", i); cout<<msg<<"|\t"; for(int j = 0; j< len && (i+j)<sz; j++){ sprintf(msg, "%04d", *(uint8_t*)(t+j)); cout<<msg<<"\t"; } cout<<endl; i += len; t += len; } } //// //... A a{'a', 2, 'b'}; B b{'a', 'b', 2}; cout<<"A:"<<endl; showmem((unsigned char*)&a, sizeof(a)); cout<<endl; cout<<"B:"<<endl; showmem((unsigned char*)&b, sizeof(b)); cout<<endl; |
结果如下:
|
A: 0 1 2 3 4 5 6 7 -------|-------|-------|-------|-------|-------|-------|-------|-------| 00| 0097 0000 0000 0000 0002 0000 0000 0000 08| 0098 0000 0000 0000 B: 0 1 2 3 4 5 6 7 -------|-------|-------|-------|-------|-------|-------|-------|-------| 00| 0097 0098 0000 0000 0002 0000 0000 0000 |
在 A
中,c_1 i
共占一个字,c_2
占一个字。这种将多个变量放入一个字中的动作,叫作 packing
。 而组成一个字时,补充不足的部分,叫 padding
。
如何知道某种类型的变量要对齐到哪个位置呢?它取决于一个类型的 alignment
如 int 类型的alignment 为4, 那么int 类型变量的地址必定为 4 的倍数。
继续阅读
在前一部分(从单例模式谈起(一))我们讨论单例模式时,谈到了 Double-Checked Locking
问题。我们讲, volatile
关键字并不能解决Double-Checked Locking 问题。这一节我们讨论与此相关的问题。
现代编译器为了提高程序的效率而对代码做了很多优化。这些优化大部分是有益的,但是也为多线程编程带来问题。
寄存器缓存
我们来看如下代码:
|
x = 0; Thread1 { lock(); x++; unlock(); } Thread2 { lock(); x++; unlock(); } |
x++
的值有锁保护,所以它是独占的,x++ 的行为不会被并发破坏。那么 x 的值必然为 2。 然而现实中并非一定如此:编译器有可能为了提高 x 的访问速度而将 x 放入线程的某个寄存器中,而线程的寄存器是线程私有的。 如果 Thread1 先获得了锁,那么程序的执行过程有可能是这样的:
- [Thread1] 读取 x 到寄存器 R1. (R1 = x = 0)
- [Thread1] R1++. unlock. 由于之后可能还会访问 x, 所以 Thread1 不将 R1 写回 x . (x = 0)
- [Thread2] 读取 x 到寄存器 R2 (R2 = x = 0)
- [Thread2] R2++
- [Thread2] 将 R2 写回 x (x = R2 =1)
- [Thread1] 在某个时候将 R1 写回 x (x = R1 = 1)
由此可见,即使正确的加了锁,也不能保证多线程安全。
继续阅读