原文连接css
Author: haraken@
Last update: 2018 Aug 14
Status: PUBLIC
译: LeoYhtml
对于刚接触 Blink
的开发者来讲, Blink
相关的工做并不简单。由于实现一个高效快速的渲染引擎,须要了解大量与 Blink
相关的概念和代码约定。这对于经验丰富的 Blink
开发者来讲也并不简单,由于 Blink
项目很庞大,而且对于性能、内存和安全性很敏感。前端
本文的目标是提供一个关于 Blink
工做原理的概览,但愿可以帮助开发者快速熟悉 Blink
的架构。node
Blink
架构细节和代码风格的详细教程,而是关于 Blink
基本原理的简单介绍。这部分原理在短时间内不会有大的改变,另外提供了一些深刻了解这些部分的相关资源。ServiceWorkers
, editing
等),而是介绍了代码中普遍使用的一些基本的功能(好比内存管理, V8 APIs
等)访问 Chromium wiki page 来获取更多的关于 Blink
开发的信息c++
Blink
作了什么 Blink
是一个Web平台的渲染引擎。粗略地说,在一个浏览器tab页中与内容渲染相关的全部事情都是由 Blink
实现的。git
实现Web平台的规格(好比, HTML标准规格),包括 DOM
, CSS
和 Web IDL
(Web浏览器编程接口描述)github
嵌入 V8
和运行 Javascript
web
从底层的网络堆栈请求资源数据库
构建 DOM tree
编程
计算样式和布局
嵌入 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
调用这些独立服务来或者与浏览器进程交互。
了解更多:
在一个渲染进程中建立了多少线程?
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. mdBlink
的初始化和终止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/
的目录以下,查阅这个文档了解更多。
Blink
底层功能,好比地理位置 geometry
和图形 graphics
相关的库DOM
相关的功能。modules/主要实现了一些浏览器自有的功能,好比 webaudio
, indexeddb
。bindings/core/
是 core/
的一部分, bindings/modules/
是 modules/
的一部分。频繁调用 V8 APIs
的文件都放在 bindings/{core,modules}
里面core/
和 modules/
的顶层工具库(好比devtools的前端部分 devtools front-end
)各部分代码的依赖关系以下:
Chromium
=> controller/
=> modules/
和 bindings/modules/
=> core/
和 bindings/core/
=> platform/
=> 底层原语 好比 //base
, //v8
和 //cc
提供给 //third_party/blink/
底层原语在 Blink
项目中被当心翼翼地精心维护着
了解更多:
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
上的堆内存,能够用 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. mdOilpan
: platform/heap/BlinkGCAPIReference. 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
的高效调度是十分重要的。
了解更多:
Page
, Frame
, Document
, DOMWindow
etcPage
, 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
被重用了)。
了解更多:
Site Isolation 网站隔离增长了安全性的同时也增长了复杂性。:) Site Isolation
的设想是为每个网站建立一个渲染进程 。
(A site is a page’s registrable domain + 1 label, and its URL scheme. For example, mail.example.com and chat.example.com are in the same site, but noodles.com and 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
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. } }; 复制代码
当 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()
。
了解更多:
当你写和 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/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@提问。
了解更多:
每一个 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.
了解更多:
一个HTML文件从传递到 Blink
再到屏幕上显示的像素之间有一段很长的历程。渲染管道的架构以下:
Life of A Pixel里面介绍了渲染管道的每个阶段。
了解更多:
有问题能够到 blink-dev@chromium.org 和 platform-architecture-dev@chromium 提问。