chrome渲染引擎blink如何工作

眨眼的工作原理

bit.ly/how-blink-works

作者:haraken @

最后更新:2018年8月14日

狀態:PUBLIC

 

在眨眼上工作并不容易。對于新的Blink開發人員而言,這并不容易,因為已經引入了許多特定于Blink的概念和編碼約定來實現非??焖俚匿秩疽?。即使對于有經驗的Blink開發人員來說,這也不容易,因為Blink龐大且對性能,內存和安全性極為敏感。

 

本文檔旨在提供1?萬英尺的“ Blink的工作原理”概述,我希望它將有助于Blink開發人員快速熟悉該體系結構:

 

  • 該文檔不是有關Blink詳細架構和編碼規則(可能會更改和過時)的詳盡教程。相反,該文檔簡明扼要地描述了Blink的基本原理(短期內不太可能改變),并指出了您想了解更多信息時可以閱讀的資源。
  • 該文檔未解釋特定功能(例如ServiceWorkers,編輯)。而是該文檔解釋了廣泛的代碼庫所使用的基本功能(例如,內存管理,V8 API)。

 

有關Blink開發的更多常規信息,請參閱Chromium Wiki頁面。

 

眨眼做什么

流程/線程架構

工藝流程

線程數

閃爍的初始化

目錄結構

內容公共API和Blink公共API

目錄結構和依賴性

WTF

內存管理

任務調度

頁面,框架,文檔,DOMWindow等

概念

OOPIF

分離的框架/文件

Web IDL綁定

V8和閃爍

隔離,上下文,世界

V8 API

V8包裝器

渲染管線

有什么問題嗎

 

眨眼做什么

Blink是Web平臺的渲染引擎。粗略地說,Blink實現了所有在瀏覽器選項卡中呈現內容的內容:

 

  • 實施Web平臺的規范(例如HTML標準),包括DOM,CSS和Web IDL
  • 嵌入V8并運行JavaScript
  • 從基礎網絡堆棧請求資源
  • 建立DOM樹
  • 計算樣式和布局
  • 嵌入Chrome合成器并繪制圖形

 

許多用戶(例如Chromium,Android WebView和Opera)通過內容公開API嵌入了Blink?。

 

從代碼庫的角度來看,“閃爍”通常是指// third_party / blink /。從項目角度來看,“閃爍”通常表示實現Web平臺功能的項目。實現Web平臺功能的代碼跨越// third_party / blink /,// content / renderer /,// content /瀏覽器/和其他地方。

流程/線程架構

工藝流程

鉻具有多工藝體系結構。Chromium具有一個瀏覽器進程和N個沙盒渲染器進程。閃爍在渲染器進程中運行。

 

創建了多少個渲染器進程?出于安全原因,隔離跨站點文檔之間的內存地址區域非常重要(這稱為Site Isolation)。從概念上講,每個渲染器過程最多應專用于一個站點。但是實際上,當用戶打開太多標簽頁或設備沒有足夠的RAM時,有時將每個渲染器進程限制在一個站點上有時會很繁瑣。然后,渲染器進程可以由從不同站點加載的多個iframe或標簽共享。這意味著一個選項卡中的iframe可以由不同的渲染器進程托管,而不同選項卡中的iframe可以由相同的渲染器進程托管。渲染器進程,iframe和Tab之間沒有1:1映射。

 

假定渲染器進程在沙箱中運行,則Blink需要要求瀏覽器進程調度系統調用(例如,文件訪問,播放音頻)并訪問用戶配置文件數據(例如Cookie,密碼)。這種瀏覽器-渲染器過程通信是由Mojo實現的。(注意:過去我們使用的是Chromium IPC,但仍然有很多地方在使用它。但是,它已被棄用,并在后臺使用Mojo。)在Chromium方面,服務化正在進行中,并將瀏覽器過程抽象為一組“服務。從Blink角度來看,Blink可以僅使用Mojo與服務和瀏覽器進程進行交互。

 

如果您想了解更多信息:

 

  • 多進程架構
  • Blink中的Mojo編程:platform / mojo / MojoProgrammingInBlink.md

線程數

在渲染器進程中創建了多少個線程?

 

眨眼有一個主線程,N個工作線程和幾個內部線程。

 

