Author: haraken@ Last update: 2018 Aug 14 Status: PUBLIC 译: LeoY
对于刚接触 Blink
的开发者来说, Blink
相关的工作并不简单。因为实现一个高效快速的渲染引擎,需要了解大量与 Blink
相关的概念和代码约定。这对于经验丰富的 Blink
开发者来说也并不简单,因为 Blink
项目很庞大,并且对于性能、内存和安全性很敏感。
本文的目标是提供一个关于 Blink
工作原理的概览,希望能够帮助开发者快速熟悉 Blink
的架构。
- 本文不是一个关于
Blink
架构细节和代码风格的详细教程,而是关于Blink
基本原理的简单介绍。这部分原理在短期内不会有大的改变,另外提供了一些深入了解这些部分的相关资源。 - 本文不会介绍具体的功能(比如
ServiceWorkers
,editing
等),而是介绍了代码中广泛使用的一些基本的功能(比如内存管理,V8 APIs
等)
访问 Chromium wiki page 来获取更多的关于 Blink
开发的信息
Blink
做了什么- 进程/线程 架构
- 目录架构
- 内存管理
- 任务调度
Page
,Frame
,Document
,DOMWindow
etc- Web IDL bindings: Web IDL绑定
- V8 和 Blink
- 渲染管道 Rendering pipeline
- Questions?
Blink
做了什么
Blink
是一个Web平台的渲染引擎。粗略地说,在一个浏览器tab页中与内容渲染相关的所有事情都是由 Blink
实现的。
-
实现Web平台的规格(比如, HTML标准规格),包括
DOM
,CSS
和Web IDL
(Web浏览器编程接口描述) -
嵌入
V8
和运行Javascript
-
从底层的网络堆栈请求资源
-
构建
DOM tree
-
计算样式和布局
-
嵌入
Chrome Compositor
和图形渲染绘制- what is Chrome Compositor?
cc is responsible for taking painted inputs from its embedder, figuring out where and if they appear on screen, rasterizing and decoding and animating images from the painted input into gpu textures, and finally forwarding those textures on to the display compositor in the form of a compositor frame. cc also handles input forwarded from the browser process to handle pinch and scroll gestures responsively without involving Blink.
在很多地方都能见到 Blink
的身影,比如 Chromium
, Android WebView
以及通过 content public APIs 内嵌 Blink
的 Opera
浏览器。
从代码库的角度来看, Blink
对应 //third_party/blink/
。
从项目本身来看, Blink
实现了Web平台的功能,这些代码在主要在 //third_party/blink/
, //content/renderer/
, //content/browser/
目录中。
进程/线程 架构
进程
Chromium
是一个 多进程架构 multi-process architecture 的浏览器引擎 。 Chromium
运行时会创建一个浏览器进程和N个在沙盒中运行的渲染进程。 Blink
则是在渲染进程中运行的。
创建多少渲染进程?一般来说,一个 site
会独占一个渲染进程,而当用户开太多tabs页面内存不足时,多个 site
可能会共享一个渲染进程。
出于安全性的考虑,跨站文档(cross-site documents)的内存地址会被隔离开来(这被称为Site Isolation)。理想情况下,每个渲染进程是每个网站专用的,然而当用户打开太多标签页或者机器内存不够时,这种限制就很麻烦。所以实际上,多个页面或者不同网站的多个
iframe
可能会共享同一个渲染。这意味着一个tab页中的多个iframe
可能是不同的渲染进程渲染的,不同的tab页中的iframe
也有有可能是同一个渲染进程渲染的。所以渲染进程,iframe 和 tab 三者之间不是(1:1)一对一的映射关系
由于渲染进程是运行在沙盒中的,所以 Blink
需要向浏览器进程发起系统调用(比如文件访问,音频播放)和(用户配置)数据的获取(比如 Cookie
,密码)。浏览器进程和渲染进程之间通过 Mojo 实现通信。(Note: 以前是通过 Chromium IPC 实现,现在还有部分代码仍在使用,但是会逐渐弃用) Chromium
中的 Servicification
将浏览器进程封装出了一些独立的服务。 Blink
可以直接使用 Mojo
调用这些独立服务来或者与浏览器进程交互。
了解更多:
- 多进程架构 Multi-process Architecture
- 在Blink中使用Mojo进行开发 Mojo programming in Blink: platform/mojo/MojoProgrammingInBlink. md
线程
在一个渲染进程中创建了多少线程?
Blink
中会有一个主线程,N个工作线程和三两个内部线程。
几乎所有重要的事情都发生在主线程中。 Javascript
(不包括 service worker
), DOM
, CSS
,样式和布局计算都在主线程中运行。
Blink
通过许多优化来最大化主线程的性能,模拟了一个近乎单线程的架构。
Blink
可能会创建多个工作线程来运行 Web Workers
, ServiceWorker
以及 Worklet
。
Blink
和 V8
可能会创建三两个内部进程来处理 音频 webaudio
, 数据库 database
, 内存回收 GC
等。
线程之间的通信,需要使用 PostTask APIs
来传递。
出于性能的考虑,除了几个特别的地方,共享内存编程是不推荐的,所以在 Blink
中源码中也很少使用互斥锁这种东西。
了解更多:
Blink
中的线程: platform/wtf/ThreadProgrammingInBlink. md- Workers: core/workers/README. md
Blink
的初始化和终止
Blink
通过 BlinkInitializer::Initialize()
来初始化。这个方法必须在执行 Blink
代码前调用。
Blink
没有终止化的状态,原因是渲染进程是被强制退出的,而不是被清理回收的。原因之一是出于性能的考虑(强制退出不需要做额外的操作)。另一个原因是渲染进程在正常退出的情况下,一般很难把所以东西都清理回收掉。(并且这样做的代价高于带来的效益)
目录架构
Content public APIs
和 Blink public APIs
Content public APIs
是用嵌入渲染引擎的API层。 Content public APIs
必须小心维护,因为它们是提供给(想内嵌 Blink
引擎的)嵌入器的。
Blink public APIs
是为 Chromium
提供 //third_party/blink/
中的 Blink
功能的API层。 Blink public APIs
继承自 Webkit APIs
。在 Webkit
时代,由于 Chromium
和 Safari
会共享 Webkit
的实现,所以当时这个API层既要顾及 Chromium
也要顾及 Safari
。而现在 Blink
内核的功能只需要提供给 Chromium
,旧的API层有些API就不需要了。所以我们将 Chromium
中与平台相关的代码迁移到 Blink
中来减少 Blink public APIs
的数量(这个项目被叫作 Onion Soup
)
目录架构和依赖
//third_party/blink/
的目录如下,查阅这个文档了解更多。
- platform/
- 一组从core/里面分解出来的
Blink
底层功能,比如地理位置geometry
和图形graphics
相关的库
- 一组从core/里面分解出来的
- core/ 和 modules/
- 实现Web平台规格文件的所有功能。core/主要是实现
DOM
相关的功能。modules/主要实现了一些浏览器自有的功能,比如webaudio
,indexeddb
。
- 实现Web平台规格文件的所有功能。core/主要是实现
- bindings/core/ 和 bindings/modules/
- 从命名就可以猜到,
bindings/core/
是core/
的一部分,bindings/modules/
是modules/
的一部分。频繁调用V8 APIs
的文件都放在bindings/{core,modules}
里面
- 从命名就可以猜到,
- controller/
- 一些调用
core/
和modules/
的顶层工具库(比如devtools的前端部分devtools front-end
)
- 一些调用
各部分代码的依赖关系如下:
Chromium
=>controller/
=>modules/
和bindings/modules/
=>core/
和bindings/core/
=>platform/
=> 底层原语 比如//base
,//v8
和//cc
提供给 //third_party/blink/
底层原语在 Blink
项目中被小心翼翼地精心维护着
了解更多:
-
目录架构和依赖:blink/renderer/README. md
WTF
WTF
是 Blink
特有的工具库,位于 platform/wtf/
。我们尽可能统一 Chromium
和 Blink
的代码,所以 WTF
的体积会很小。 WTF
这个工具库之所以存在,是因为对于 Blink
的工作负载和 Oilpan
(即 Blink GC
) 中有大量的类型( types
),容器( containers
)和宏( macros
)需要做性能优化。如果类型在 WTF
中有相应的定义,在 Blink
中就需要使用 WTF
的类型而不是定义在 //base
或者 std libraries
中的类型。
使用的最多的类型是 vectors
, hashsets
, hashmaps
和 strings
。相应的在 Blink
中应该使用 WTF::Vector
, WTF::HashSet
, WTF::HashMap
, WTF::String
和 WTF::AtomicString
而不是 std::vector
, std::*set,
std::*map
和 std::string
。
了解更多:
- 如何使用
WTF
: platform/wtf/README. md
内存管理
你需要关注三个与 Blink
相关的内存分配器。
- PartitionAlloc
- Oilpan (即Blink GC)
- malloc/free or new/delete (禁止使用c++原生的)
给一个对象分配 PartitionAlloc
上的堆内存,可以用 USING_FAST_MALLOC()
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
是不允许的。
给一个对象分配 Oilpan
上的堆内存,你可以使用 GarbageCollected
。
class SomeObject : public GarbageCollected<SomeObject> {
static SomeObject* Create() {
return new SomeObject; // Allocated on Oilpan's heap.
}
};
Oilpan
堆内存中的对象生命周期是由 garbage collection
自动管理的。你需要使用特殊的指针(比如 Member<>
, Persistent<>
)来存 Oilpan
堆内存中的对象。参考这个API手册this API reference来熟悉在 Oilpan
上开发的一些限制。最重要的一个限制就是在一个 Oilpan
对象的解构函数中不允许处理任何其他的 Oilpan
对象。(原因是解构的顺序是没有保证的)
如果你既没有使用 USING_FAST_MALLOC()
也没有使用 GarbageCollected
,那么对象就被分配在系统堆内存中。这在 Blink
中是极其不推荐的。所有的 Blink
对象都应该按如下规则分配在 PartitionAlloc
或者 Oilpan
的堆内存中。
- 默认使用
Oilpan
- 仅在三种情况下使用
PartitionAlloc
- 对象的生命周期非常清晰,只用
std::unique_ptr<>
和scoped_refptr<>
就可以满足需求 - 当使用
Oilpan
分配内存给当前情况增加很多的复杂性时 - 当使用
Oilpan
分配内存给当前情况的垃圾回收机制garbage collection runtime
带来了大量不必要的(性能)压力时
- 对象的生命周期非常清晰,只用
不管使用 PartitionAlloc
还是 Oilpan
来分配内存,都需要极其小心,以免创建出悬空指针( Dangling pointer
)甚至内存泄露(Note: 裸指针也是极其其不推荐的)
了解更多:
- 如何使用
PartitionAlloc
: platform/wtf/allocator/Allocator. md - 如何使用
Oilpan
: platform/heap/BlinkGCAPIReference. md - Oilpan GC 设计: platform/heap/BlinkGCDesign. md
任务调度
为了提升渲染引擎的响应速度,在 Blink
中的任务都应该尽可能的异步执行。同步的 IPC/Mojo
或者其他可能耗费数毫秒的操作都是不推荐使用的。(尽管有些操作无法避免,比如执行用户的 JavaScript
,其 JavaScript
代码本身可能会阻塞渲染)。
在渲染进程中的所有任务都会通知 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 Schedule
的高效调度是十分重要的。
了解更多:
- How to post tasks 如何发起任务: platform/scheduler/PostTask. md
Page
, Frame
, Document
, DOMWindow
etc
概念
Page
, Frame
, Document
, ExecutionContext
和 DOMWindow
的含义如下:
Page
对应tab页(如果下文介绍的OOPIF
没有开启的话)。一个渲染进程可能会渲染多个tab页Frame
对应frame
(main frame
或者一个iframe
)。一个Page
包含一个或者多个Frame
并且包含在一个树结构中。DOMWindow
对应Javascript
中的window
对象。每个Frame
有一个DOMWindow
。Document
对应Javascript
中的window.document
对象。每个Frame
有一个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
和 Document
,而 Frame
则可能被重用。(Note: 准确的来说,还存在一些更复杂的情况创建了新的 Document
,而 DOMWindow
和 Frame
被重用了)。
了解更多:
- core/frame/FrameLifecycle. md
Out-of-Process iframes (OOPIF 进程外的iframe)
Site Isolation 网站隔离增加了安全性的同时也增加了复杂性。:) Site Isolation
的设想是为每一个网站创建一个渲染进程 。
(A site is a page’s registrable domain + 1 label, and its URL scheme. For example, https://mail.example.com and https://chat.example.com are in the same site, but https://noodles.com and https://pumpkins.com are not. )
如果 Page
包含跨站的 iframe
, 那么这个 Page
可能被两个渲染进程共同渲染。参考下面这种 Page
:
<!-- https://example.com -->
<body>
<iframe src="https://example2.com"></iframe>
</body>
主 frame
和 iframe
可能运行在不同的渲染进程中。渲染进程本地的 frame
由 LocalFrame
呈现,不属于渲染进程本地的 frame
由 RemoteFrame
呈现。
从主 frame
的角度来看,主 frame
是一个 LocalFrame
而 iframe
是一个 RemoteFrame
。从 iframe
的角度来看,主 frame
是一个 RemoteFrame
,而 iframe
是一个 LocalFrame
。
LocalFrame
和 RemoteFrame
(两者可能存在与不同的渲染进程中)之间的通信是浏览器进程来处理的。
了解更多:
-
Design docs 设计文档: Site isolation design docs
-
How to write code with site isolation: core/frame/SiteIsolation. md
Detached Frame / Document 分离的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.
一个很骚的事实是,在分离的 frame
中你仍能够运行脚本和执行DOM操作。由于 frame
已经被分离,大部分DOM操作会失败并报错。可惜分离的 frame
的表现在不同浏览器中并不一致,在规格文件中也没有非常明确的定义。大体上来说,期望的表现是在 frame
分离后 JavaScript
还是可以正常的执行,但是大多数DOM操作都应该失败并抛出异常,如:
void someDOMOperation(...) {
if (!script_state_->ContextIsValid()) { // The frame is already detached
…; // Set an exception etc
return;
}
}
这意味着 Blink
需要在 frame
被分离时做大量的清除回收操作。这些操作可以通过 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.
}
};
Web IDL bindings: Web IDL绑定
当 JavaScript
访问 node.firstChild
的时候, node.h
中的 Node::firstChild()
即被调用。它是如何工作的呢,一起来看看 node.firstChild
是怎么工作的:
首先,你需要为每一个规格定义一个IDL文件,如:
// node.idl
interface Node : EventTarget {
[...] readonly attribute Node? firstChild;
};
Web IDL的语法定义在 the Web IDL spec 中。 [...]
被称为 IDL extended attributes
。
the Web IDL spec
里面定义了一些 IDL extended attributes
,其他的在 Blink-specific IDL extended attributes中。除了 Blink
特有的 IDL extended attributes
,其他IDL文件都应该按照和规格文件一致的格式来写(意思就是直接从规格文件里面cv)。
接下来,你需要为 Node
节点定义一个 C++ class
类,并用c++实现 firstChild
的 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
, the IDL compiler
会为 Node interface
和 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()
。
了解更多:
- How to add Web IDL bindings: bindings/IDLCompiler. md
- How to use IDL extended attributes: bindings/- IDLExtendedAttributes. md
- Spec: Web IDL spec
V8 和 Blink
Isolate, Context, World
当你写和 V8 APIs
有关的代码时,理解 Isolate, Context, World
这三个概念很重要。它们在代码库中分别是 v8::Isolate
, v8::Context
和 DOMWrapperWorld
。
Isolate
是一个物理上的线程,在 Blink
中 Isolate : physical=1:1
。主线程和工作线程都有其独立的 Isolate
。
Context
是一个全局的对象(以 Frame
来说, Frame
的 Context
是 window
对象)。由于每个 frame
有自己的 window
对象,所以一个渲染进程中会有多个 Context
。当调用 V8 APIs
时,你需要确认你在正确的 Context
中。否则, v8::Isolate::GetCurrentContext()
就会返回一个不正确的 Context
,最坏的情况会造成对象泄露并导致安全问题。
World
支撑 Chrome extensions
脚本的运行。 Worlds
和任何web标准都没有关系。 Chrome extensions
脚本和页面共享DOM,不过处于安全的考虑, Chrome extensions
脚本的 JavaScript
和页面的 JavaScript
堆内存是相互隔离的。(并且 Chrome extensions
脚本之间的 JavaScript
堆内存也是相互隔离的)。主线程通过为页面创建一个 main world
和为每个 Chrome extensions
脚本创建一个 isolated world
来实现隔离。 main world
和 isolated worlds
都可以访问到C++上的DOM对象,但是他们各自的 JavaScript
对象都是隔离的。这种隔离是通过为每个 C++DOM
对象创建多个 V8 wrapper
来实现的。即每个 world
对应一个 V8 wrapper
。
Context
, World
和 Frame
之间有什么联系?
想象一下, 在主线程中存在N个 World
(一个 main world
+(N-1)个 isolated worlds)
)。那么一个 Frame
就有N个 window objects
,每个 window objects
对应一个 world
。而 Context
也是对应 window objects
,这意味着当存在N个 Frame
和N个 Worlds
的时候,有M*N个 Contexts
(不过 Contexts
是懒加载创建的)
对于 worker
而言,只有一个 World
和一个 global object
,所以就只存在一个 Context
此外,当你使用 V8 APIs
的时候,你应该非常注意是否使用了正确的 context
,否则你可能导致在不同的 isolated worlds
间泄露 JavaScript
对象甚至导致灾难般的安全问题。(比如,使得A. com的 Chrome extetion
可以操纵B. com的 Chrome extetion
)
了解更多:
V8 APIs
//v8/include/v8. h. 里面有大量的V8 APIs。由于 V8 APIs
都比较底层,使用起来略显麻烦,所以一般使用platform/bindings/ 提供的一组封装了的 V8 APIs
辅助类来( helper classes
)进行调用。你应该尽量使用 helper classes
。如果你的代码中会重度使用原生 V8 APIs
,这些代码应该放到 bindings/{core,modules}
里面去。
V8使用 handle
来指向 V8 objects
。最常见的 handle
是 v8::Local<>
, v8::Local<>
用于从机器堆栈 machine stack
指向 V8 objects
。 v8::Local<>
必须在 v8::HandleScope
从机器堆栈 machine stack
分配之后才能使用。 v8::Local<>
也不能在 machine stack
之外使用:
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.
};
要从机器堆栈 machine stack
指外指向 V8 objects
,你需要使用 wrapper tracing。然而你需要特别小心地使用,以免创建出循环引用。通常 V8 APIs
都是难用的。如果你不确定你的用法可以上blink-review-bindings@提问。
了解更多:
- How to use V8 APIs and helper classes: platform/bindings/HowToUseV8FromBlink. md
V8 wrappers
每个 C++ DOM
对象(比如,Node节点)都有其对应的 V8 wrapper
。准确的说,每个 world
的每个 C++ DOM
对象都有其对应的 V8 wrapper
。
V8 wrappers
对它相应的 C++ DOM
是强引用关系。而 C++ DOM
对 V8 wrappers
则是弱引用关系。所以如果想要使 V8 wrappers
在一段特定的周期延续,你需要明确地指定。否则 V8 wrappers
可能会被提前回收,导致 V8 wrappers
上的 JS properties
丢失…
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.
如果什么都不做的话, child
就会被 GC
回收,即 child.foo
就不存在了。要保留 div.firstChild
上的 V8 wrapper
的话,我们需要增加一个机制来实现:只要 div
所属的 DOM tree
还可以通过 V8
访问到,就一直保留 div.firstChild
上的 V8 wrapper
。
有两种方式来保留 V8 wrappers
:ActiveScriptWrappable和wrapper tracing.
了解更多:
- How to manage lifetime of V8 wrappers: bindings/core/v8/V8Wrapper. md
- How to use wrapper tracing: platform/bindings/TraceWrapperReference. md
渲染管道 Rendering pipeline
一个HTML文件从传递到 Blink
再到屏幕上显示的像素之间有一段很长的历程。渲染管道的架构如下:
Life of A Pixel里面介绍了渲染管道的每一个阶段。
了解更多:
- 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
Questions?
有问题可以到 [email protected] 和 platform-architecture-dev@chromium 提问。