Airing

Airing

哲学系学生 / 小学教师 / 程序员,个人网站: ursb.me
github
email
zhihu
medium
tg_channel
twitter_id

Chromium 渲染流水線——字節碼到像素的一生

現代瀏覽器架構#

在開始介紹渲染流水線之前,我們需要先介紹一下 Chromium 的瀏覽器架構與 Chromium 的進程模型作為前置知識。

兩個公式#

公式 1: 瀏覽器 = 瀏覽器內核 + 服務

  • Safari = WebKit + 其他組件、庫、服務  
  • Chrome = Chromium + Google 服務集成  
  • Microsoft Edge (Chromium) = Chromium + Microsoft 服務集成  
  • Yandex Browser = Chromium + Yandex 服務集成  
  • 360 安全瀏覽器 = Trident + Chromium + 360 服務集成  
  • Chromium = Blink + V8 + 其他組件、庫、服務

公式 2:內核 = 渲染引擎 + JavaScript 引擎 + 其他

BrowserRendering EngineJavaScript Engine
Internet ExplorerTrident (MSHTML)JScript/Chakra
Microsoft EdgeEdgeHTML → BlinkChakra → V8
FirefoxGeckoSpiderMonkey
SafariKHTML → WebKitJavaScriptCore
ChromeWebKit → BlinkV8
OperaPresto → WebKit → BlinkCarakan → V8

這裡我們可以發現除了 Firefox 和已經死去的 IE,市面上大部分瀏覽器都朝著 Blink + V8 或是 WebKit + JavaScriptCore 的路線進行演變。

渲染引擎#

負責解析 HTML, CSS, JavaScript,渲染頁面。

以 Firexfox 舉例,有以下工作組:

  • Document parser (handles HTML and XML)  
  • Layout engine with content model  
  • Style system (handles CSS, etc.)  
  • JavaScript runtime (SpiderMonkey)  
  • Image library  
  • Networking library (Necko)  
  • Platform-specific graphics rendering and widget sets for Win32, X, and Mac
  • User preferences library  
  • Mozilla Plug-in API (NPAPI) to support the Navigator plug-in interface  
  • Open Java Interface (OJI), with Sun Java 1.2 JVM  
  • RDF back end  
  • Font library  
  • Security library (NSS)

接下來,我們看看 WebKit 的發展歷程。

Apple 2001 年基於 KHTML 開發了 WebKit 作為 Safari 的內核,之後 Google 在 2008 年時基於 WebKit 自研 Chromium,那時候的 Chrome 渲染引擎採用的也是 Webkit。2010 年時,Apple 升級重構了 WebKit,其就是如今 WKWebView 與 Safari 的渲染引擎 WebKit2。2013 年時,Google 基於 WebKit 開發了自己的渲染引擎 —— Blink,其作為如今 Chromium 的渲染引擎。因為開源協議的關係,我們如今看 Blink 源碼依然能看到很多 Apple 和 WebKit 的影子。

WebKit 的演變路線大致歷程如下圖所示:

image
通過 Web Platform Tests 的測試報告可見 Chromium 渲染引擎的兼容性也是極好的:

image

JavaScript 引擎#

JavaScript 引擎在瀏覽器中通常作為渲染引擎內置的一個模塊,但同時它的獨立性非常好,也可以作為獨立的引擎移植到其他地方使用。

這裡列舉幾個業內有名的 JavaScript 引擎:

  • SpiderMonkey: Mozilla 的 JavaScript 引擎,使用 C/C++ 編寫,作為 Firefox 的 JavaScript 引擎。
  • Rhino: Mozilla 的開源 JavaScript 引擎,使用 Java 編寫。 
  • Nashorn: Oracle Java Development Kit (JDK) 8 開始內置的 JavaScript 引擎,使用 Java 編寫。
  • JavaScriptCore: WebKit 內置的 JavaScript 引擎,其作為系統提供給開發者使用,iOS 移動端應用可以直接零增量引入 JavaScriptCore(但這種場景下無法開啟 JIT)。
  • ChakraCore: Microsoft 的開源 JavaScript 引擎,而如今已全面使用 Chromium 作為 Edge,因此除了 Edge iOS 移動端以外(Chromium iOS 端使用 JavaScriptCore 作為 JavaScript 引擎),其他端的 Edge 使用的都是 V8 引擎。
  • V8: Google 的開源 JavaScript 引擎,使用 C++ 編寫,作為 Chromium(或者更進一步可以說 Blink)的內置 JavaScript 引擎,同時也是 Android 系統 WebView 的內置引擎(因為 Android WebView 也是 Chromium 嘛,笑)。性能優異,開啟 JIT 之後的性能吊打一眾引擎。此外,ES 語法兼容性表現也比較優秀(可見後文表格)。
  • JerryScript: Samsung 開源的 JavaScript 引擎,被 IoT.js 使用。
  • Hermes: Facebook 的開源 JavaScript 引擎,為 React Native 等 Hybrid UI 系統打造的引擎。支持直接加載字節碼,從而使得 JS 加載時間縮短,讓 TTI 得到優化。此外引擎還對字節碼做過優化,且支持增量加載,對中低端機更友好。但是其設計為膠水語言解釋器而存在,故不支持 JIT。(移動端 JS 引擎會限制 JIT 的使用,因為開 JIT 之後預熱時間會變得很長,從而影響頁面首屏時間;此外也會增加包體積和內存占用。)
  • QuickJS: 由 FFmpeg 作者 Fabrice Bellard 開發,體積極小(210 KB),且兼容性良好。直接生成字節碼,且支持引入 C 原生模塊,性能優異。在單核機器上有著 300 μs 極低的啟動時間,內存占用也極低,使用引用計數,內存管理優秀。QuickJS 非常適合 Hybrid 架構、遊戲腳本系統或其他嵌入式系統。