幾乎所有重要的事情都在主線程上發生。所有JavaScript(工人除外),DOM,CSS,樣式和布局計算都在主線程上運行。假設主要是單線程體系結構,Blink進行了高度優化以最大化主線程的性能。

 

眨眼可能會創建多個工作線程來運行Web Workers,ServiceWorkerWorklets。

 

Blink和V8可能會創建幾個內部線程來處理webaudio,數據庫,GC等。

 

對于跨線程通信,必須使用通過PostTask API傳遞消息。不建議使用共享內存編程,除了出于性能原因確實需要使用它的幾個地方。這就是為什么您在Blink代碼庫中看不到很多MutexLocks的原因。

 

如果您想了解更多信息:

 

閃爍的初始化和完成

眨眼由BlinkInitializer :: Initialize()初始化。在執行任何Blink代碼之前必須調用此方法。

 

另一方面,Blink從未完成。也就是說,渲染器進程被強制退出而不進行清理。原因之一是性能。另一個原因是,通常很難以一種有序的方式清理渲染器進程中的所有內容(這是不值得的工作)。

目錄結構

內容公共API和Blink公共API

內容公共API是使嵌入程序嵌入呈現引擎的API層。內容公共API必須小心維護,因為它們會暴露在嵌入程序中。

 

眨眼公共API是將// // third_party / blink /的功能公開給Chromium的API層。該API層只是從WebKit繼承的歷史工件。在WebKit時代,Chromium和Safari共享WebKit的實現,因此需要API層才能將WebKit的功能公開給Chromium和Safari。既然Chromium是// third_party / blink /的唯一嵌入者,那么API層就沒有意義了。通過將網絡平臺代碼從Chromium移到Blink(該項目稱為Onion Soup),我們正在積極減少Blink公共API的數量。

 

目錄結構和依賴性

// third_party / blink /具有以下目錄。有關這些目錄的更詳細定義,請參閱此文檔

 

  • 平臺/
    • Blink的較低級功能的集合,這些功能是從整體內核中剔除的。例如,幾何和圖形工具。
  • 核心/和模塊/
    • 規范中定義的所有Web平臺功能的實現。core /實現與DOM緊密結合的功能。模塊/實現更多獨立功能。例如webaudio,indexeddb。
  • 綁定/核心/和綁定/模塊/
    • 從概念上講,bindings / core /是core /的一部分,而bindings / modules /是modules /的一部分。大量使用V8 API的文件被放在bindings / {core,modules}中。
  • 控制器/
    • 一組使用core /和modules /的高級庫。例如devtools前端。

 

依存關系按以下順序流動:

 

  • 鉻=>控制器/ =>模塊/和綁定/模塊/ =>核心/和綁定/核心/ =>平臺/ =>底層基元,例如// base,// v8和// cc

 

Blink仔細維護暴露于// third_party / blink /的低級基元列表。

 

如果您想了解更多信息:

 

WTF

WTF是一個“特定于眨眼的基礎”庫,位于platform / wtf /。我們正在嘗試盡可能多地統一Chromium和Blink之間的編碼原語,因此WTF應該很小。需要此庫是因為確實需要針對Blink的工作量和Oilpan(Blink GC)優化許多類型,容器和宏。如果類型是在WTF中定義的,則Blink必須使用WTF類型而不是// base或std庫中定義的類型。最受歡迎的是矢量,哈希集,哈希圖和字符串。眨眼應該使用WTF :: Vector,WTF :: HashSet,WTF :: HashMap,WTF :: String和WTF :: AtomicString而不是std :: vector,std :: * set,std :: * map和std :: string 。

 

如果您想了解更多信息:

 

內存管理

就Blink而言,您需要關心三個內存分配器:

 

 

您可以使用USING_FAST_MALLOC()在PartitionAlloc的堆上分配一個對象:

 

類SomeObject {

??USING_FAST_MALLOC(SomeObject);

??靜態std :: unique_ptr <SomeObject> Create(){

????返回std :: make_unique <SomeObject>();?//分配在PartitionAlloc的堆上。

}

};

 

由PartitionAlloc分配的對象的生存期應由scoped_refptr <>或std :: unique_ptr <>管理。強烈建議不要手動管理生命周期。閃爍禁止手動刪除。

 

您可以使用GarbageCollected在Oilpan的堆上分配一個對象:

 

