[譯] Blink內核是如何工做的?

原文連接css

Blink 是如何工做的

Author: haraken@
Last update: 2018 Aug 14
Status: PUBLIC
譯: LeoYhtml

對於剛接觸 Blink 的開發者來講, Blink 相關的工做並不簡單。由於實現一個高效快速的渲染引擎,須要瞭解大量與 Blink 相關的概念和代碼約定。這對於經驗豐富的 Blink 開發者來講也並不簡單,由於 Blink 項目很龐大,而且對於性能、內存和安全性很敏感。前端

本文的目標是提供一個關於 Blink 工做原理的概覽,但願可以幫助開發者快速熟悉 Blink 的架構。node

  • 本文不是一個關於 Blink 架構細節和代碼風格的詳細教程,而是關於 Blink 基本原理的簡單介紹。這部分原理在短時間內不會有大的改變,另外提供了一些深刻了解這些部分的相關資源。
  • 本文不會介紹具體的功能(好比 ServiceWorkersediting 等),而是介紹了代碼中普遍使用的一些基本的功能(好比內存管理, V8 APIs 等)

訪問 Chromium wiki page 來獲取更多的關於 Blink 開發的信息c++

Blink 作了什麼

Blink 是一個Web平臺的渲染引擎。粗略地說,在一個瀏覽器tab頁中與內容渲染相關的全部事情都是由 Blink 實現的。git

  • 實現Web平臺的規格(好比, HTML標準規格),包括 DOM , CSSWeb IDL (Web瀏覽器編程接口描述)github

  • 嵌入 V8 和運行 Javascriptweb

  • 從底層的網絡堆棧請求資源數據庫

  • 構建 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 的身影,好比 ChromiumAndroid WebView 以及經過 content public APIs 內嵌 BlinkOpera 瀏覽器。

從代碼庫的角度來看, 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 ), DOMCSS ,樣式和佈局計算都在主線程中運行。 Blink 經過許多優化來最大化主線程的性能,模擬了一個近乎單線程的架構。 Blink 可能會建立多個工做線程來運行 Web Workers ServiceWorker 以及 Worklet BlinkV8 可能會建立三兩個內部進程來處理 音頻 webaudio , 數據庫 database , 內存回收 GC 等。

線程之間的通訊,須要使用 PostTask APIs 來傳遞。 出於性能的考慮,除了幾個特別的地方,共享內存編程是不推薦的,因此在 Blink 中源碼中也不多使用互斥鎖這種東西。

瞭解更多:

Blink 的初始化和終止

Blink 經過 BlinkInitializer::Initialize() 來初始化。這個方法必須在執行 Blink 代碼前調用。

Blink 沒有終止化的狀態,緣由是渲染進程是被強制退出的,而不是被清理回收的。緣由之一是出於性能的考慮(強制退出不須要作額外的操做)。另外一個緣由是渲染進程在正常退出的狀況下,通常很難把因此東西都清理回收掉。(而且這樣作的代價高於帶來的效益)

目錄架構

Content public APIsBlink 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 時代,因爲 ChromiumSafari 會共享 Webkit 的實現,因此當時這個API層既要顧及 Chromium 也要顧及 Safari 。而如今 Blink 內核的功能只須要提供給 Chromium ,舊的API層有些API就不須要了。因此咱們將 Chromium 中與平臺相關的代碼遷移到 Blink 中來減小 Blink public APIs 的數量(這個項目被叫做 Onion Soup

目錄架構和依賴

//third_party/blink/ 的目錄以下,查閱這個文檔瞭解更多。

  • platform/
    • 一組從core/裏面分解出來的 Blink 底層功能,好比地理位置 geometry 和圖形 graphics 相關的庫
  • core/ 和 modules/
    • 實現Web平臺規格文件的全部功能。core/主要是實現 DOM 相關的功能。modules/主要實現了一些瀏覽器自有的功能,好比 webaudio , indexeddb
  • 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 項目中被當心翼翼地精心維護着

瞭解更多:

WTF

WTFBlink 特有的工具庫,位於 platform/wtf/ 。咱們儘量統一 ChromiumBlink 的代碼,因此 WTF 的體積會很小。 WTF 這個工具庫之因此存在,是由於對於 Blink 的工做負載和 Oilpan (即 Blink GC ) 中有大量的類型( types ),容器( containers )和宏( macros )須要作性能優化。若是類型在 WTF 中有相應的定義,在 Blink 中就須要使用 WTF 的類型而不是定義在 //base 或者 std libraries 中的類型。 使用的最多的類型是 vectors , hashsets , hashmapsstrings 。相應的在 Blink 中應該使用 WTF::Vector , WTF::HashSet , WTF::HashMap , WTF::StringWTF::AtomicString 而不是 std::vector , std::*set, std::*mapstd::string

瞭解更多:

內存管理

你須要關注三個與 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: 裸指針也是極其其不推薦的)

瞭解更多:

任務調度

爲了提高渲染引擎的響應速度,在 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 , ExecutionContextDOMWindow 的含義以下:

  • 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. 建立了新的 DOMWindowDocument ,而 Frame 則可能被重用。(Note: 準確的來講,還存在一些更復雜的狀況建立了新的 Document ,而 DOMWindowFrame 被重用了)。

瞭解更多:

  • 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, 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>
複製代碼

frameiframe 可能運行在不一樣的渲染進程中。渲染進程本地的 frameLocalFrame 呈現,不屬於渲染進程本地的 frameRemoteFrame 呈現。

從主 frame 的角度來看,主 frame 是一個 LocalFrameiframe 是一個 RemoteFrame 。從 iframe 的角度來看,主 frame 是一個 RemoteFrame ,而 iframe 是一個 LocalFrame

LocalFrameRemoteFrame (二者可能存在與不一樣的渲染進程中)之間的通訊是瀏覽器進程來處理的。

瞭解更多:

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 attributesthe Web IDL spec 裏面定義了一些 IDL extended attributes ,其餘的在 Blink-specific IDL extended attributes中。除了 Blink 特有的 IDL extended attributes ,其餘IDL文件都應該按照和規格文件一致的格式來寫(意思就是直接從規格文件裏面cv)。

接下來,你須要爲 Node 節點定義一個 C++ class 類,並用c++實現 firstChildgetter ,如:

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 interfaceNode.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::ContextDOMWrapperWorld

Isolate 是一個物理上的線程,在 BlinkIsolate : physical=1:1 。主線程和工做線程都有其獨立的 Isolate

Context 是一個全局的對象(以 Frame 來講, FrameContextwindow 對象)。因爲每一個 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 worldisolated worlds 均可以訪問到C++上的DOM對象,可是他們各自的 JavaScript 對象都是隔離的。這種隔離是經過爲每一個 C++DOM 對象建立多個 V8 wrapper 來實現的。即每一個 world 對應一個 V8 wrapper

Context , WorldFrame 之間有什麼聯繫? 想象一下, 在主線程中存在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 。最多見的 handlev8::Local<> , v8::Local<> 用於從機器堆棧 machine stack 指向 V8 objectsv8::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++ DOMV8 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 wrappersActiveScriptWrappablewrapper tracing.

瞭解更多:

渲染管道 Rendering pipeline

一個HTML文件從傳遞到 Blink 再到屏幕上顯示的像素之間有一段很長的歷程。渲染管道的架構以下:

Life of A Pixel裏面介紹了渲染管道的每個階段。

瞭解更多:

Questions?

有問題能夠到 blink-dev@chromium.orgplatform-architecture-dev@chromium 提問。

相關文章
相關標籤/搜索