各引擎性能表現如下圖所示:

image

ECMAScript 標準支持情況:

image

Chromium 進程模型#

Chromium 有 5 類進程:

  • Browser Process:1 個
  • Utility Process:1 個
  • Viz Process:1 個
  • Plugin Process:多個
  • Render Process:多個

抛開 Chrome 擴展的 Plugin Process,和渲染強相關的有 Browser Process、Render Process、Viz Process。接下來,我們重點看看這 3 類進程。

image

Render Process#

  • 數量:多個
  • 職責:負責單個 Tab 內單個站點(注意跨站點 iframe 的情況)的渲染、動畫、滾動、Input 事件等。
  • 线程:
    • Main thread x 1
    • Compositor thread x 1
    • Raster thread x 1
    • worker thread x N

Render Process 負責的區域是 WebContent:

image

Main thread#

職責:

  • 執行 JavaScript
  • Event Loop
  • Document 生命週期
  • Hit-testing
  • 事件調度
  • HTML、CSS 等數據格式的解析
    image

Compositor Thread#

職責:

  • Input Handler & Hit Tester
  • Web Content 中的滾動與動畫
  • 計算 Web Content 的最優分層
  • 協調圖片解碼、繪製、光柵化任務(helpers)

其中,Compositor thread helpers 的數目取決於 CPU 核心數。

image

Browser Process#

  • 數量:1 個
  • 職責:負責 Browser UI (不包含 WebContent 的 UI)的全部能力,包括渲染、動畫、路由、Input 事件等。
  • 线程:
    • Render & Compositing Thread
    • Render & Compositing Thread Helpers

Viz Process#

  • 數量:1 個
  • 職責:接受 Render Process 和 Browser Process 產生的 viz::CompositorFrame,並將其合成 (Aggregate),最後使用 GPU 將合成結果上屏 (Display)。
  • 线程:
    • GPU main thread
    • Display Compositor Thread

Chromium 的進程模式#

  • Process-per-site-instance:老版本的默認策略,如果從一個頁面打開了另一個新頁面,而新頁面和當前頁面屬於同一站點(根域名與協議相同)的话,那麼這兩個頁面會共用一個 Render Process。
  • Process-per-site
  • Process-per-tab:如今版本的默認策略,每個 Tab 起一個 Render Process。但注意站點內部的跨站 iframe 也會啟動一個新的 Render Process。可看下文 Example。
  • Single Process:單進程模式,啟動參數可控,用於 Debug。

示例:

假設現在有 3 個 Tab,分別打開了 foo.com,bar.com,baz.com 三個站點,其中 bar.com、baz.com 不涉及 iframe;但 foo.com 涉及,它的代碼如下所示:

<html>
  <iframe id=one src="foo.com/other-url"></iframe>
  <iframe id=two src="bar.com"></iframe>
</html>

那麼按照 Process-per-tab 模式,最終的進程模型如下圖所示:

image

Chromium 渲染流水線#

至今前置知識已介紹完畢,開啟本文的核心部分 —— Chromium Rendering Pipeline。

所謂渲染流水線,就是從接受網絡的字節碼開始,一步步處理這些字節碼把它們轉變成屏幕上像素的過程。經過梳理之後,包括以下 13 個流程:

  1. Parsing
  2. Style
  3. Layout
  4. Pre-paint
  5. Paint
  6. Commit
  7. Compositing
  8. Tiling
  9. Raster
  10. Activate
  11. Draw
  12. Aggregate
  13. Display

整理了一下各自流程所在的模塊與進程線程,繪製的最終流水線如下圖所示:

image

下文,我們一步步來看。

注:本文屬於 Overview,所以力求簡潔、不貼源碼,但是會把設計到源碼的部分打上源碼鏈接,讀者們可以自己索引閱讀。同時,有些環節我撰寫了更詳細的流程分析文章,會貼在對應章節的開頭處,感興趣的讀者可以點進去詳細閱讀。

Parsing#

本節推薦閱讀該系列的文章《Chromium Rendering Pipeline - Parsing》以深入了解 Parsing。

image

  • 模塊:blink
  • 進程:Render Process
  • 线程:Main thread
  • 職責:解析 Browser Process 網絡線程傳過來的 bytes,經過解析處理,生成 DOM Tree
  • 輸入:bytes
  • 輸出:DOM Tree

這個環節設計的數據流為:bytes → characters → token → nodes → object model (DOM Tree)

