原文連接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 提問。