類SomeObject:公共GarbageCollected <SomeObject> {

??靜態SomeObject * Create(){

????返回新的SomeObject;?//分配在Oilpan的堆上。

??}

};

 

Oilpan分配的對象的生存期由垃圾收集自動管理。您必須使用特殊的指針(例如Member <>,Persistent <>)將對象保存在Oilpan的堆上。請參閱此API參考以熟悉有關Oilpan的編程限制。最重要的限制是不允許您在油鍋對象的析構函數中觸摸任何其他油鍋對象(因為無法保證銷毀順序)。

 

如果您既不使用USING_FAST_MALLOC()也不使用GarbageCollected,則在系統malloc的堆上分配對象。在眨眼中強烈建議不要這樣做。所有Blink對象應由PartitionAlloc或Oilpan分配,如下所示:

 

  • 默認情況下使用Oilpan。
  • 僅在以下情況下才使用PartitionAlloc:1)對象的生存期非常明確并且std :: unique_ptr <>或scoped_refptr <>足夠,2)在Oilpan上分配對象會帶來很多復雜性,或者3)在Oilpan上分配對象會導致給垃圾收集運行時帶來了不必要的壓力。

 

無論使用PartitionAlloc還是Oilpan,都必須非常小心,不要創建懸空的指針(注意:強烈建議不要使用原始指針)或內存泄漏。

 

如果您想了解更多信息:

 

任務調度

為了提高渲染引擎的響應速度,Blink中的任務應盡可能異步執行。不鼓勵同步IPC / Mojo和任何其他可能花費幾毫秒的操作(盡管某些操作是不可避免的,例如用戶的JavaScript執行)。

 

呈現器進程中的所有任務都應使用正確的任務類型發布到Blink Scheduler,如下所示:

 

//使用kNetworking的任務類型將任務發布到框架的調度程序

frame-> GetTaskRunner(TaskType :: kNetworking)-> PostTask(…,WTF :: Bind(&Function));

 

Blink Scheduler維護多個任務隊列,并巧妙地對任務進行優先級排序,以最大化用戶感知的性能。重要的是要指定適當的任務類型,以使Blink Scheduler能夠正確,智能地調度任務。

 

如果您想了解更多信息:

 

  • 如何發布任務:platform / scheduler / PostTask.md

頁面,框架,文檔,DOMWindow等

概念

頁面,框架,文檔,ExecutionContext和DOMWindow是以下概念:

 

  • 頁面與選項卡的概念相對應(如果未啟用下面說明的OOPIF)。每個渲染器進程可能包含多個選項卡。
  • 框架對應于框架(主框架或iframe)的概念。每個頁面可以包含一個或多個以樹狀層次結構排列的框架。
  • DOMWindow對應于JavaScript中的窗口對象。每個框架都有一個DOMWindow。
  • Document對應于JavaScript中的window.document?對象。每個框架都有一個文檔。
  • ExecutionContext是一個抽象文檔(用于主線程)和WorkerGlobalScope(用于工作線程)的概念。

 

渲染過程:頁面= 1:N

 

頁:框架= 1:M.

 

框架:DOMWindow:文檔(或ExecutionContext)= 1:1:1在任何時間點,但映射可能隨時間而變化。例如,考慮以下代碼:

 

iframe.contentWindow.location.href =“ https://example.com”;

 

在這種情況下,將為https://example.com創建一個新的DOMWindow和一個新的Document?。但是,可以重復使用該框架。

 

(注意:確切地說,在某些情況下會創建一個新的Document,但是DOMWindow和Frame會被重用。甚至還有一些更復雜的情況。)

 

如果您想了解更多信息:

 

  • 核心/框架/FrameLifecycle.md

進程外iframe(OOPIF)