我們把數據流的每次扭轉進行梳理,得到以下 5 個環節:

  1. Loading:Blink 從網絡線程接收 bytes
  2. Conversion: HTMLParser 將 bytes 轉為 characters
  3. Tokenizing: 將 characters 轉為 W3C 標準的 token
  4. Lexing: 通過詞法分析將 token 轉為 Element 對象
  5. DOM construction: 使用構建好的 Element 對象構建 DOM Tree

image

Loading#

職責:Blink 從網絡線程接收 bytes。

流程:

image

Conversion#

職責:將 bytes 解析為 characters。

核心堆棧:

#0 0x00000002d2380488 in blink::HTMLDocumentParser::Append(WTF::String const&) at /Users/airing/Files/code/chromium/src/third_party/blink/renderer/core/html/parser/html_document_parser.cc:1037
#1 0x00000002cfec278c in blink::DecodedDataDocumentParser::UpdateDocument(WTF::String&) at /Users/airing/Files/code/chromium/src/third_party/blink/renderer/core/dom/decoded_data_document_parser.cc:98
#2 0x00000002cfec268c in blink::DecodedDataDocumentParser::AppendBytes(char const*, unsigned long) at /Users/airing/Files/code/chromium/src/third_party/blink/renderer/core/dom/decoded_data_document_parser.cc:71
#3 0x00000002d2382778 in blink::HTMLDocumentParser::AppendBytes(char const*, unsigned long) at /Users/airing/Files/code/chromium/src/third_party/blink/renderer/core/html/parser/html_document_parser.cc:1351

Tokenizing#

職責:將 characters 解析為 token。

核心函數:

需要注意的是,這一步中如果解析到 link、script、img 標籤時會繼續發起網絡請求;同時解析到 script 時,需要先執行完解析到的 JavaScript,才會繼續往後解析 HTML。因為 JavaScript 可能會改變 DOM 樹的結構 (如 document.write() 等),所以需要先等待它執行完。

Lexing#

職責:將 token 解析為 Element。

核心函數:

注意這一步在處理的過程中,就會使用棧結構存儲 Node (HTML Tag),以便後續構造 DOM Tree —— 例如對於 HTMLToken::StartTag 類型的 Token,就會調用 ProcessStartTag 執行一個壓棧操作,而對於HTMLToken::EndTag 類型的 Token,就會調用 ProcessEndTag 執行一個出棧操作。

如針對如下所示的 DOM Tree:

<div>
  <p>
    <div></div>
  </p>
  <span></span>
</div>

各 Node 壓榨與出棧流程如下:

image

DOM construction#

職責:將 Element 實例化為 DOM Tree。

image

最終 DOM Tree 的數據結構可以斷點從 blink::TreeScope 中預覽:

image

我們可以使用 DevTools 查看頁面的 Parsing 流程:

image

但是這個火焰圖看不到 C++ 端的棧調用情況。如果想深入查看內核端的堆棧情況,可以使用 Perfetto 進行頁面錄製與分析,它不僅能看到 C++ 端的堆棧情況,還能分析每個調用所屬的線程,以及跨進程通信時也會連線標出發出通信與接收到通信的函數調用。

image

分析完 Paring 之後,我們可以完善一下我們的流程圖:
image

Style#

image

  • 模塊:blink
  • 進程:Render Process
  • 线程:Main thread
  • 職責:Style Engine 遍歷 DOM,通過匹配 CSSOM 進行樣式分析 (resolution) 和樣式重算 (recalc) 構建出 Render Tree
  • 輸入:DOM Tree
  • 輸出:Render Tree

RenderTree 由 RenderObject 構成,每個 RenderObject 對應一個 DOM 節點上,它會在 DOM 附加 ComputedStyle (計算樣式)信息。

ComputedStyle 可以通過 DevTools 直接查看,CSS 調試時經常使用。

image

核心函數:Document::UpdateStyleAndLayout (可以先不看 Layout 的部分)

該函數的的邏輯如下圖所示,這個生成 ComputedStyle 的環節我們稱之為 style recalc(樣式計算):

image

完整的 Style 的流程如下圖所示:

image
我們可以拆成 3 個環節:

  1. CSS 加載
  2. CSS 解析
  3. CSS 計算

CSS 加載#

核心堆棧的打印:

[DocumentLoader.cpp(558)] “<!DOCType html>\n<html>\n<head>\n<link rel=\”stylesheet\” href=\”demo.css\”> \n</head>\n<body>\n<div class=\”text\”>\n <p>hello, world</p>\n</div>\n</body>\n</html>\n”
[HTMLDocumentParser.cpp(765)] “tagName: html |type: DOCTYPE|attr: |text: “
[HTMLDocumentParser.cpp(765)] “tagName: |type: Character |attr: |text: \n”
[HTMLDocumentParser.cpp(765)] “tagName: html |type: startTag |attr: |text: “

