Chromium 的工程師們寫了兩篇技術文章 How Blink Works 和 How cc Works
,分别介紹了 Chrome 浏覽器核心内部的兩個重要子產品 Blink 和 cc 内部設計和實作的一些細節。對于想要了解 Chrome 核心内部實作的同學,這兩篇文章提供了不錯的入門指引。在征得作者同意後,我将其翻譯成中文,以饋讀者。
文中部分術語覺得難以翻譯成合适的中文的,我會保留原文。對于一些較為複雜的概念會根據自己的了解給出一些額外的解釋。如果有我了解有誤,沒有正确翻譯的地方,也請讀者留言指出。
Blink 的開發并不容易。對于新的 Blink 開發者來說,這種不容易展現在内部的許許多多 Blink 特定的概念和編碼約定,它們是為了實作一個高性能渲染引擎而引入的。對于有經驗的 Blink 開發者來說也是不容易的,因為 Blink 非常龐大,并且對性能,記憶體占用和安全非常敏感。
本文旨在提供一篇指引,從一萬公尺的高空概覽 “Blink 是如何工作” 的全貌,我希望這能夠幫助 Blink 的開發者快速了解 Blink 的整體架構設計:
- 本文并不是一篇完整的教程,去解析 Blink 架構的細節和編碼規則(這些部分也容易因為發生變化而過時)。相反,本文簡明地描述了 Blink 的基礎設計,它們短期内不太容易發生變化,如果讀者需要了解更多,本文包含了其它資料的指引可供進一步閱讀;
- 本文也不對特定的特性進行解析(比如 ServiceWorkers,editing)。相反,本文描述的是基礎特性,它們被其它子產品所廣泛使用(比如記憶體管理,V8 APIs)。
有關 Blink 開發的更多一般資訊,請參閱
Chromium wiki 頁面。
Blink 做些什麼
Blink 是 Web 平台的渲染引擎。粗略地說,Blink 實作了将網頁内容繪制到一個浏覽器标簽内的所有代碼:
- 實作了 Web 平台的規範(也就是 HTML 标準 ),包括 DOM,CSS 和 Web IDL;
- 内嵌 V8 運作 JavaScript;
- 通過底層的網絡棧請求資源;
- 建構 DOM 樹;
- 計算樣式和排版;
- 内嵌 Chrome Compositor 用于繪圖;
Blink 的使用者比如 Chromium,Android WebView 和 Opera 通過
content public APIs内嵌 Blink 并調用。

