首先,回顧一下,上一篇文章的内容,我們首先介紹了content概念,知道了我們最終要的結果是顯示在螢幕上的像素,了解了浏覽器渲染的目标【html/css/js 轉換到 正确的opengl調用來調整像素的樣式】,了解了Style,了解了Layout,了解了Panit,現在所有的繪制操作已經記錄在顯示項清單(display items)中了,那麼現在就可以開始本篇文章的閱讀。
讀前提要
因為浏覽器渲染機制的兩篇文章是基于前面資料中的ppt為出發點研究,是以文章的順序沒有按照完整的渲染順序編寫,讀起來體驗應該比較一般,文章中也可能存在或多或少的問題,但是我會在後續進行修正,并梳理出一篇清晰易懂的文章。
raster光栅化
顯示項清單中的繪制操作由一個稱為光栅化的程序執行。光栅化可以将顯示項清單轉換成顔色值的位圖。生成的位圖中每個單元都儲存着這個位圖的顔色值與透明度的編碼(如下圖FFFFFFF,其實就是RGBA的16進制表示)。

光栅化這個過程還會去解碼嵌入在頁面中的圖像資源,繪制操作會引用壓縮的資料(比如JPEG,PNG等等),而光栅化會調用适當的解碼器對其進行适當的解壓。
- 過去的GPU隻是作為一個記憶體,這些記憶體被OpenGL紋理對象引用,我們會将栅格化的位圖放到主記憶體裡,然後上傳到GPU,用來分擔記憶體壓力。
- 現在的GPU也可以執行生成位圖的指令(“加速光栅化”),這屬于硬體加速,但是無論是硬體光栅化還是軟體光栅化,本質都是生成了某種記憶體中像素的位圖。
現在僅僅是位圖存儲在了記憶體中,像素還沒有顯示在螢幕上。GPU光栅化并不是直接調用的GPU,而是通過Skia圖形庫(谷歌維護的2D圖形庫,在Android,Flutter,Chromium都有使用)。
Skia提供了某種抽象層,可以了解更加複雜的東西,比如貝塞爾曲線。Skia是開源的,裝載在Chrome二進制檔案中,而不是存在于一個單獨的代碼庫中。當需要光栅化的顯示項(display item)時,會先去調用SkCanvas上面的方法,他是Skia的調用入口,SkCanvas提供了Skia内部更多的抽象,在硬體加速的時候,它會建構另一個繪圖操作緩沖區,然後對其進行重新整理,在栅格化任務結束時,通過flush操作,我們獲得了真正的GL指令,GL指令運作在GPU程序中。
Skia和GL指令可以運作在不同的程序中,也可以運作在同一個程序,于是産生了兩種調用方式。
- In Process Raster
- Out of Process Raster
1.In Process Raster
老版本采用了這種方式,Skia是在渲染程序中執行的,他會生産GL調用指令,GPU有單獨的GPU process,這種模式下Skia沒辦法直接進行渲染系統的調用。在初始化Skia的時候,會給它一個函數指針表(這個指針指向了GL API,但不是真正的OpenGL API,而是Chromium提供的代理)。下面的GpuChannelMsg_FlushCommandBuffers是一個指令緩沖區,會将函數指針表轉換成真正的OpenGl API。
單獨的GPU程序有利于隔離GL操作,提升穩定性和安全性,這種模式也成為沙箱機制(就是把不安全操作放在單獨的程序去執行)。
(圖中 GLES2後端映射到桌面的OpenGL 2.1)
2.Out of Process Raster
新版本把繪制的操作都放在了GPU程序裡面,在GPU程序中去運作Skia,可以提升性能。
光栅化的繪制操作包裝在GPU的指令緩沖區在發送到IPC通道(程序間通信方式)。
接下來就是去執行GL指令,GL指令一般是由底層的so庫提供的,在Windows平台還會被轉換成DirectX(微軟的圖形API,用于圖形加速)
問題
現在我們從content 一點一點的講到了如何轉換到記憶體裡面的像素,但是頁面的呈現并不是一個靜态的過程(頁面滾動,js腳本執行,動畫等等。。。),在發生變化的時候去重新運作整個管道代價是十分昂貴的。
那麼我們如何去提高性能呢???
Compositing Update
在Layout操作完成之後,按理是去進行Paint,但是我們直接Paint代價是十分昂貴的,于是引入了一個圖層合成加速的概念。
那麼什麼是圖層合成加速?
圖層合成加速是把整個頁面按照一定規則劃分成多個圖層,在渲染的時候,隻要操作必要的圖層,其他的圖層隻要參與合成就好了,以這種方式提高了渲染的效率,完成這個工作的線程叫做:Compositor Thread(合成器線程),合成器線程還具備處理事件輸入的能力,比如滾動事件,但是如果在js中進行了事件的注冊和監聽,它會把輸入事件轉發給主線程。
主線程把頁面拆分成多個可以獨立光栅化的層,并在另一個線程(合成器線程)中将這些層合并。
這使得某些RenderLayer擁有自己獨立的緩存,它們被稱作合成圖層(Compositing Layer),核心會為這些RenderLayer建立對應的GraphicsLayer。
- 擁有自己的GraphicsLayer的RenderLayer在繪制的時候會繪制在自己的緩存裡面
- 沒有自己的GraphicsLayer的RenderLayer會向上查找父節點的GraphicsLayer,直到RootRenderLayer(他總是有自己的GraphicsLayer)為止,然後繪制在有GraphicsLayer的父節點的緩存裡面。
這樣就形成了與RenderLayer Tree對應的GraphicsLayer Tree。當Layer的内容發生變化時,隻需要更新對應的GraphicsLayer即可,而單一緩存架構下,就會更新整個圖層,會比較耗時。這樣就提高了渲染的效率。但是過多的GraphicsLayer也會帶來記憶體的消耗,雖然減少了不必要的繪制,但也可能因為記憶體問題導緻整體的渲染性能下降。因而圖層合成加速追求的是一個動态的平衡。
圖層化的決策目前是由Blink來負責,根據DOM樹生成一個圖層樹,并以DisplayList記錄每個圖層的内容。
了解了圖層合成加速的概念以後,我們再來看看發生在Layout操作之後的Compositing update(合成更新),合成更新就是為特定的RenderLayer建立GraphicsLayer的過程,如下所示:
Prepaint
屬性樹:在描述屬性的層次結構這一塊,之前的方式是使用圖層樹的方式,如果父圖層具有矩陣變換(平移、縮放或者透視)、裁剪或者特效(濾鏡等),需要遞歸的應用到子節點,時間複雜度是O(圖層數),這在極端情況下會有性能問題。
于是,為了提高性能,引入了屬性樹的概念,合成器提供了變換樹,裁剪樹,特效樹等。每個圖層都有若幹節點id,分别對應不同屬性樹的矩陣變換節點、裁剪節點和特效節點。這樣的時間複雜度就是O(要變化的節點),如下所示:
prepaint的過程其實就是建構屬性樹的過程。
commit
前面提到了我們在paint這個階段前,多了兩件事情要做,先把頁面拆成了很多圖層,還建構了屬性樹。paint階段做了什麼?paint階段把繪制的操作放在了display item裡面。
下面就來到了commit階段,這個階段會更新圖層和屬性樹的副本到合成器線程。
Tiling
合成器線程接收到資料之後,不會立即開始合成,而是把圖層進行分塊,這裡涉及到了一個叫做“分塊渲染”的技術,分塊渲染會将網頁的緩存分成一塊一塊的,比如256*256的塊,之後進行分塊渲染。
為什麼要分塊渲染?
- .GPU合成通常是使用OpenGL ES貼圖實作的,這時候的緩存實際就是紋理(GL Texture),很多GPU對紋理的大小是有限制的,比如長寬必須是2的幂次方,最大不能超過2048或者4096等。無法支援任意大小的緩存。
- 分塊緩存,友善浏覽器使用統一的緩沖池來管理緩存。緩沖池的小塊緩存由所有WebView共用,打開網頁的時候向緩沖池申請小塊緩存,關閉網頁是這些緩存被回收。
tiling圖塊也是栅格化的基本機關,栅格化會根據圖塊與可見視口的距離安排優先順序進行栅格化。離得近的會被優先栅格化,離得遠的會降級栅格化的優先級。這些圖塊拼接在一起,就形成了一個圖層,如下所示:
Activate
在Commit之後,Draw之前有一個Activate操作。Raster和Draw都發生在合成器線程裡的Layer Tree上,但是我們知道Raster操作是異步的,有可能需要執行Draw操作的時候,Raster操作還沒完成,這個時候就需要解決這個問題。
它将LayerTree分為了
- PendingTree:負責接收commit,然後将Layer進行Raster操作
- ActiveTree:會從這裡取出光栅化好的Layer進行draw操作。
主線程的圖層樹由LayerTreeHost擁有,每個圖層以遞歸的方式擁有其子圖層。Pending樹、Active樹、Recycle樹都是LayerTreeHostImpl擁有的執行個體。這些樹被定義在cc/trees目錄下。之是以稱之為樹,是因為早期它們是基于樹結構實作的,目前的實作方式是清單。
Draw
好,現在到了Draw這個步驟,當每個圖塊都被光栅化之後,合成器線程會為每個圖塊生成draw quads(在螢幕的指定位置繪制圖塊的指令,也包含了屬性樹裡面的變換,特效等操作),這些draw quads會被封裝在CompositorFrame對象裡面,CompositorFrame對象也是Render Process的産物,它會被送出到Gpu Process中,我們平時提到的60fps輸出幀率指的幀其實就是CompositorFrame。
Draw指的就是 把光栅化的圖塊,轉換成draw quads的過程。
Display Compositor
Display
Draw操作完成之後,就生成了CompositorFrame,它會被輸出到Gpu process,它會從多個來源的Render Process接收CompositorFrame。
多個來源:
- Browser Process也有自己的Compositor來生成Compositor Frame,這些一般是用來繪制Browser UI(導航欄,視窗等)
- 每次建立tab或者使用iframe,會建立一個獨立的Render Process。
Display Compositor運作在Viz Compositor thread,Viz會調用OpenGL指令來渲染Compositor Frame裡面的draw quads,把像素點輸出到螢幕上。
VIz也是雙緩沖輸出的,它會在背景緩沖區繪制draw quads,然後執行交換指令最終讓它們顯示在螢幕上。
雙緩沖機制:
在渲染的過程中,如果隻對一塊緩沖區進行讀寫,這樣會導緻一方面螢幕要等待去讀,而GPU要等待去寫,這樣要造成性能低下。一個很自然的想法是把讀寫分開,分為:
- 前台緩沖區(Front Buffer):螢幕負責從前台緩沖區讀取幀資料進行輸出顯示。
- 背景緩沖區(Back Buffer):GPU負責向背景緩沖區寫入幀資料。
這兩個緩沖區并不會直接進行資料拷貝(性能問題),而是在背景緩沖區寫入完成,前台緩沖區讀出完成,直接進行指針交換,前台變背景,背景變前台,那麼什麼時候進行交換呢,如果背景緩存區已經準備好,螢幕還沒有處理完前台緩沖區,這樣就會有問題,顯然這個時候需要等螢幕處理完成。螢幕處理完成以後(掃描完螢幕),裝置需要重新回到第一行開始新的重新整理,這期間有個間隔(Vertical Blank Interval),這個時機就是進行互動的時機。這個操作也被稱為垂直同步(VSync)。
垂直同步也會在後續進行介紹。
viz
viz是做什麼的?
在 Chromium 中
viz
的核心邏輯運作在 GPU 程序中,負責接收其他程序(渲染程序?)産生的
viz::CompositorFrame(簡稱 CF)
,然後把這些 CF 進行合成,并将合成的結果最終渲染在視窗上。
CF是什麼?
一個 CF 對象表示一個矩形顯示區域中的一幀畫面。内部存儲了 3 類資料,分别是 CompositorFrameMetadata, TransferableResoruce 和 RenderPass/DrawQuad,如下圖所示:
- CompositorFrameMetadata:CF的中繼資料,比如畫面的縮放級别,滾動區域。。。
- TransferableResoruce :CF引用到的資源。
- RenderPass/DrawQuad:CF包含的繪制操作。
由一系列相關的viz::RenderPass
構成。viz::DrawQuad
CF 是
viz
中的核心資料結構,它代表某塊區域中UI的一幀畫面,使用 DrawQuad 來存儲 UI 要顯示的内容。它代表了 viz 運作時的資料流。
CF合成
CF 合成指的是viz線程把 CF 中的内容或者多個 CF 合成到一起,形成一個完整的畫面。
CF渲染
CF 的渲染主要由
viz::DirectRenderer
和
viz::OutputSurface
負責,他們将合成的結果渲染到程式選擇的渲染目标上去。
CC
現在我們在統一的梳理一下cc的功能作用以及cc的工作流程。
我們先回顧一下,看看上面這個圖,Blink 進行了DOM,Style,Layout,comp.assign,prepaint,paint。
我們可以發現,Paint是blink和cc對接的橋梁。
整體的流程其實可以 了解成:
Blink一頓操作 -> paint生成了cc子產品的資料源(cc:layer)->commit->(Tiling->)Raster->Active->draw(submit)->Viz(呈像)
就是Blink一頓操作,并在paint階段生成cc的資料源,cc進行一系列操作并最終在draw階段将結果(CF)送出給viz。也就是說,Blink負責網頁内容繪制,cc負責将繪制的結果合成并送出給viz。
cc的架構設計
cc的設計相對比較簡單,我們可以把他了解成一個多線程排程的異步流水線,運作在 Browser 程序中的 cc 負責合成浏覽器非網頁部分的 UI,運作在 Renderer 程序中的 cc 負責網頁的合成。
在chromium.googlesource.com/chromium/sr… how cc works官網有這樣一張圖,我覺得可以很好的反應cc的核心邏輯。
cc的多線程展現在在不同的階段,cc運作在不同線程中,
Paint
運作在 Main 線程,
Commit
,
Activate
,
Submit
運作在 Compositor 線程,而
Raster
運作在專門的 Raster 線程。
下面我們開始對cc流水線的各個階段進行分析。
Paint
Paint階段會産生cc的資料源(cc:layer樹),一個cc:layer會表示一個矩形區域的UI,它有很多子類,用于存儲不同類型的UI資料:
- cc::PictureLayer。用于實作自繪型的UI元件,比如上層的各種 Button,Label 等都可以用它來實作。它允許外部通過實作
接口提供一個cc::ContentLayerClient
對象,它表示一個繪制操作的清單,記錄了一系列的繪制操作,比如畫線,畫矩形,畫圓等。通過cc::DisplayItemList
接口可以友善的建立複雜的自繪 UI。cc::PaintCanvas
還是唯一需要 Raster 的cc::PictureLayer
。它經過 cc 的流水線之後轉換為一個或多個cc::Layer
存儲在viz::TileDrawQuad
中。viz::CompositorFrame
-
對應 viz 中的cc::TextureLayer
,所有想要使用自己的邏輯進行 Raster 的 UI 元件都可以使用這種 Layer,比如 Flash 插件,WebGL等。viz::TextureDrawQuad
-
對應 viz 中的cc::SurfaceLayer
,用于嵌入其他的 CompositorFrame。Blink 中的 iframe 和視訊播放器可以使用這種 Layer 實作。viz::SurfaceDrawQuad
-
用于顯示純色的 UI 元件。cc::SolidColorLayer
-
以前用于專門顯示視訊,被 SurfaceLayer 取代。cc::VideoLayer
-
類似 TextureLayer,用于軟體渲染。cc::UIResourceLayer/cc::NinePatchLayer
Blink
通過以上各種
cc::Layer
來描述 UI 并實作和 cc 的對接。由于
cc::Layer
本身可以儲存 Child
cc::Layer
,是以給定一個 Layer 對象,它實際上表示一棵
cc::Layer
樹,這個 Layer 樹即主線程 Layer 樹,因為它運作在主線程中,并且主線程有且隻有一棵
cc::Layer
樹。
Commit
Commit 階段的核心作用是将儲存在
cc::Layer
中的資料送出到
cc::LayerImpl
中。
cc::LayerImpl
和
cc::Layer
一一對應,隻不過運作在 Compositor 線程中(也稱為 Impl 線程)。在 Commit 完成之後會根據需要建立 Tiles 任務,這些任務被 Post 到 Raster 線程中執行。
Tiling+Raster
在 Commit 階段建立的 Tiles 任務(
cc::RasterTaskImpl
)在該階段被執行。Tiling 階段最重要的作用是将一個
cc::PictureLayerImpl
根據不同的 scale 級别,不同的大小拆分為多個
cc::TileTask
任務。Raster 階段會執行每一個 TileTask,将 DisplayItemList 中的繪制操作 Playback 到 viz 的資源中。 由于 Raster 比較耗時,屬于渲染的性能敏感路徑,是以Chromium在這裡實作了多種政策以适應不同的情況。這些政策主要在兩方面進行優化,一方面是 Raster 結果(也就是資源)存儲的位置,一方面是 Raster 中 Playback 的方式。這些方案被封裝在了
cc::RasterBufferProvider
的子類中,下面一一進行介紹:
-
使用 GPU 進行 Raster,Raster 的結果直接存儲在 SharedImage 中。(前文以及提到過的硬體加速)cc::GpuRasterBufferProvider
-
使用 Skia 進行 Raster,結果先儲存到 GpuMemoryBuffer 中,然後再将 GpuMemoryBuffer 中的資料通過 CopySubTexture 拷貝到資源的 SharedImage 中。GpuMemeoryBuffer 在不同平台有不同的實作,也并不是所有的平台都支援,在 Linux 平台上底層實作為 Native Pixmap(來自X11中的概念),在 Windows 平台上底層實作為 DXGI,在 Android 上底層實作為 AndroidHardwareBuffer,在 Mac 上底層實作為 IOSurface。cc::OneCopyRasterBufferProvider
-
使用 Skia 進行 Raster,結果儲存到 GpuMemoryBuffer 中,然後使用 GpuMemoryBuffer 直接建立 SharedImage。cc::ZeroCopyRasterBufferProvider
-
使用 Skia 進行 Raster,結果儲存到共享記憶體中。cc::BitmapRasterBufferProvider
Raster最終會産生一個資源,這個資源被記錄在了cc:
PictureLayerImpl
中,他們會在Draw階段被放在CF中。
Activate
在 Impl 端有三個
cc::LayerImpl
樹,分别是 Pending,Active,Recycle 樹。Commit 階段送出的目标其實就是 Pending 樹,Raster 的結果也被存儲在了 Pending 樹中。
在 Activate 階段,Pending 樹中的所有
cc::LayerImpl
會被複制到 Active 樹中,為了避免頻繁的建立
cc::LayerImpl
對象,此時 Pending 樹并不會被銷毀,而是退化為 Recycle 樹。