站點隔離使事情變得更加安全,但更加復雜。🙂站點隔離的想法是為每個站點創建一個渲染器進程。(網站是頁面的可注冊域+ 1標簽及其URL方案。例如,https://mail.example.comhttps://chat.example.com在同一網站中,但https:// noodles.comhttps://pumpkins.com都沒有。)如果一個頁面包含一個跨站點IFRAME,該頁面可以由兩個渲染過程托管??紤]以下頁面:

 

<!– https://example.com –>

<身體>

<iframe src =” https://example2.com”> </ iframe>

</ body>

 

主框架和<iframe>可以由不同的渲染器進程托管。渲染器進程本地的幀由LocalFrame表示,而不是渲染器進程本地的幀由RemoteFrame表示。

 

從主框架的角度來看,主框架是LocalFrame,而<iframe>是RemoteFrame。從<iframe>的角度來看,主框架是RemoteFrame,而<iframe>是LocalFrame。

 

本地框架和遠程框架(可能存在于不同的渲染器進程中)之間的通信是通過瀏覽器進程進行處理的。

 

如果您想了解更多信息:

 

分離的框架/文件

相框/文檔可能處于分離狀態??紤]以下情況:

 

doc = iframe.contentDocument;

iframe.remove();?//將iframe與DOM樹分離。

doc.createElement(“ div”);?//但是您仍然可以在分離的框架上運行腳本。

 

棘手的事實是,您仍然可以在分離的框架上運行腳本或DOM操作。由于框架已經分離,大多數DOM操作將失敗并引發錯誤。不幸的是,分離框架上的行為在瀏覽器之間并不能真正實現互操作,在規范中也沒有明確定義?;旧?,人們期望JavaScript可以繼續運行,但是大多數DOM操作應該會因某些適當的異常而失敗,例如:

 

無效someDOMOperation(…){

??if(!script_state _-> ContextIsValid()){//框架已經分離

????…;//設置例外等

????返回;

}

}

 

這意味著在通常情況下,當框架分離時,Blink需要執行一系列清理操作。您可以通過從ContextLifecycleObserver繼承來做到這一點,如下所示:

 

類SomeObject:公共GarbageCollected <SomeObject>,公共ContextLifecycleObserver {

??void ContextDestroyed()覆蓋{

????//在此進行清理操作。

}

???SomeObject(){

????//在這里進行清理操作不是一個好主意,因為現在進行清理已經太遲了。此外,不允許析構函數接觸Oilpan堆上的任何其他對象。

??}

};

Web IDL綁定

當JavaScript訪問node.firstChild時,將調用node.h?中的Node :: firstChild()。它是如何工作的?讓我們看一下node.firstChild的工作方式。

 

首先,您需要根據規范定義一個IDL文件:

 

// node.idl

接口Node:EventTarget {

??[…]只讀屬性Node?第一個孩子;

};

 

Web IDL的語法在Web IDL規范中定義。[…]?稱為IDL擴展屬性。一些IDL擴展屬性是在Web IDL規范中定義的,而另一些是特定于Blink的IDL擴展屬性。除了特定于閃爍的IDL擴展屬性外,IDL文件應以符合規范的方式編寫(即,僅從規范中復制并粘貼)。

 

其次,您需要為Node定義一個C ++類,并為firstChild實現一個C ++ getter:

 

class EventTarget:public Sc??riptWrappable {//所有暴露給JavaScript的類都必須從ScriptWrappable繼承。

…;

};

 

類Node:public EventTarget {

??DEFINE_WRAPPERTYPEINFO();?//所有具有IDL文件的類都必須具有此宏。

??節點* 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()。

 

如果您想了解更多信息:

 

V8和閃爍

隔離,上下文,世界

當您編寫涉及V8 API的代碼時,了解隔離,上下文和世界的概念很重要。它們分別在代碼庫中由v8 :: Isolate,v8 :: Context和DOMWrapperWorld表示。

 

隔離對應于物理線程。隔離:閃爍中的物理線程= 1:1。主線程具有自己的隔離。輔助線程具有其自己的隔離。

 

上下文對應于全局對象(在使用框架的情況下,它是框架的窗口對象)。由于每個框架都有其自己的窗口對象,因此渲染器進程中存在多個上下文。調用V8 API時,必須確保您使用的是正確的上下文。否則,v8 :: Isolate :: GetCurrentContext()將返回錯誤的上下文,在最壞的情況下,它將最終導致對象泄漏并導致安全問題。

 

World是支持Chrome擴展程序的內容腳本的概念。世界與Web標準中的任何內容都不對應。內容腳本希望與網頁共享DOM,但是出于安全原因,必須將內容腳本的JavaScript對象與網頁的JavaScript堆隔離。(還必須將一個內容腳本的JavaScript堆與另一個內容腳本的JavaScript堆隔離。)為了實現隔離,主線程為網頁創建了一個主世界,為每個內容腳本創建了一個隔離世界。主世界和孤立世界可以訪問相同的C ++ DOM對象,但是它們的JavaScript對象是孤立的。通過為一個C ++ DOM對象創建多個V8包裝器來實現這種隔離。也就是說,每個世界一個V8包裝器。

 

上下文,世界和框架之間有什么關系?

 

想象一下,在主線程上有N個世界(一個主世界+(N – 1)個孤立世界)。然后,一個框架應具有N個窗口對象,每個窗口對象用于一個世界。上下文是與窗口對象相對應的概念。這意味著,當我們有M個框架和N個世界時,我們有M * N個上下文(但是這些上下文是惰性創建的)。

 

如果是工人,則只有一個世界和一個全局對象。因此,只有一個上下文。

 

同樣,當您使用V8 API時,應該非常小心使用正確的上下文。否則,您最終將在孤立的世界之間泄漏JavaScript對象并造成安全災難(例如,來自A.com的擴展程序可以操縱來自B.com的擴展程序)。

 

如果您想了解更多信息:

 

V8 API

//v8/include/v8.h中定義了許多V8 API?。由于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范圍;

??v8 :: Local <v8 :: Object> object =…;?// 這是對的。

}

 