從代碼結構的角度來看,"Blink" 一般意味着 "//third_party/blink/" 目錄下的代碼。從項目的角度來看,"Blink" 一般意味着實作 Web 平台特性的項目。實作這些 Web 特性的代碼分布在 "//third_party/blink/", "//content/renderer/","//content/browser/" 和其它地方。
[譯注]
- 關于 Embedder 的概念,可以參考我的一篇舊文 - 了解 Embedder,了解 Chromium 的系統層次結構
- “Blink 實作了将網頁内容繪制到一個浏覽器标簽内的所有代碼” 這句話我的了解應該是指廣義的 "Blink",并不僅僅指 "//third_party/blink/"。
程序/線程架構
程序
Chromium 使用
多程序的架構。 Chromium 擁有一個 browser 程序和 N 個沙盒 renderer 程序。Blink 運作在 renderer 程序。
有多少個 renderer 程序會被建立?為了安全的原因,對跨站的 documents 進行記憶體位址空間隔離是非常重要的(這被稱為
Site Isolation)。理想的情況,每個站點都應該配置設定一個獨立的 renderer 程序,但是實際上,當使用者打開太多的标簽頁,或者裝置沒有足夠的記憶體,就很難限制每個 renderer 程序都隻對應一個站點。是以有時一個 renderer 程序會被多個來自不同站點的 iframes 或者标簽頁所共享。這也意味着一個标簽頁下的 iframes 可能位于不同的 renderer 程序,或者不同标簽頁的 iframes 位于同一個 renderer 程序。renderer 程序,iframes,标簽頁三者之間不是完全 1:1 的映射關系。
因為 renderer 程序運作在沙盒環境下,Blink 需要請求 browser 程序去處理系統調用(比如檔案通路,播放音頻等),還有通路使用者的賬号資料(比如 cookie,密碼等)。browser-renderer 程序間通訊是由
Mojo來實作的。(過去是使用
Chromium IPC,現在還有很多代碼仍舊繼續使用。不過 Chromium IPC 已經逐漸放棄,實際上它的底層也換成了用 Mojo 實作) 對 Chromium 來說,
服務化還在繼續進行,browser 程序會被抽象成一組服務的集合。從 Blink 的角度來看,Blink 可以通過 Mojo 跟其它服務和 browser 程序進行互動。
如果你希望了解更多:
- 多程序架構
- Blink 的 Mojo 程式設計:platform/mojo/MojoProgrammingInBlink.md
線程
在 renderer 線程會建立多少個線程?
Blink 會擁有一個主線程,N 個 worker 線程和一些内部線程。
幾乎所有的主要活動都發生在主線程。所有的 JavaScript 調用(除了運作在 workers 線程的 JS 代碼),DOM,CSS,樣式和排版計算都運作在主線程。Blink 經過高度優化來最大化主線程的性能,它的架構被認為主要是單線程的。
Blink 可能會建立若幹 worker 線程來運作
Web Workers,
Service Worker,和
WorkletsBlink 和 V8 還可能會建立一些内部線程來處理 webaudio,database,GC 等等。
對跨線程通訊,你需要使用 PostTask APIs 來發送消息。我們并不鼓勵使用共享記憶體程式設計,除了少數地方因為性能的原因才需要使用。這也是你在 Blink 的代碼裡面并沒有看到太多需要使用 Mutex 加鎖的原因。
- Blink 的線程程式設計:platform/wtf/ThreadProgrammingInBlink.md
- Workers: core/workers/README.md
Blink 的初始化和終結
Blink 的初始化位于
BlinkInitializer::Initialize(),在執行任何 Blink 代碼之前必須先調用該方法。
另一方面來說,Blink 并沒有提供終結的方法,也就是說,我們會強制退出 renderer 程序,也不做任何清理。一個原因是因為性能。另外一個原因是,通常非常困難以優雅有序的方式去清理 renderer 程序的所有内容(并且也不值得去做)。
目錄結構
Content public APIs 和 Blink public APIs
Content public APIs是 embedders 使用來嵌入 Blink 渲染引擎的 API 層。Content public APIs 需要小心地進行維護,因為它們會被暴露給 embedders。
Blink public APIs是将 //third_party/blink/ 的功能暴露給 Chromium 的 API 層。這個 API 層是從 WebKit 時代繼承過來的曆史産物。在 WebKit 時代,Chromium 和 Safari 共享同樣的 WebKit 實作,是以我們需要一個 API 層來暴露 WebKit 内部的功能給 Chromium 和 Safari 使用。現在 Chromium 是 //third_party/blink/ 的唯一 embedder,是以實際上我們已經不再需要一個額外的 API 層。我們正在積極地減少 Blink public APIs 的數量,通過将更多的 web 平台相關的代碼從 Chromium 移到 Blink 内部(這個項目被成為 Onion Soup)。
目錄結構和依賴性
//third_party/blink/ 的子目錄如下。
這篇文檔提供了更多的資訊:
- platform/ - Blink 的低階功能集合,從龐大的 core/ 中分解出來,包括 geometry 和 graphics 等相關的功能。
- core/ 和 modules/ - 在規範定義的所有 web 平台特性的實作。core/ 實作了跟 DOM 緊密相關的特性。modules/ 實作的特性相對來說會更自包含。比如 webaudio,indexeddb。
- bindings/core 和 bindings/modules/ - 概念上來說 bindings/core 屬于 core/ 的一部分,bindings/modules/ 屬于 modules/ 的一部分。放置于獨立的目錄是因為這部分代碼跟 V8 緊密相關。
- controller/ 一些使用 core/ 和 modules/ 的進階庫。比如,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/ 所使用。
目錄結果和依賴性:
blink/renderer/README.mdBlink 需要很小心地避免過多的外部依賴,維持更高程度的自包含性,但是像 //base 和 //cc 這樣的基礎子產品都不能直接使用的話,也會導緻非常複雜的橋接層,重複實作和多餘的類型轉換,也妨礙了 Blink 後續的演進。早期 Blink 的确是不能使用 //base 和 //cc。
WTF
WTF 是 “Blink 版本的 base” 庫,位于 platform/wtf/. 我們試圖盡可能統一 Chromium 和 Blink 所使用的基礎編碼原語,是以 WTF 應該規模很小。這個庫仍然被需要是因為仍有一些類型,容器和宏是針對 Blink 特定的用途所優化的,另外還有 Olipan(Blink GC)。如果一個類型在 WTF 中存在,Blink 需要優先使用 WTF 的版本而不是 //base 或者 std 庫。最常見的例子是 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
- Olipan (也就是所謂的 Blink GC)
- malloc(不鼓勵使用)
使用 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<> 來管理。我們強烈地不鼓勵手動管理對象的生命周期。手動調用 delete 在 Blink 裡面是被禁止的。
使用 GarbageCollected 可以在 Olipan 堆上配置設定一個對象:
class SomeObject : public GarbageCollected<SomeObject> {
static SomeObject* Create() {
return new SomeObject; // Allocated on Oilpan's heap.
}
};
Oilpan 配置設定的對象生命周期是由垃圾收集機制來管理的。你必須使用特殊的指針來持有 Oilpan 堆上的對象(比如,Member<>, Persistent<>)。需要了解 Oilpan 的編碼限制可以查閱這份
API參考。其中最重要的限制是不允許在一個 Olipan 對象的析構函數裡面通路其它 Olipan 對象,因為析構的順序是無法保證的。
如果你不使用 USING_FAST_MALLOC() 或者 GarbageCollected,對象就會直接配置設定在系統的 malloc 堆上。我們強烈地不建議在 Blink 裡面這樣做。所有的 Blink 對象都應該使用 PartitionAlloc 或者 Oilpan 進行配置設定,規則如下:
- 預設使用 Oilpan。
- 僅當滿足以下條件時可以使用 PartitionAlloc 1) 對象的生命周期非常清晰,使用 std::unique_ptr<> 就足夠了,2) 使用 Olipan 配置設定對象增加了大量的複雜度,或者 3) 使用 Oilpan 配置設定對象給垃圾收集運作時帶來太大不必要的壓力。
無論是使用 PartitionAlloc 還是 Oilpan,你都需要非常小心避免造成懸挂指針(注:強烈不建議使用 raw pointers)或者記憶體洩露。
- 如何使用 PartitionAlloc: platform/wtf/allocator/Allocator.md
- 如何使用 Oilpan: platform/heap/BlinkGCAPIReference.md
- Oilpan GC 設計: platform/heap/BlinkGCDesign.md
任務排程
為了改進渲染引擎的響應性,Blink 的任務應該盡可能異步執行。同步 IPC/Mojo 或者其它可能花費幾毫秒的操作都應該盡量避免(雖然有些确實無法避免,比如運作使用者的 JavaScript)。
所有在 renderer 程序執行的任務都需要通過 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 可以正确且巧妙地排程任務是非常重要。
- 如何送出任務:platform/scheduler/PostTask.md
Page, Frame, Document, DOMWindow 和其它
概念
Page, Frame, Document, ExecutionContext 和 DOMWindow 這些 Blink 裡面的的重要對象,它們的概念如下:
- 一個 Page 對應着一個标簽頁(如果 OOPIF 沒有開啟)。每個 renderer 程序可以擁有多個标簽頁。
- 一個 Frame 對應着網頁裡面的 frame(主 frame 或者 iframe)。每個 Page 都可能包含一個或者多個 Frame,構成樹狀的結構。
- 一個 DOMWindow 對應 JavaScript 裡面的 window 對象。每個 Frame 擁有一個 DOMWindow。
- 一個 Document 對應 JavaScript 裡面的 window.document 對象。每個 Frame 擁有一個 Document。
- 一個 ExecutionContext 是 Document(主線程)或者 WorkerGlobalScope(worker 線程)的抽象。
Renderer 程序 : Page = 1 : N
Page : Frame = 1 : M
Frame : DOMWindow : Document (或者 ExecutionContext) 無論何時都是 1 : 1 : 1 的關系,但是映射的對象可能會發生變化。比如,考慮如下的代碼:
iframe.contentWindow.location.href = "https://example.com";
上面的例子裡面,Blink 會為
https://example.com建立一個新的 Window 和新的 Document 對象。但是它們仍然對應原來的 Frame 對象。
(注:更精确地說,仍有一些特定的情況,我們會建立新的 Document 對象,但是仍舊重用原來的 Window 和 Frame 對象。另外還有一些
更複雜的情況。)
- core/frame/FrameLifecycle.md
程序外 iframe(OOPIF)
機制可以進一步保護網頁的安全,但是也使得事件變得更複雜。Site Isolation 的基本思路是一個 renderer 程序隻對應一個站點。(一個站點是由網頁的注冊域名和它的 URL scheme 組合定義的。例如,
https://mail.example.com https://chat.example.com被認為是同一個站點,但是
https://noodles.com https://pumpkins.com就不是)如果一個 Page 包括一個跨站的 iframe,那麼這個 Page 應該被兩個 renderer 程序所托管。考慮如下頁面:
<!-- https://example.com -->
<body>
<iframe src="https://example2.com"></iframe>
</body>
主 frame 和
<iframe>
可能從屬于不同的 renderer 程序。一個從屬于該 renderer 程序的 frame 對象由 LocalFrame 表征,而不從屬于該 renderer 程序的 frame 對象用 RemoteFrame 表征。
上面的例子,如果從主 frame 的角度看,主 frame 是 LocalFrame 而
<iframe>
是 RemoteFrame。從
<iframe>
的角度看,主 frame 是 RemoteFrame 而
<iframe>
是 LocalFrame。
LocalFrame 和 RemoteFrame 之間的通訊通過 browser 程序來處理(它們可能存在于不同的 renderer 程序)。
- 設計文檔: Site isolation 設計文檔
- 如何編寫符合 Site isolation 的代碼:core/frame/SiteIsolation.md
Detached 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 的行為在規範上并沒有明确定義,不同浏覽器的實作也有差異。基本上可以期望的是,腳本仍然可以正常運作,但是除了少數适當的例外,大部分 DOM 操作都會失敗,例如:
void someDOMOperation(...) {
if (!script_state_->ContextIsValid()) { // The frame is already detached
…; // Set an exception etc
return;
}
}
通常這意味着,當 frame 被分離的時候,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.
}
};
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 檔案應以符合規範的方式編寫(即,隻需從規範中複制和粘貼)。
[Affects=Nothing, PerWorldBindings] readonly attribute Node? firstChild;
像上面的實際例子,Affects=Nothing 和 PerWorldBindings 就是擴充屬性,有些是在規範内的,有些是 Blink 自己特有的。
其次,您需要為 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()。
如果你希望解更多:
- 如何添加 Web IDL 綁定: bindings/IDLCompiler.md
- 如何使用 IDL 擴充屬性: bindings/IDLExtendedAttributes.md
- 規範:
V8 和 Blink
Isolate, Context, World
當你編寫涉及 V8 API 的代碼時,了解 Isolate,Context 和 World 的概念非常重要。它們分别由代碼庫中的 v8::Isolate,v8::Context 和 DOMWrapperWorld 表示。
Isolate 對應于實體線程。Isolate:實體線程的比例在 Blink 中是 1:1。主線程有自己的 Isolate。Worker 線程也有自己的 Isolate。
Context 對應于全局對象(在 Frame 的情況下,它是 Frame 的 window 對象)。由于每個 Frame 有自己的 window 對象,是以在 renderer 程序中會有多個 Contexts。當你調用 V8 API 時,你必須確定你處于正确的 Context 中。否則,v8::Isolate::GetCurrentContext() 将傳回錯誤的 Context,在最壞的情況下,它将最終導緻對象洩漏并導緻安全問題。
World 是用于支援 Chrome 擴充程式中的 content 腳本的概念。World 并不對應 Web 标準中的任何内容。Content 腳本希望與網頁共享 DOM,但出于安全原因,Content 腳本的 JavaScript 對象必須與網頁的 JavaScript 堆進行隔離。 (并且一個 content 腳本的 JavaScript 堆也必須與另一個 content 腳本的 JavaScript 堆隔離。)為了實作隔離,主線程為網頁建立一個 main world,為每個 Content 腳本建立一個 isolated world。Main world 和 isolated world 可以通路相同的 C++ DOM 對象,但它們的 JavaScript 對象是隔離的。我們通過為一個 C++ DOM 對象建立多個 V8 wrappers 來實作這種隔離。即一個 C++ DOM 對象在每個 world 都有一個對應的 V8 wrapper。
Context,World 和 Frame 之間有什麼關系?
想象一下主線程線上有 N 個 Worlds(一個 main world +(N - 1)個 isolated world)。 然後一個 Frame 應該有 N 個 window 對象,一個 window 對象被一個對應的 world 所使用。Context 是對應于 window 對象的概念。 這意味着當我們有 M 個 Frames 和 N 個 Worlds 時,我們有 M * N Contexts(但是 Contexts 是延遲建立的)。
對于 worker 來說,隻有一個 World 和一個全局對象。是以隻有一個 Context。
再次強調,當你使用 V8 API 時,應該非常小心保證使用正确的 context。 否則,你最終可能會在 isolated world 之間洩漏 JavaScript 對象并導緻安全災難(例如,A.com 的擴充可以操縱來自 B.com 的擴充)。
V8 APIs
V8 API 大部分都是在 //v8/include/v8.h 中定義。由于 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.
};
如果是在堆棧外部指向 V8 對象,則需要使用
wrapper tracing。但是,必須非常小心避免建立循環引用。一般而言,V8 API 的确難以使用。如果你不太确定應該怎麼做,請咨詢
blink-review-bindings@- 如何使用 V8 API 和輔助類:platform/bindings/HowToUseV8FromBlink.md
V8 wrappers
每個 C++ DOM 對象(例如,Node)都有其對應的 V8 wrapper。準确地說,每個 C++ DOM對象在每個 world 都有對應的 V8 wrapper。
V8 wrappers 對它們對應的 C++ DOM 對象持有強引用。反之,C++ DOM 對象對 V8 wrappers 隻持有弱引用。 是以如果希望 V8 wrappers 保持存活一段時間,則必須顯式地進行聲明。 否則,V8 wrappers 将可能被過早回收,而 V8 wrappers 上的 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.
如果我們不做任何事情,child 就會被 GC 回收,是以 child.foo 将丢失。為了使 div.firstChild 的 V8 wrapper 保持存活,我們需要增加一種機制,“隻要 div 所屬的 DOM 樹從 V8 是可達的,則讓 div.firstChild 的 V8 wrapper 處于存活狀态”。
有兩種方法可以保持 V8 wrappers 存活:
ActiveScriptWrappable- 如何管理 V8 wrappers 的生命周期:bindings/core/v8/V8Wrapper.md
- 如何使用 wrapper tracing: platform/bindings/TraceWrapperReference.md
渲染流水線
從傳送給 Blink 的 HTML 文檔到螢幕上顯示的像素是一個十分漫長的旅程。渲染流水線架構的示意圖如下:
這一篇
非常棒的文章描述了上面渲染流水線的每一個步驟。我想恐怕我無法寫的比這篇文章更好了 :-)
如果你希望了解更多:
- 概覽: 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
更多問題?
如果你有更多問題,可以發郵件到 [email protected](一般性的問題),或者 [email protected](架構相關的問題)。我們總是樂于提供幫助!:D