[HTMLDocumentParser.cpp(765)] “tagName: html |type: EndTag |attr: |text: “
[HTMLDocumentParser.cpp(765)] “tagName: |type: EndOfFile|attr: |text: “
[Document.cpp(1231)] readystatechange to Interactive
[CSSParserImpl.cpp(217)] recieved and parsing stylesheet: “.text{\n font-size: 20px;\n}\n.text p{\n color: #505050;\n}\n”

需要注意的是 DOM 構建之後不會立刻渲染 HTML 頁面,而是要等待 CSS 處理完畢。因為 CSS 加載完之後才會進行後續的 style recalc 等流程,如果沒有 CSS 只渲染無樣式的 DOM 是無意義的。

The browser blocks rendering until it has both the DOM and the CSSOM.  ——Render blocking CSS

CSS 解析#

CSS 解析涉及的數據流為:bytes → characters → tokens → StyleRule → RuleMap,bytes 的處理前文已經說過,不再贅述,我們重點看後續的流程。

首先是:characters → tokens。

css 涉及到的 token 有下圖這些:
image
image

需要注意的是 FunctionToken 會有額外的計算。例如,Blink 底層使用 RGBA32 來存儲 Color (CSSColor::Create)。根據我微基準測試的結果,Hex 轉換為 RGBA32 比 rgb () 的效率快 15% 左右。

第二步是:tokens → StyleRule。

StyleRules = selectors (選擇器) + properties (屬性集)。

值得注意的是 CSS 選擇器解析是從右向左

例如對於這個 CSS:

.text .hello{
    color: rgb(200, 200, 200);
    width: calc(100% - 20px);
}
#world{
    margin: 20px;
}

解析結果如下所示:

selector text = “.text .hello”
value = “hello” matchType = “Class” relation = “Descendant”
tag history selector text = “.text”
value = “text” matchType = “Class” relation = “SubSelector”
selector text = “#world”
value = “world” matchType = “Id” relation = “SubSelector”

這裡額外說一下 Blink 的默認樣式,Blink 有一套應用默認樣式的規則:加載順序為 html.css (默認樣式)→ quirk.css (怪異樣式)→ android/linux/mac.css(各操作系統樣式) → other.css(業務樣式)。

更多內置 CSS 加載順序可參考 blink_resources.grd 配置。

最後是:StyleRule → RuleMap。

所有的 StyleRule 會根據選擇器類型存儲在不同的 Map 中,這樣做的目的是為了在比較的時候能夠很快地取出匹配第一個選擇器的所有 rule,然後每條 rule 再檢查它的下一個 selector 是否匹配當前元素。

建議閱讀: blink/renderer/core/css/rule_set.h

CSS 計算#

  • 產物:ComputedStyle

image

為什麼要計算 CSS Style?因為可能會有多個選擇器的樣式命中了 DOM 節點,還需要繼承父元素的屬性以及 UA 提供的屬性。

步驟:

  1. 找到命中的選擇器
  2. 設置樣式

指的注意的是最後應用樣式的優先級順序:

  1. Cascade layers 順序
  2. 選擇器優先級順序
  3. proximity 排序
  4. 聲明位置順序

源碼:ElementRuleCollector::CompareRules

我們都知道應用樣式的優先級順序是選擇器優先級相加,但這只是裡面的第二級優先級。如果前三個優先級完全相同的情況下,最後應用的樣式會取決於樣式的聲明時機 —— 聲明靠後的優先級越大。

如圖:
image

這裡的 h1 的 class,無論寫成 main-heading 2 main-heading 還是調轉順序,標題都是藍色的,因為 .main-heading2 的聲明靠後,因此優先級更高。

Layout#

image

  • 模塊:blink
  • 進程:Render Process
  • 线程:Main thread
  • 職責:處理 Element 的幾何屬性,即位置與尺寸
  • 輸入:Render Tree
  • 輸出:Layout Tree

Layout Object 記錄了 Render Object 的幾何屬性。

image

一個 LayoutObject 附加了一個 LayoutRect 屬性,包括:

  • x
  • y
  • width
  • height

但需要注意的是,LayoutObject 與 DOM Node 並非 1:1 的關係,理由如下圖所示:

image

Layout 流程的核心函數:Document::UpdateStyleAndLayout ,經過這一步之後 DOM tree 會變成 Layout Tree,如下圖代碼:

<div style="max-width: 100px">
  <div style="float: left; padding: 1ex">F</div>
  <br>The <b>quick brown</b> fox
  <div style="margin: -60px 0 0 80px">jumps</div>
</div>

image
每個 LayoutObject 節點都記錄了位置和尺寸信息:
image
我們知道避免 Layout (reflow),可以提高頁面的性能。那么如何減少重排呢?主旨是合併多個 reflow,最後再反饋到 render tree 中。具體有以下措施:

  • 直接更改 classname 而非 style → 避免 CSSOM 重新生成與合成
  • 讓頻繁 reflow 的 Element “離線”
  • 替代會觸發 reflow 的屬性
  • 將 reflow 的影響範圍控制在單獨的圖層內

其中,會首次 / 二次觸發 Layout (reflow),Paint (repaint),Compositor 的屬性可以參考 CSS Triggers

image