類SomeObject:公共GarbageCollected <SomeObject> {

??v8 :: Local <v8 :: Object> object_;?//這是錯誤的。

};

 

如果要從計算機堆棧外部指向V8對象,則需要使用包裝器跟蹤。但是,您必須非常小心,不要用它創建參考循環。通常,V8 API很難使用。如果您不確定自己在做什么,請詢問blink-review-bindings @。

 

如果您想了解更多信息:

 

  • 如何使用V8 API和幫助程序類:platform / bindings / HowToUseV8FromBlink.md

V8包裝器

每個C ++ DOM對象(例如Node)都有其對應的V8包裝器。確切地說,每個C ++ DOM對象每個世界都有其對應的V8包裝器。

 

V8包裝器對其相應的C ++ DOM對象有很強的引用。但是,C ++ DOM對象僅對V8包裝程序具有弱引用。因此,如果您想讓V8包裝器存活一段時間,則必須明確地做到這一點。否則,將過早收集V8包裝器,并且V8包裝器上的JS屬性將丟失。

 

div = document.getElementbyId(“ div”);

child = div.firstChild;

child.foo =“酒吧”;

child = null;

GC();?//如果不執行任何操作,則| firstChild |的V8包裝器?由GC收集。

assert(div.firstChild.foo ===“ bar”);?// …這將失敗。

 

如果我們什么都不做,GC會收集child?,因此child.foo?會丟失。為了使div.firstChild?的V8包裝器保持活動狀態,我們必須添加一種機制,“?只要div?所屬的DOM樹可以從V8到達,則使div.firstChild?的V8包裝器保持活動狀態”。

 

有兩種方法可以使V8包裝器保持活動狀態:ActiveScriptWrappable包裝器跟蹤。

 

如果您想了解更多信息:

 

渲染管線

從將HTML文件發送到Blink到在屏幕上顯示像素還有很長的一段路要走。渲染管道的結構如下。

 

閱讀這個出色的資料,以了解渲染管線的每個階段的功能。(我認為我能寫出比甲板更好的解釋🙂

 

如果您想了解更多信息,請聯系:GC收集。

 

assert(div.firstChild.foo ===“ bar”);?// …這將失敗。

 

如果我們什么都不做,GC會收集child,因此child.foo會丟失。為了使div.firstChild的V8包裝器保持活動狀態,我們必須添加一種機制,“只要div所屬的DOM樹可以從V8到達,則使div.firstChild的V8包裝器保持活動狀態”。

 

有兩種方法可以使V8包裝器保持活動狀態:ActiveScriptWrappable和包裝器跟蹤。

 

如果您想了解更多信息:

 

如何管理V8包裝器的生命周期:bindings / core / v8 / V8Wrapper.md

 

如何使用包裝程序跟蹤:platform / bindings / TraceWrapperReference.md

 

渲染管線

從將HTML文件發送到Blink到在屏幕上顯示像素還有很長的一段路要走。渲染管道的結構如下。