Chromium内核原理之blink内核工作解密
《Chromium内核原理之blink内核工作解密》
《Chromium内核原理之多进程架构》
《Chromium内核原理之进程间通信(IPC)》
《Chromium内核原理之网络栈》
《Chromium内核原理之网络栈HTTP Cache》
《Chromium内核原理之Preconnect》
《Chromium内核原理之Prerender》
《Chromium内核原理之cronet独立化》
概要
1.Blink做了什么
2.进程/线程架构2.1 进程
2.2 线程
2.3 Blink初始化3.目录结构
3.1 内容公共API和Blink公共API
3.2 目录结构和依赖项
3.3 WTF4.内存管理
5.任务调度
6.Page, Frame, Document, DOMWindow等6.1 概念
6.2 Out-of-Process iframes (OOPIF)
6.3 分离的 Frame/ Document7.Web IDL绑定
8.V8和Blink8.1 Isolate, Context, World
8.2 V8 API
8.3 V8 wrappers9.渲染管道
概要:
在Blink上工作并不容易。对于新的Blink开发人员来说并不容易,因为为了实现非常快速的渲染引擎,已经引入了许多特定于Blink的概念和编码约定。即使是经验丰富的Blink开发人员也不容易,因为Blink非常庞大,对性能,内存和安全性极为敏感。
1.Blink做了什么
Blink是Web平台的渲染引擎。粗略地说,Blink实现了在浏览器选项卡中呈现内容的所有内容:
- 实现Web平台的规范(例如,HTML标准),包括DOM,CSS和Web IDL
- 嵌入V8并运行JavaScript
- 从底层网络堆栈请求资源
- 构建DOM树
- 计算样式和布局
- 嵌入Chrome Compositor并绘制图形
Blink通过内容公共API嵌入许多客户,如Chromium,Android WebView和Opera。
blink_content_arch.jpg
从代码库的角度来看,“Blink”通常表示// third_party / blink /。从项目角度来看,“Blink”通常表示实现Web平台功能的项目。实现Web平台功能的代码跨度为// third_party / blink /,// content / renderer /,// content / browser /和其他位置。
2.进程/线程架构
2.1 进程
Chromium具有多进程架构。 Chromium有一个浏览器进程和N个沙盒渲染器进程。 Blink在渲染器进程中运行。
创建了多少个渲染器进程?出于安全原因,在跨站点文档之间隔离内存地址区域很重要(这称为站点隔离)。从概念上讲,每个渲染器进程应该专用于最多一个站点。然而,实际上,当用户打开太多选项卡或设备没有足够的RAM时,将每个渲染器进程限制为单个站点有时太重了。然后,渲染器进程可以由从不同站点加载的多个iframe或选项卡共享。这意味着一个选项卡中的iframe可能由不同的渲染器进程托管,并且不同选项卡中的iframe可能由同一渲染器进程托管。渲染器进程,iframe和制表符之间没有1:1映射。
假定渲染器进程在沙箱中运行,则Blink需要请求浏览器进程分派系统调用(例如,文件访问,播放音频)和访问用户简档数据(例如,cookie,密码)。这个浏览器渲染器进程通信由Mojo实现。 (注意:过去我们使用的是Chromium IPC,但仍有一些地方正在使用它。但是,它已被弃用,并在引擎盖下使用Mojo。)在Chromium方面,Servicification正在进行并将浏览器进程抽象为一组“服务。从Blink的角度来看,Blink可以使用Mojo与服务和浏览器进程进行交互。
blink_process.jpg
2.2 线程
在渲染器进程中创建了多少个线程?
Blink有一个主线程,N个工作线程和几个内部线程。
几乎所有重要的事情都发生在主线程上。所有JavaScript(工作者除外),DOM,CSS,样式和布局计算都在主线程上运行。假设大多数是单线程架构,Blink经过高度优化以最大化主线程的性能。
Blink可能会创建多个工作线程来运行Web Workers,ServiceWorker和Worklet。
Blink和V8可能会创建几个内部线程来处理webaudio,数据库,GC等。
对于跨线程通信,您必须使用PostTask API使用消息传递。不鼓励共享内存编程,除非出于性能原因需要使用它的几个地方。这就是为什么你在Blink代码库中看不到很多MutexLock的原因。
thread_arc.jpg
2.3 Blink初始化
Blink由BlinkInitializer :: Initialize()
初始化。必须在执行任何Blink代码之前调用此方法。
另一方面,Blink永远不会完成;即渲染器过程被强制退出而不被清理。一个原因是表现。另一个原因是,通常很难以优雅的顺序清理渲染器过程中的所有内容(并且不值得付出努力)。
3.目录结构
3.1 内容公共API和Blink公共API
内容公共API是API层,它使嵌入器能够嵌入呈现引擎。必须仔细维护内容公共API,因为它们暴露给嵌入器。
Blink公共API是API层,它公开了从// third_party / blink /到Chromium的功能。此API层只是从WebKit继承的历史工件。在WebKit时代,Chromium和Safari共享了WebKit的实现,因此需要API层来将功能从WebKit公开到Chromium和Safari。现在Chromium是// third_party / blink /的唯一嵌入器,API层没有意义。我们通过将网络平台代码从Chromium移动到Blink(该项目称为Onion Soup)来积极减少Blink公共API的数量。
public_api.jpg
3.2 目录结构和依赖项
/ third_party / blink /具有以下目录。有关这些目录的更详细定义,请参阅此文档:https://chromium.googlesource.com/chromium/src/+/master/third_party/blink/renderer/README.md
- platform/
Blink的低级功能集合,由单片内核/。例如,几何和图形工具。 - core/ 和 modules/
实现了规范中定义的所有Web平台功能。核心/实现与DOM紧密结合的功能。模块/实现更多自包含功能。例如webaudio,indexeddb。 - bindings/core/ 和 bindings/modules/
概念上绑定/ core /是core /的一部分,而bindings / modules /是modules /的一部分。大量使用V8 API的文件放在绑定/ {core,modules}中。 - controller/
一组使用核心/和模块/的高级库。例如,devtools前端。
依赖关系按以下顺序:
Chromium => controller/ => modules/ and bindings/modules/ => core/ and bindings/core/ => platform/ => low-level primitives such as //base, //v8 and //cc
Blink仔细维护暴露于// third_party / blink /的低级原语列表。
3.3 WTF
WTF是一个“Blink特定的基础”库,位于platform / wtf /。我们试图尽可能地在Chromium和Blink之间统一编码原语,因此WTF应该很小。这个库是必需的,因为有许多类型,容器和宏真正需要针对Blink的工作负载和Oilpan(Blink GC)进行优化。如果在WTF中定义了类型,则Blink必须使用WTF类型而不是// base或std库中定义的类型。最流行的是矢量,哈希集,哈希映射和字符串。 Blink应该使用WTF :: Vector,WTF :: HashSet,WTF :: HashMap,WTF :: String和WTF :: AtomicString而不是std :: vector,std :: * set,std :: * map和std :: string 。
4.内存管理
就Blink而言,您需要关心三个内存分配器:
- PartitionAlloc
- Oilpan
- malloc/free 或者 new/delete
您可以使用USING_FAST_MALLOC()在PartitionAlloc的堆上分配对象:
class SomeObject {
USING_FAST_MALLOC(SomeObject);
static std::unique_ptr<SomeObject> Create() {
return std::make_unique<SomeObject>(); // Allocated on PartitionAlloc's heap.
}
};
PartitionAlloc分配的对象的生命周期应由scoped_refptr <>或std :: unique_ptr <>管理。强烈建议不要手动管理生命周期。 Blink禁止手动删除。
您可以使用GarbageCollected在Oilpan的堆上分配一个对象:
class SomeObject : public GarbageCollected<SomeObject> {
static SomeObject* Create() {
return new SomeObject; // Allocated on Oilpan's heap.
}
};
Oilpan分配的对象的生命周期由垃圾收集自动管理。你必须使用特殊的指针(例如,Member <>,Persistent <>)来保存Oilpan堆上的对象。请参阅此API参考以熟悉有关Oilpan的编程限制。最重要的限制是不允许在Oilpan的对象的析构函数中触摸任何其他Oilpan的对象(因为无法保证销毁顺序)。
如果既不使用USING_FAST_MALLOC()也不使用GarbageCollected,则在系统malloc的堆上分配对象。在Blink中强烈建议不要这样做。所有Blink对象应由PartitionAlloc或Oilpan分配,如下所示:
- 默认使用 Oilpan
- 仅当1)对象的生命周期非常清楚且std :: unique_ptr <>或scoped_refptr <>足够时才使用PartitionAlloc,2)在Oilpan上分配对象引入了很多复杂性或者3)在Oilpan上分配对象引入了垃圾收集运行时有很多不必要的压力。
无论您使用的是PartitionAlloc还是Oilpan,您都必须非常小心,不要创建悬空指针(注意:强烈建议不要使用原始指针)或内存泄漏。
5.任务调度
为了提高渲染引擎的响应能力,Blink中的任务应尽可能异步执行。不鼓励同步IPC / Mojo和可能需要几毫秒的任何其他操作。
渲染器进程中的所有任务都应该使用适当的任务类型发布到Blink Scheduler,如下所示:
// Post a task to frame's scheduler with a task type of kNetworking
frame->GetTaskRunner(TaskType::kNetworking)->PostTask(..., WTF::Bind(&Function));
Blink Scheduler维护多个任务队列,并巧妙地确定任务的优先级,以最大限度地提高用户感知的性能。指定正确的任务类型以使Blink Scheduler正确而巧妙地安排任务非常重要。
6.Page, Frame, Document, DOMWindow等
6.1 概念
Page,Frame,Document,ExecutionContext和DOMWindow是以下概念:
- Page:页面对应于选项卡的概念(如果未启用下面解释的OOPIF)。每个渲染器进程可能包含多个选项卡。
- Frame:帧对应于帧的概念(主帧或iframe)。每个页面可能包含一个或多个以树形层次结构排列的框架。
- DOMWindow对应于JavaScript中的窗口对象。每个Frame都有一个DOMWindow。
- Document对应于JavaScript中的window.document对象。每个框架都有一个文档。
- ExecutionContext是一个抽象Document(用于主线程)和WorkerGlobalScope(用于工作线程)的概念。
渲染器过程:Page = 1:N。
Page : Frame = 1 : M.
Frame : DOMWindow : Document (or ExecutionContext) = 1:1:1在任何时间点,但映射可能会随时间而变化。例如,请考虑以下代码:
iframe.contentWindow.location.href = "https://example.com";
在这种情况下,将为https://example.com创建新的DOMWindow和新文档。但是,帧可以重复使用。
6.2 Out-of-Process iframes (OOPIF)
站点隔离使事情更安全,但更复杂。 :)站点隔离的想法是为每个站点创建一个渲染器进程。 (网站是网页的可注册域名+ 1标签及其网址方案。例如,https://mail.example.com和https://chat.example.com位于同一网站,但https:// noodles.com和https://pumpkins.com不是。)如果一个页面包含一个跨站点iframe,那么该页面可能由两个渲染器进程托管。请考虑以下页面:
<!-- https://example.com -->
<body>
<iframe src="https://example2.com"></iframe>
</body>
主框架和<iframe>可以由不同的渲染器进程托管。渲染器进程的本地帧由LocalFrame表示,而渲染器进程不是本地的帧由RemoteFrame表示。
从主框架的角度来看,主框架是LocalFrame,<iframe>是RemoteFrame。从<iframe>的角度来看,主框架是RemoteFrame,<iframe>是LocalFrame。
LocalFrame和RemoteFrame(可能存在于不同的渲染器进程中)之间的通信通过浏览器进程处理。
6.3 分离的 Frame/ Document
Frame/ Document可能处于分离状态。考虑以下情况:
doc = iframe.contentDocument;
iframe.remove(); // The iframe is detached from the DOM tree.
doc.createElement("div"); // But you still can run scripts on the detached frame.
棘手的事实是您仍然可以在分离的帧上运行脚本或DOM操作。由于帧已经分离,大多数DOM操作都会失败并抛出错误。不幸的是,分离帧上的行为在浏览器之间并不真正可互操作,也没有在规范中明确定义。基本上期望JavaScript应该继续运行,但是大多数DOM操作都应该失败并带有一些适当的例外,例如:
void someDOMOperation(...) {
if (!script_state_->ContextIsValid()) { // The frame is already detached
…; // Set an exception etc
return;
}
}
这意味着在常见情况下,当框架分离时,Blink需要进行一系列清理操作。您可以通过继承ContextLifecycleObserver来执行此操作,如下所示:
class SomeObject : public GarbageCollected<SomeObject>, public ContextLifecycleObserver {
void ContextDestroyed() override {
// Do clean-up operations here.
}
~SomeObject() {
// It's not a good idea to do clean-up operations here because it's too late to do them. Also a destructor is not allowed to touch any other objects on Oilpan's heap.
}
};
7.Web IDL绑定
当JavaScript访问node.firstChild时,将调用node.h中的Node :: firstChild()。它是如何工作的?我们来看看node.firstChild是如何工作的。
首先,您需要根据规范定义IDL文件:
// node.idl
interface Node : EventTarget {
[...] readonly attribute Node? firstChild;
};
Web IDL的语法在Web IDL规范中定义。 [...]称为IDL扩展属性。一些IDL扩展属性在Web IDL规范中定义,而其他属性是特定于Blink的IDL扩展属性。除了特定于Blink的IDL扩展属性外,IDL文件应以特定的方式编写(即只需从规范中复制和粘贴)。
其次,您需要为Node定义C ++类并为firstChild实现C ++ getter:
class EventTarget : public ScriptWrappable { // All classes exposed to JavaScript must inherit from ScriptWrappable.
...;
};
class Node : public EventTarget {
DEFINE_WRAPPERTYPEINFO(); // All classes that have IDL files must have this macro.
Node* firstChild() const { return first_child_; }
};
在一般情况下,就是这样。构建node.idl时,IDL编译器会自动为Node接口和Node.firstChild生成Blink-V8绑定。自动生成的绑定在// src / out / {Debug,Release} / gen / third_party / blink / renderer / bindings / core / v8 / v8_node.h中生成。当JavaScript调用node.firstChild时,V8在v8_node.h中调用V8Node :: firstChildAttributeGetterCallback(),然后它调用您在上面定义的Node :: firstChild()。
8.V8和Blink
8.1 Isolate, Context, World
当您编写涉及V8 API的代码时,了解Isolate,Context和World的概念非常重要。它们分别由代码库中的v8 :: Isolate,v8 :: Context和DOMWrapperWorld表示。
Isolate对应于物理线程。 Isolate : physical thread in Blink = 1 : 1。主线程有自己的隔离。工作线程有自己的隔离。
Context对应于全局对象(在Frame的情况下,它是Frame的窗口对象)。由于每个帧都有自己的窗口对象,因此渲染器进程中有多个上下文。当您调用V8 API时,您必须确保您处于正确的上下文中。否则,v8 :: Isolate :: GetCurrentContext()将返回错误的上下文,在最坏的情况下,它将最终泄漏对象并导致安全问题。
World是支持Chrome扩展程序内容脚本的概念。世界与Web标准中的任何内容都不对应。内容脚本希望与网页共享DOM,但出于安全原因,必须将内容脚本的JavaScript对象与网页的JavaScript堆隔离。 (另外一个内容脚本的JavaScript堆必须与另一个内容脚本的JavaScript堆隔离。)为了实现隔离,主线程为网页创建一个主要世界,为每个内容脚本创建一个隔离的世界。主要世界和孤立的世界可以访问相同的C ++ DOM对象,但它们的JavaScript对象是隔离的。通过为一个C ++ DOM对象创建多个V8包装器来实现这种隔离。即每个世界一个V8包装器。
Context,World和Frame之间有什么关系?
想象一下,主线上有N个世界(一个主要世界+(N - 1)个孤立的世界)。然后一个Frame应该有N个窗口对象,每个窗口对象用于一个世界。上下文是对应于窗口对象的概念。这意味着当我们有M帧和N个世界时,我们有M * N上下文(但是上下文是懒洋洋地创建的)。
对于worker,只有一个世界和一个全球对象。因此,只有一个上下文。
同样,当您使用V8 API时,您应该非常小心使用正确的上下文。否则,您最终会在孤立的世界之间泄漏JavaScript对象并导致安全灾难(例如,A.com的扩展可以操纵来自B.com的扩展)。
8.2 V8 API
在//v8/include/v8.h中定义了很多V8 API。由于V8 API是低级的并且难以正确使用,因此platform / bindings /提供了一堆包装V8 API的辅助类。您应该考虑尽可能多地使用帮助程序类。如果您的代码必须大量使用V8 API,那么这些文件应该放在bindings / {core,modules}中。
V8使用句柄指向V8对象。最常见的句柄是v8 :: Local <>,用于指向机器堆栈中的V8对象。在机器堆栈上分配v8 :: HandleScope后,必须使用v8 :: Local <>。不应在机器堆栈外使用v8 :: Local <>:
void function() {
v8::HandleScope scope;
v8::Local<v8::Object> object = ...; // This is correct.
}
class SomeObject : public GarbageCollected<SomeObject> {
v8::Local<v8::Object> object_; // This is wrong.
};
8.3 V8 wrappers
每个C ++ DOM对象(例如,Node)具有其对应的V8包装器。准确地说,每个C ++ DOM对象每个世界都有相应的V8包装器。
V8包装器对其对应的C ++ DOM对象具有强引用。但是,C ++ DOM对象只有对V8包装器的弱引用。因此,如果您希望将V8包装器保持活动一段时间,则必须明确地执行此操作。否则,V8包装器将过早收集,V8包装器上的JS属性将丢失。
div = document.getElementbyId("div");
child = div.firstChild;
child.foo = "bar";
child = null;
gc(); // If we don't do anything, the V8 wrapper of |firstChild| is collected by the GC.
assert(div.firstChild.foo === "bar"); //...and this will fail.
如果我们不做任何事情,那么孩子会被GC收集,因此child.foo会丢失。为了使div.firstChild的V8包装器保持活动状态,我们必须添加一种机制,“只要div所属的DOM树可以从V8到达,就可以使div.firstChild的V8包装器保持活动状态”。
9.渲染管道
从HTML文件传送到Blink到屏幕上显示像素的过程很长。渲染管道的架构如下。
blink_render_process.jpg
-
Overview: Life of a pixel
-
DOM: core/dom/README.md
-
Style: core/css/README.md
-
Layout: core/layout/README.md
-
Paint: core/paint/README.md
-
Compositor thread: Chromium graphics**