可以發現每個瀏覽器內核對於屬性的處理是不一樣的,如果需要優化性能,就可以對照查看這張表格,看看有沒有 css 屬性是可以優化的。

Pre-paint#

image

  • 模塊:blink
  • 進程:Render Process
  • 线程:Main thread
  • 職責:生成 Property trees,供 Compositor thrread 使用,避免某些資源重複 Raster
  • 輸入:Layout Tree
  • 輸出:Property Tree

基於屬性樹,Chromium 可以單獨操作某個節點的變換、裁剪、特效、滾動,不至於影響它的子節點。

核心函數:

新版本 Chromium 改成了 CAP(composite after paint)模式

Property trees 包括以下四棵樹:

image

Paint#

image

  • 模塊:blink
  • 進程:Render Process
  • 线程:Main thread
  • 職責:Blink 對接 cc 的繪製接口進行 Paint,生成 cc 模塊的數據源 cc::Layer
  • 輸入:Layout Object
  • 輸出:PaintLayer (cc::Layer)

注意:cc = content collator (內容編排器),而不是 Chromium Compositor。

核心函數:

image

Paint 階段將 Layout Tree 中的 Layout Object 轉換成繪製指令,並把這些操作封裝在 cc::DisplayItemList 中,之後將其注入進 cc::PictureLayer 中。

生成 display item list 的流程也是一個棧結構的遍歷:

image

再舉一個例子,針對以下 HTML:

<style> #p {
  position: absolute; padding: 2px;
  width: 50px; height: 20px;
  left: 25px; top: 25px;
  border: 4px solid purple;
  background-color: lightgrey;
} </style>
<div id=p> pixels </div> 

對應生成的 display items 如下圖所示:

image

最後再介紹一下 cc::Layer,它運行在主線程,且一個 Render Process 內有且只有一棵 cc::Layer 樹。

一個 cc::Layer 表示一個矩形區域內的 UI,以下子類代表不同類型的 UI 數據:

  • cc::PictureLayer:用於實現自繪型的 UI 組件,它允許外部通過實現 cc::ContentLayerClient 接口提供一個 cc::DisplayItemList 對象,它表示一個繪製操作的列表,記錄了一系列的繪製操作。它經過 cc 的流水線之後轉換為一個或多個 viz::TileDrawQuad 存儲在 viz::CompositorFrame 中。  
  • cc::TextureLayer:對應 viz 中的 viz::TextureDrawQuad,所有想要使用自己的邏輯進行 Raster 的 UI 組件都可以使用這種 Layer,比如 Flash 插件,WebGL 等。  
  • cc::UIResourceLayer/cc::NinePatchLayer:類似 TextureLayer,用於軟件渲染。  
  • cc::SurfaceLayer/cc::VideoLayer (廢棄):對應 viz 中的 viz::SurfaceDrawQuad,用於嵌入其他的 CompositorFrame。Blink 中的 iframe 和視頻播放器可以使用這種 Layer 實現。  
  • cc::SolidColorLayer:用於顯示純色的 UI 組件。

Commit#

image

  • 模塊:cc
  • 進程:Render Process
  • 线程:Compositor thread
  • 職責:將 Paint 階段的產物數據 (cc::Layer 提交給 Compositor 線程
  • 輸入:cc::Layer (main thread)
  • 輸出:LayerImpl (compositor thread)

核心函數:PushPropertiesTo

image

核心邏輯是將 LayerTreeHost 的數據 commit 到 LayerTreeHostImpl,我們在接收到 Commit 消息的地方進行斷點,堆棧如下所示:

libcc.so!cc::PictureLayer::PushPropertiesTo(cc::PictureLayer * this, cc::PictureLayerImpl * base_layer)
libcc.so!cc::PushLayerPropertiesInternal<std::__Cr::__wrap_iter<cc::Layer**> >(std::__Cr::__wrap_iter<cc::Layer**> source_layers_begin, std::__Cr::__wrap_iter<cc::Layer**> source_layers_end, cc::LayerTreeHost * host_tree, cc::LayerTreeImpl * target_impl_tree)
libcc.so!cc::TreeSynchronizer::PushLayerProperties(cc::LayerTreeHost * host_tree, cc::LayerTreeImpl * impl_tree)
libcc.so!cc::LayerTreeHost::FinishCommitOnImplThread(cc::LayerTreeHost * this, cc::LayerTreeHostImpl * host_impl)
libcc.so!cc::SingleThreadProxy::DoCommit(cc::SingleThreadProxy * this)libcc.so!cc::SingleThreadProxy::ScheduledActionCommit(cc::SingleThreadProxy * this)libcc.so!cc::Scheduler::ProcessScheduledActions(cc::Scheduler * this)
libcc.so!cc::Scheduler::NotifyReadyToCommit(cc::Scheduler * this, std::__Cr::unique_ptr<cc::BeginMainFrameMetrics, std::__Cr::default_delete<cc::BeginMainFrameMetrics> > details)
libcc.so!cc::SingleThreadProxy::DoPainting
libcc.so!cc::SingleThreadProxy::BeginMainFrame(cc::SingleThreadProxy * this, const viz::BeginFrameArgs & begin_frame_args)

Compositing#

image

  • 模塊:cc
  • 進程:Render Process
  • 线程:Compositor thread
  • 職責:將整個頁面按照一定規則,分成多個獨立的圖層,便於隔離更新
  • 輸入:PaintLayer (cc::Layer)
  • 輸出:GraphicsLayer

核心函數:

image

為什麼需要 Compositor 線程?那我們假設下如果沒有這個步驟,Paint 之後直接光柵化上屏又會怎樣:

image

如果直接走光柵化上屏,如果 Raster 所需要的數據源因為各種原因,在垂直同步信號來臨時沒有準備就緒,那麼就會導致丟幀,發生 “Janky”。

當然,為了避免 Janky,Chromium 也在每個階段也做了很常規的優化 —— 緩存。如下圖所示,在 Style、Layout、Paint、Raster 階段都做了對應了緩存策略,以避免不必要的渲染,從而減少 Janky 發生的可能性:

image

但即便做了如此多的緩存優化,一個簡單的滾動會導致所有的像素重新 Paint + Raster!

image

而 Compositing 階段經過分層之後的產物 GraphicsLayer,可以讓 Chromium 在渲染時只需要操作必要的圖層,其他圖層只需要參與合成就行了,以此提高渲染效率:

如下圖所示:
image
wobble 類有個 transform 動畫,那麼這整個 div 節點就是一個獨立的 GraphicsLayer,動畫只需要渲染這部分 layer 即可。

我們也可以通過 DevTools 的圖層工具查看所有的 Layers,它會告訴我們這個圖層產生的原因是什麼、內存占用多少,至今為止繪製了多少次,以便我們進行內存與渲染效率的優化。

image

這也解答了為什麼 CSS 動畫性能表現優秀?因為有 Compositor 線程的參與,它基於 Property Trees 合成的圖層,單獨在 Compositor 線程處理 CSS 動畫。此外,我們也可以使用 will-change 去提前告知 Compositor 線程,以優化圖層合併。但這個方案也不是萬能的,每個 Layer 都會消耗一定的內存。

Compositor Thread 還具備處理輸入事件的能力,如下圖所示,它會監聽從 Browser Process 過來的各種事件:

image

但需要注意的是如果在 JavaScript 註冊了事件監聽,它會把輸入事件轉發給 main thread 進行處理。

Tiling#

image

  • 模塊:cc
  • 進程:Render Process
  • 线程:Compositor thread
  • 職責:將一個 cc::PictureLayerImpl 根據不同的 scale 級別,不同的大小拆分為多個 cc::TileTask 任務給到 Raster 線程處理。
  • 輸入:LayerImpl (compositor thread)
  • 輸出:cc::TileTask (raster thread)

圖塊(Tiling)是 Raster 的基本工作單位,這個階段中 Layer (LayerImpl) 會拆成一個個 Tiling。在 Commit 完成之後會根據需要創建 Tiles 任務 cc::RasterTaskImpl,這些任務被 Post 到 Raster 線程中執行。

核心函數:PrepareTiles
推薦閱讀:cc/tiles/tile_manager.h
image

這個環節主要是提交 cc::TileTask 任務給到 raster thread 做分塊渲染 (Tile Rendering),所謂分塊渲染就是把網頁的緩存分為一格一格的小塊,通常為 256x256 或者 512x512,然後分塊進行渲染。

分塊渲染的必要性體現在以下兩個方面:

  • GPU 合成通常是使用 OpenGL ES 貼圖實現的,這時候的緩存實際就是紋理(GL Texture),很多 GPU 對紋理的大小是有限制的。GPU 無法支持任意大小的緩存。  
  • 分塊緩存,方便瀏覽器使用統一的緩衝池來管理緩存。緩衝池的小塊緩存由所有 WebView 共用,打開網頁的時候向緩衝池申請小塊緩存,關閉網頁是這些緩存被回收。

如果說前一個環境的分層是宏觀上提升了渲染效率,那麼分塊就是微觀上提升了渲染效率。

Chromium 對分塊渲染的策略還有以下優化點:

  1. 優先繪製靠近視口的圖塊:Raster 會根據 Tiling 與可見視口的距離安排優先順序進行 Raster,離得近的會被優先 Raster,離得遠的會降級 Raster 的優先級。
  2. 在首次合成圖塊的時候,降低分辨率,以減少紋理合成和上傳的耗時。

在提交 TileTask 的位置我們斷點,可以看到該環節的完整堆棧:

libcc.so!cc::SingleThreadTaskGraphRunner::ScheduleTasks(cc::TestTaskGraphRunner * this, cc::NamespaceToken token, cc::TaskGraph * graph)
libcc.so!cc::TileTaskManagerImpl::ScheduleTasks(cc::TileTaskManagerImpl * this, cc::TaskGraph * graph)
libcc.so!cc::TileManager::ScheduleTasks(cc::TileManager * this, cc::TileManager::PrioritizedWorkToSchedule work_to_schedule)
libcc.so!cc::TileManager::PrepareTiles(cc::TileManager * this, const cc::GlobalStateThatImpactsTilePriority & state)
libcc.so!cc::LayerTreeHostImpl::PrepareTiles(cc::LayerTreeHostImpl * this)
libcc.so!cc::LayerTreeHostImpl::NotifyPendingTreeFullyPainted(cc::LayerTreeHostImpl * this)
libcc.so!cc::LayerTreeHostImpl::UpdateSyncTreeAfterCommitOrImplSideInvalidation(cc::LayerTreeHostImpl * this)
libcc.so!cc::LayerTreeHostImpl::CommitComplete(cc::LayerTreeHostImpl * this)
libcc.so!cc::SingleThreadProxy::DoCommit(cc::SingleThreadProxy * this)
libcc.so!cc::SingleThreadProxy::ScheduledActionCommit(cc::SingleThreadProxy * this)
libcc.so!cc::Scheduler::ProcessScheduledActions(cc::Scheduler * this)
libcc.so!cc::Scheduler::NotifyReadyToCommit(cc::Scheduler * this, std::__Cr::unique_ptr<cc::BeginMainFrameMetrics, std::__Cr::default_delete<cc::BeginMainFrameMetrics> > details)
libcc.so!cc::SingleThreadProxy::DoPainting(cc::SingleThreadProxy * this)
libcc.so!cc::SingleThreadProxy::BeginMainFrame(cc::SingleThreadProxy * this, const viz::BeginFrameArgs & begin_frame_args)

Raster#

image

  • 模塊:cc
  • 進程:Render Process
  • 线程:Raster thread
  • 職責:Raster 階段會執行每一個 TileTask,最終產生一個資源,記錄在產生一個資源,該資源被記錄在了 LayerImpl (cc::PictureLayerImpl) 。它會將 DisplayItemList 中的繪製操作 Playback 到 viz 的 CompositorFrame 中。
  • 輸入:cc::TileTask
  • 輸出:LayerImpl (cc::PictureLayerImpl)

推薦閱讀:cc/raster/

image

這些顏色值位圖存儲與 OpenGL 引用會在 GPU 的內存中(GPU 也可以進行栅格化,即硬件加速。)

除此之外,Raster 還包括的圖片解碼的能力:

image

Raster 的核心類 cc::RasterBufferProvider 有以下幾個關鍵子類:

GPU Shared Image#

所謂 SharedImage 機制本質上抽象了 GPU 的數據存儲能力,即允許應用直接把數據存儲到 GPU 內存中,以及直接從 GPU 中讀取數據,並且允許跨過 shared group 邊界。在早期的 Chromium 中使用的的是 Mailbox 機制,如今的模塊基本都重構為 GPU Shared Image 了。

GPU Shared Image 包括 Client 端和 Service 端,其中 Client 端可以為 Browser / Render / GPU 進程等,Client 端可以有多個;而 Service 端則只能用一個,運行在 GPU 進程。架構圖如下所示:

image

Chromium 中使用 SharedImage 機制的一些場景:

  • CC 模塊:先將畫面 Raster 到 SharedImage,然後再發送給 Viz 進行合成。
  • OffscreenCanvas:先將 Canvas 的內容 Raster 到 SharedImage,然後再發送給 Viz 進行合成。
  • 圖片處理 / 渲染:一個線程將圖片解碼到 GPU 中,另一個線程使用 GPU 來修改或者渲染圖片。
  • 視頻播放:一個線程將視頻解碼到 GPU 中,另一個線程來渲染。

光柵化策略#

根據 Compositor 和 Raster 這兩個階段是同步進行(注意同步不一定要求在同一線程)還是異步進行,分為同步光柵化和異步光柵化,而異步光柵化都是分塊進行的,因此也叫異步分塊光柵化。

同步光柵化,如 Android、iOS、Flutter 都使用的同步光柵化機制,同時它們也支持圖層分屏額外的像素緩衝區來進行間接光柵化。

同步光柵化的渲染管線很簡單,如下圖所示:

image

異步光柵化則是目前瀏覽器與 WebView 採用的策略,除卻一些特殊的圖層外(如 Canvas、Video),圖層會進行分塊光柵化,每個光柵化任務執行對應圖層的對應分塊區域內的繪圖指令,結果寫入該分塊的像素緩衝區;此外光柵化和合成不在同一線程執行,並且不是同步的,如果合成過程中某個分塊沒有完成光柵化,那它就會保留空白或者繪製一個棋盤格的圖形。

兩種光柵化策略各有優劣,大致如下表所示:

同步光柵化異步光柵化
內存占用極好極差
首屏性能一般
動態變化的內容渲染效率
圖層動畫一般慣性動畫絕對優勢
光柵化性能低端機略弱

內存占用上,同步光柵化具有絕對的優勢,而異步光柵化則很吃內存,基本上可以說瀏覽器內核的性能大部分是靠內存換出來的。

首屏性能上,同步光柵化的流水線由於更精煉,沒有複雜的調度任務,會更早實現上屏。但這個提升實際上也很有限,在首屏性能上,同步光柵化通常比起異步光柵化理論上可以提前一兩幀完成,可能就 20 毫秒。(當然,這裡異步光柵化的資源也是本地加載的。)

對於動態變化的內容,如果頁面的內容在不斷發生變化,這意味著異步光柵化的中間緩存大部分是失效的,需要重新光柵化。而由於同步光柵化流水更精煉,這部分重渲染效率也更高一些。

對於圖層動畫,是異步光柵化絕對的優勢了,前文也說了屬性樹與 Compositing,它可以控制重新渲染的圖層範圍,效率是很高的。雖然異步光柵化需要額外的分塊耗時,但是這個開銷不高,也就 2 ms 左右。如果頁面動畫特別複雜,那麼異步光柵化的優勢就能體現出來。對於慣性滾動,異步光柵化會提前對 Viewport 外的區域進行預光柵化以優化體驗。但是同步光柵化也各顯神通,如在編碼時,iOS、Android、Flutter 都會非常強調 Cell 層面的重用機制,以此來優化滾動效果。

最後是光柵化的性能上,同步光柵化對性能要求更高,因為需要大量的 CPU 計算,在低端機上容易出現持續掉幀。但是隨著手機 CPU 性能越好,同步光柵化策略的優勢就越明顯,因為對比異步光柵化有著絕對的內存優勢,且對於慣性動畫也可以通過重用機制來解決,總體優勢還是比較明顯的。

除此之外,異步光柵化也有一些無法規避的問題如快速滾動時頁面白屏、滾動過程中 DOM 更新不同步等問題。

Activate#

image

  • 模塊:cc
  • 進程:Render Process
  • 线程:Compositor thread
  • 職責:實現一個緩衝機制,確保 Draw 階段操作前 Raster 的數據已準備好。具體而言將 Layer Tree 分成 Pending TreeActive Tree,從 Pending Tree 拷貝 Layer 到 Activate Tree 的過程就是 Activate。

核心函數:LayerTreeHostImpl::ActivateSyncTree

image

Compositor thread 有三棵 cc::LayerImpl 樹:

  1. Pending tree: 負責接收 commit,然後將 LayerImpl 進行 Raster
  2. Active tree: 會從這裡取出栅格化好的 LayerImpl 進行 Draw 操作
  3. Recycle tree:為避免頻繁創建 LayerImpl 對象,Pending tree 後續不會被銷毀,而是退化成 Recycle tree。
// Tree currently being drawn.
std::unique_ptr<LayerTreeImpl> active_tree_;

// In impl-side painting mode, tree with possibly incomplete rasterized
// content. May be promoted to active by ActivateSyncTree().
std::unique_ptr<LayerTreeImpl> pending_tree_;

// In impl-side painting mode, inert tree with layers that can be recycled
// by the next sync from the main thread.
std::unique_ptr<LayerTreeImpl> recycle_tree_;

Commit 階段提交的目標其實就是 Pending 樹,Raster 的結果也被存儲在了 Pending 樹中。通過 Active 可以實現一邊從最新的提交中光柵化圖塊,一邊上屏繪製之前的提交。

Draw#

image

該階段也可以叫做 Submit,本文中統一術語就叫 Draw。

  • 模塊:cc
  • 進程:Render Process
  • 线程:Compositor thread
  • 職責:將 Raster 後圖塊 (Tiling) 生成为 draw quads 的過程。
  • 輸入:cc::LayerImpl (Tiling)
  • 輸出:viz::DrawQuad

Draw 階段並不執行真正的繪製,而是遍歷 Active Tree 中的 cc::LayerImpl 對象,並調用它的 cc::LayerImpl::AppendQuads 方法創建合適的 viz::DrawQuad 放入 CompositorFrame 的 RenderPass 中。

核心函數:

image

Viz#

這裡先介紹一下 Chromium 上屏的重要模塊 —— viz。

viz = visuals

在 Chromium 中 viz 的核心邏輯運行在 Viz Process 中,負責接收其他進程產生的 viz::CompositorFrame(簡稱 CF),然後把這些 CF 進行合成,並將合成的結果最終渲染在窗口上。

viz 模塊的核心類如下圖所示:

image

一個 CF 對象表示一個矩形顯示區域中的一幀畫面, viz::CompositorFrame 內部存儲了以下幾類數據:

  1. 元數據:CompositorFrameMetadata
  2. 引用到的資源:TransferableResource
  3. 繪製操作:RenderPass/DrawQuad

元數據 viz::CompositorFrameMetadata 記錄了 CF 相關的元數據,比如畫面的縮放級別,滾動區域,引用到的 Surface 等:

引用到的資源 viz::TransferableResource 記錄了該 CF 引用到的資源,所謂的資源可以理解為一張圖片。資源有兩種存在形式:

  1. 存儲在內存中的 Software 資源
  2. 存儲在 GPU 中的 Texture

如果沒有開啟硬件加速渲染,則只能使用 Software 資源;而如果開啟了硬件加速,則只能使用硬件加速的資源。

CF 的繪製操作 viz::RenderPass 由一系列相關的 viz::DrawQuad 組成。可以對一個 RenderPass 單獨應用特效,變換,mipmap,緩存,截圖等。DrawQuad 有很多種類型:

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。