Webkit 渲染基礎與硬件加速

Webkit 渲染基礎與硬件加速

當瀏覽器加載一個 html 文件並對它進行解析完畢後,內核就會生成一個極爲重要的數據結構即 DOM 樹,樹上每個節點都對應着網頁裏面的某一個元素,而且開發人員也能夠經過 JavaScript 操做這棵 DOM 樹動態改變它的結構,可是 DOM 樹自己並不能直接用於排版和渲染。html

瀏覽器中頁面的渲染過程能夠簡化爲如下五個步驟:web

image_1c82b7ng61nkqhsktmm19t5hsnm.png-59.6kB

從 DOM 到 RenderObject

在 DOM 樹構建完成以後,Webkit 所要作的事情就是爲 DOM 樹節點構建 RenderObject 樹,一個 RenderObject 對象保存爲繪製 DOM 節點所須要的各類信息,從如下這些規則出發會爲 DOM 樹節點建立 RenderObject 對象:canvas

  • DOM 樹的 document 節點
  • DOM 樹中的可視節點
  • 某些狀況下 Webkit 須要創建匿名的 RenderObject 節點,這樣的節點不對應於 DOM 樹中的任何節點,而是 Webkit 處理上的須要,典型的例子就是匿名用於表示塊元素的 RenderBlock 節點
  • 雖然 Javascript 沒法訪問影子節點,可是須要爲其建立並渲染 RenderObject
Tip:因而可知,網上有人說瀏覽器渲染的步驟中包含「將 DOM 樹和 CSSOM 樹合併爲 Render 樹」的說法是有些問題的。 CSSOM(CSS 對象模型)是用於提供方法可讓開發者自定義一些腳原本操做其樣式狀態的,它的思想是在 DOM 中的一些節點接口中加入獲取和操做 CSS 屬性或者接口的 Javascript 接口,便於 Javascript 能夠動態操做 CSS 樣式。

由此咱們能夠知道 RenderObject 樹和 DOM 樹不是一一對應的!咱們能夠簡單的認爲,RenderObject 是銜接瀏覽器排版引擎和渲染引擎之間的橋樑,它是排版引擎的輸出和渲染引擎的輸入。當 Webkit 建立 RenderObject 對象以後,每一個對象是不知道本身的位置、大小等信息的,Webkit 根據框模型來計算它們的位置、大小等信息的過程稱爲佈局計算後端

Tip:從整個網頁的加載和渲染過程來看 ,CSS 解釋器和規則匹配處於 DOM 樹創建以後,RenderObject 樹創建以前,CSS 解釋器解釋後的結果會保存起來,而後 RenderObject 樹基於該結果來進行規範匹配和佈局計算。

既然已經實現繪製每一個 DOM 節點的方法,那是否是能夠開闢一段位圖空間,而後 DFS 遍歷這個 RenderObject 樹執行繪製方法,就像「蓋章」同樣把每一個 RenderObject 的內容一個個的蓋到「畫布上」,是否是就足夠完成繪製?數組

若是沒有層疊上下文,到這兒就能夠結束了!

其實是若是沒有 Positioning,Clipping,Overflow Scroll,CSS Transform/Opacity/Animation/Filter,Mask or Reflection,Z Indexing etc. 到這兒就能夠結束了……瀏覽器

從 RenderObject 到 RenderLayer

Webkit 會爲網頁的層次建立相應的 RenderLayer 對象,當某些類型的 RenderObject 的節點或者具備某些 CSS 樣式的 RenderObject 節點出現的時候,Webkit 就會爲這些節點建立 RenderLayer 對象,通常來講某個 RenderObject 節點的後代都屬於該節點的 RenderLayer,除非 Webkit 根據規則爲某個後代 RenderObject 節點建立一個新的 RenderLayer 對象,如下是 RenderObject 節點須要創建新的 RenderLayer 節點的規則:緩存

  • DOM 樹的 document 節點對應的 RenderView 節點
  • DOM 樹中 document 的子女節點,即 html 節點對應的 RenderBlock 節點
  • 顯示指定 CSS 位置的 RendrObject 節點
  • 有透明效果的 RenderObject 節點
  • 節點有溢出(overflow)、alpha 或者反射等效果的 RenderObject 節點
  • 適用 canvas 2d 或者 3d(WebGL)技術的 RenderObject 節點
  • video 節點對應的 RenderObject 節點
由此咱們能夠知道,RenderLayer 節點和 RenderObject 節點不是一一對應的,而是一對多的關係。

具體來講,根據建立 RenderLayer 的緣由不一樣能夠將其分爲常見的 3 類:數據結構

NormalPaintLayer

  • 根元素(html)
  • 有明確的定位屬性(relative、fixed、sticky、absolute)
  • 透明的(opacity 小於 1)
  • 有 CSS 濾鏡(fliter)
  • 有 CSS mask 屬性
  • 有 CSS mix-blend-mode 屬性(不爲 normal)
  • 有 CSS transform 屬性(不爲 none)
  • backface-visibility 屬性爲 hidden
  • 有 CSS reflection 屬性
  • 有 CSS column-count 屬性(不爲 auto)
  • 有 CSS column-width 屬性(不爲 auto)
  • 當前有對於 opacity、transform、fliter、backdrop-filter 的應用動畫

OverflowClipPaintLayer

  • overflow 不爲 visible

NoPaintLayer

  • 不須要 paint 的 RenderLayer:好比一個沒有視覺屬性(背景、顏色、陰影等)的空 div

上文中講解的從 DOM 到 RenderObject 以及從 RenderObject 到 RenderLayer 能夠概括以下圖:多線程

image_1c82ab03815321e7p1ugc7m419.png-89.1kB

軟件渲染和硬件加速渲染

在 Webkit 中繪圖操做被定義爲一個抽象層即繪圖上下文,全部繪圖操做都是在該上下文中進行,能夠分爲兩種類型:2d 圖形上下文和 3d 圖形上下文。其中 2d 圖形上下文的具體做用就是提供基本繪圖單元的繪製接口以及設置繪圖的樣式,繪圖接口包括畫點、畫線、畫圖片、畫多邊形、畫文字 etc.,繪圖樣式包括顏色、線寬、字號大小、漸變 etc.,而RenderObject 對象知道本身須要畫什麼樣的點,什麼樣的圖片。3d 繪圖上下文的主要用處是支持 CSS3D、WebGL etc.。架構

網頁的渲染方式主要有兩種:軟件渲染和硬件加速渲染。每一個 RenderLyaer 對象均可以被想象成一個層,各個層一同構成一個圖像,在渲染過程當中,每一個層對應網頁中的一個或者一些可視元素,這些元素都繪製內容到該層上,若是這些繪圖操做由 CPU 萊完成則稱之爲軟件繪圖,若是這些繪圖操做由 GPU 來完成則稱之爲硬件加速繪圖。理想狀況下,每一個層都有繪製的存儲區域來保存繪圖的結果,最後須要將這些層的內容合併到同一個圖像中的過程稱爲合成(compositing),使用合成技術的渲染叫作合成化渲染

對於軟件渲染機制,Webkit 須要使用 CPU 來繪製每層的內容,然而該機制是沒有合成階段的:在軟件渲染中一般其結果就是一個位圖(Bitmap),繪製每一層時都使用同一個位圖,區別在於繪製的位置看你不同,每一層都按照從後到前的順序。而使用合成化的渲染技術,以使用軟件繪圖的合成化渲染爲例,對於使用 CPU 繪製的層,其結果保存在 CPU 內存中,以後傳輸到 GPU 中進行合成。

對於常見的 2d 繪圖操做,使用 GPU 來繪圖不必定比使用 CPU 繪圖在性能上有優點,緣由是 CPU 使用緩存機制有效減小重複繪製得開銷,並且不須要 GPU 的並行,而且 GPU 的內存資源相對 CPU 的內存資源更加緊張。

什麼是位圖

image_1c84k71cv1uo61vhafc9b6b1kh7m.png-37.1kB

在繪製出一個圖片咱們應該怎麼作,顯然首先是把這個圖片表示爲一種計算機能理解的數據結構:用一個二維數組,數組的每一個元素記錄這個圖片中的每個像素的具體顏色。因此瀏覽器能夠用位圖來記錄它想在某個區域繪製的內容,繪製的過程也就是往數組中具體的下標裏填寫像素而已。

什麼是紋理

紋理其實就是 GPU 中的位圖,存儲在 GPU video RAM 中。前面說的位圖裏的元素存什麼咱們本身定義好就行(是用3字節存256位rgb仍是1個bit存黑白本身定義便可),但紋理是 GPU 專用的,須要有固定格式便於兼容與處理,因此一方面紋理的格式比較固定,如 R5G6B五、A4R4G4B4 等像素格式, 另一方面 GPU 對紋理的大小有限制,好比長/寬必須是2的冪次方,最大不能超過2048或者4096等。

什麼是光柵化

image_1c84kb5l6vk81jmhr8a5d0a6013.png-115.4kB

在紋理裏填充像素不是那麼簡單的本身去遍歷位圖裏的每一個元素而後填寫這個像素的顏色便可,光柵化的本質是座標變換、幾何離散化,而後再填充

同時,光柵化從早期的 Full-screen Rasterization 基本都進化到如今的 Tile-Based Rasterization,也就是不對整個圖像作光柵化,而是把圖像分塊後再對每一個 tile 單獨光柵化。光柵化完成後將像素填充進紋理,再將紋理上傳至 GPU,
緣由一方面如上文所說,紋理大小有限制,即便整屏光柵化也是要填進小塊小塊的紋理中,不如事先根據紋理大小分塊光柵化後再填充進紋理裏;另外一方面是爲了減小內存佔用(整屏光柵化意味着須要準備更大的buffer空間)和下降整體延遲(分塊柵格化意味着能夠多線程並行處理)。

每秒60幀的動效裏,每次變更都重繪整個位圖是很恐怖的性能開銷!

非合成加速的渲染架構,全部的 RenderLayer 都沒有本身獨立的緩存,它們都按照前後順序被繪製到同一個緩存裏面,因此只要這個 RenderLayer 觸發重繪,變化區域的緩存就須要從新生成,此時不但須要繪製發生變化的 RenderLayer,跟變化區域(Damage Region)相交的其它 RenderLayer 也須要被繪製。

瀏覽器自己並不能直接改變屏幕的像素輸出,它須要經過系統自己的 GUI Toolkit,因此通常來講瀏覽器會將一個要顯示的網頁包裝成一個 UI 組件,一般叫作 WebView,而後經過將 WebView 放置於應用的 UI 界面上,從而將網頁顯示在屏幕上。

默認的狀況下 UI 組件沒有本身獨立的位圖緩存,構成 UI 界面的全部 UI 組件都直接繪製在當前的窗口緩存上,因此 WebView 每次繪製就至關於將它在可見區域內的 RenderLayer/RenderObject 逐個繪製到窗口緩存上。上述的渲染方式有一個很嚴重的問題,用戶拖動網頁或者觸發一個慣性滾動時,網頁滑動的渲染性能會十分糟糕:這是由於即便網頁只移動一個像素,整個 WebView 都須要從新繪製。

要提高網頁滑屏的性能,一個簡單的作法就是讓 WebView 自己持有一塊獨立的緩存,而 WebView 的繪製就分紅了兩步: 1) 根據須要更新內部緩存,將網頁內容繪製到內部緩存裏面 2) 將內部緩存拷貝到窗口緩存上。第一步咱們一般稱爲繪製(Paint)或者光柵化(Rasterization),它將一些繪圖指令轉換成真正的像素顏色值,而第二步咱們通常稱爲合成(Composite),它負責緩存的拷貝,同時還可能包括位移(Translation),縮放(Scale),旋轉(Rotation),Alpha 混合等操做。

從 RenderLayer 到 GraphicsLayer

在現實狀況中,因爲硬件能力和資源有限,爲了節省 GPU 的內存資源,硬件加速機制在 RenderLayer 樹創建以後須要作三件事情來完成網頁的渲染:

  1. Webkit 決定將哪些 RendeLayer 對象組合在一塊兒,造成一個由後端存儲(通常指 GPU 內存)的新層,對於一個 RenderLayer 對象來講,若是它沒有後端存儲的新層,那麼就使用其父親所使用的合成層
  2. 將每一個合成層包含的 RenderLayer 內容繪製在其後端存儲中,這裏的繪製能夠是軟件繪製,也能夠是硬件加速繪製
  3. 由合成器將多個合成層合成起來,造成網頁的最終可視化結果(實際就是一張圖片)

一個 RenderLayer 對象若是須要後端存儲,它會建立一個 RenderLayerBacking 對象,負責 RenderLayer 對象所須要的各類存儲,每一個 RenderLayer 對象均可以建立本身的後端存儲,然而不是全部 RenderLayer 對象都有本身的 RenderLayerBacking,若是一個 RenderLayer 對象被 Webkit 按照必定的規則建立後端存儲,那麼該層被稱爲合成層,後端存儲可能須要管理多個存儲空間,使用 GraphicsLayer 類來表示。

每一個 GraphicsLayer 都擁有一個 GraphicsContext,用於爲該 GraphicsLayer 開闢一段位圖,也就意味着每一個 GraphicsLayer 都擁有一個獨立的位圖,GraphicsLayer 負責將本身的 RenderLayer 及其所包含的 RenderObject 繪製到位圖裏,而後將位圖做爲紋理交給 GPU 進行合成。若是一個 RenderLayer 對象具備如下特徵之一,那麼它就是合成層:

  • RenderLayer 具備 CSS3D 屬性或者 CSS 透視效果
  • RenderLayer 包含 video 節點對應的 RenderObject 節點
  • RenderLayer 包含使用 canvas 2d 或者 3d(WebGL)技術的 RenderObject 節點
  • RenderLayer 使用 CSS 透明效果的動畫或者 CSS 變換動畫
  • RenderLayer 使用硬件加速的 CSS Filters 技術
  • RenderLayer 使用裁剪或者反射屬性,而且其後代包含合成層
  • RenderLayer 有一個 Z 座標比本身小的兄弟節點,且該兄弟節點是一個合成層

直接緣由

  • 硬件加速的 iframe 元素(好比 iframe 嵌入的頁面中有合成層)
  • video 元素
  • 覆蓋在 video 元素上的視頻控制欄
  • 3D 或者 硬件加速的 2D Canvas 元素
  • 硬件加速的插件:好比 flash etc.
  • 在 DPI 較高的屏幕上 fix 定位的元素會自動地被提高到合成層中;但在 DPI 較低的設備上卻並不是如此:由於這個渲染層的提高會使得字體渲染方式由子像素變爲灰階
  • 有 3D transform
  • backface-visibility 爲 hidden
  • 對 opacity、transform、fliter、backdropfilter 應用 animation 或者 transition(須要是 active 的 animation 或者 transition,當 animation 或者 transition 效果未開始或結束後,提高合成層也會失效)
  • will-change 設置爲 opacity、transform、top、left、bottom、right(其中 top、left 等須要設置明確的定位屬性:好比 relative etc.)

後代元素緣由

  • 有合成層後代同時自己有 transform、opactiy(小於 1)、mask、fliter、reflection 屬性
  • 有合成層後代同時自己 overflow 不爲 visible(若是自己是由於明確的定位因素產生的 SelfPaintingLayer,則須要 z-index 不爲 auto)
  • 有合成層後代同時自己 fixed 定位
  • 有 3D transfrom 的合成層後代同時自己有 preserves-3d 屬性
  • 有 3D transfrom 的合成層後代同時自己有 perspective 屬性

重疊緣由

重疊或者說部分重疊在一個合成層之上,最多見和容易理解的就是元素的 border box(content + padding + border) 和合成層的有重疊,其餘的還有一些不常見的狀況,也算是同合成層重疊的條件以下:

  • filter 效果同合成層重疊
  • transform 變換後同合成層重疊
  • overflow scroll 狀況下同合成層重疊

假設重疊在一個合成層之上,其實也比較好理解,好比一個元素的 CSS 動畫效果在運行期間,元素有可能和其餘元素髮生重疊的狀況,須要注意的是該緣由下,有一個很特殊的狀況:若是合成層有內聯的 transform 屬性,會致使其兄弟渲染層假設重疊從而提高爲合成層。

基本上常見的一些合成層的提高緣由如上所說,咱們會發現:因爲重疊的緣由可能隨隨便便就會產生出大量合成層來,而每一個合成層都要消耗 CPU 和內存資源,豈不是嚴重影響頁面性能?!

層壓縮

這一點瀏覽器也考慮到,所以就有層壓縮(Layer Squashing)的處理。若是多個渲染層同一個合成層重疊時,這些渲染層會被壓縮到一個 GraphicsLayer 中,以防止因爲重疊緣由致使可能出現的「層爆炸」。

固然,瀏覽器的自動層壓縮也不是萬能的,在不少特定狀況下,瀏覽器是沒法進行層壓縮的,而這些狀況也是咱們應該儘可能避免的(如下狀況都是基於重疊緣由而言):

  • 沒法進行會打破渲染順序的壓縮
  • video 元素的渲染層沒法被壓縮,同時也沒法將別的渲染層壓縮到 video 所在的合成層上
  • iframe、plugin 的渲染層沒法被壓縮,同時也沒法將別的渲染層壓縮到其所在的合成層上
  • 沒法壓縮有 reflection 屬性的渲染層
  • 沒法壓縮有 blend mode 屬性的渲染層
  • 當渲染層同合成層有不一樣的裁剪容器時,該渲染層沒法壓縮
  • 相對於合成層滾動的渲染層沒法被壓縮
  • 當渲染層同合成層有不一樣的具備 opacity 的祖先層(一個設置 opacity 且小於 1 一個沒有設置 opacity 也算是不一樣)時,該渲染層沒法壓縮
  • 當渲染層同合成層有不一樣的具備 transform 的祖先層時,該渲染層沒法壓縮
  • 當渲染層同合成層有不一樣的具備 filter 的祖先層時,該渲染層沒法壓縮
  • 當覆蓋的合成層正在運行動畫時,該渲染層沒法壓縮,只有在動畫未開始或者運行完畢之後,該渲染層才能夠被壓縮

多線程

進一步來講,瀏覽器還可使用多線程的渲染架構,將網頁內容繪製到緩存的操做放到另一個獨立的線程(繪製線程),而原來線程對 WebView 的繪製就只剩下緩存的拷貝(合成線程),繪製線程跟合成線程之間可使用同步,部分同步,徹底異步等做業模式,讓瀏覽器能夠在性能與效果之間根據須要進行選擇。

Main thread or WebKit/Blink thread

內核線程 - 負責解析,排版,Render 樹繪製,JavaScript 執行等任務,它有可能執行真正的網頁內容的光柵化,也有可能只是紀錄繪製指令,由獨立的光柵化線程執行

Rasterize thread

光柵化線程 - 若是內核線程只負責將網頁內容轉換爲繪圖指令列表,則真正的光柵化(執行繪圖指令計算出像素的顏色值)由獨立的光柵化線程完成

Compositor thread

合成線程 - 負責將網頁內部位圖緩存/紋理輸出到窗口的幀緩存,從而把網頁顯示在屏幕上,可是在使用 GPU 合成的狀況下,也有可能只是產生 GL 繪圖指令,而後將繪圖指令的緩存發送給 GPU 線程執行

GPU thread

GPU 線程 - 若是使用 GPU 合成,則由 GPU 線程負責執行 GL 繪圖指令,訪問 GPU,可能跟合成線程是同一個線程,也有多是獨立的線程(合成線程產生GL指令 GPU 線程執行)

Browser UI thread

瀏覽器 UI 線程,若是跟 GPU 線程不是同一個線程,則只負責外殼的繪製,若是跟 GPU 線程是同一個線程,則同時負責繪製外殼的UI界面,和網頁的合成輸出,到窗口幀緩存

image_1c84d20k41v5kia34ig1kk71aro9.png-146.9kB

重排&重繪

重排和重繪是老生常談的東西,你們也應該很是熟悉,但在這裏能夠結合瀏覽器機制順帶講一遍。

重排

首先,若是你改變一個影響元素佈局信息的 CSS 樣式:好比 width、height、left、top etc.(transform除外),那麼瀏覽器會將當前的 Layout 標記爲 dirty,這會使得瀏覽器在下一幀執行重排,由於元素的位置信息發生改變將可能會致使整個網頁其餘元素的位置狀況都發生改變,因此須要執行 Layout 全局從新計算每一個元素的位置。

須要注意到,瀏覽器是在下一幀、下一次渲染的時候才重排,並非 JS 執行完這一行改變樣式的語句以後當即重排,因此你能夠在 JS 語句裏寫 100 行修改 CSS 的語句,可是隻會在下一幀的時候重排一次。

會觸發重排的屬性和方法以下:

Element

clientHeight, clientWidth, clientTop, clientLeft, focus(), getBoundingClientRect(), getClientRects(), innerText, offsetHeight, offsetLeft, OffsetParent, offsetTop, offsetWidth, outerText, scrollByLines(), scrollByPages(), scrollHeight, scrollIntoView(), scrollIntoViewIfNeeded(), scrollLeft, scrollTop, scrollWidth

Frame, Image

height, width

Range

getBoundingClientRect(), getClientRects()

SVGLocatable

computeCTM(), getBBox()

SVGTextContent

getCharNumAtPosition(), getComputedTextLength(), getEndPositionOfChar(), getExtentOfChar(), getNumberOfChars(), getRotationOfChar(), getStartPositionOfChar(), getSubStringLength(), selectSubString()

SVGUse

instanceRoot

window

getComputedStyle(), scrollBy(), scrollTo(), scrollX, scrollY, webkitConvertPointFromNodeToPage(), webkitConvertPointFromPageToNode()

強制重排

若是你在當前 Layout 被標記爲 dirty 的狀況下訪問 offsetTop、scrollHeight 等屬性,那麼瀏覽器會當即從新 Layout,計算出此時元素正確的位置信息,以保證你在 JS 裏獲取到的 offsetTop、scrollHeight 等是正確的。

這一過程被稱爲強制重排 Force Layout,強制瀏覽器將原本在渲染流程中才執行的 Layout 過程提早至 JS 執行過程當中,每次當咱們在 Layout 爲 dirty 時訪問會觸發重排的屬性都會 Force Layout,這會極大延緩 JS 的執行效率

另外,每次重排或者強制重排後,當前 Layout 就再也不 dirty,這時再訪問 offsetWidth 之類的屬性並不會再觸發重排。

重繪

重繪也是類似的,一旦你更改某個元素的會觸發重繪的樣式,那麼瀏覽器就會在下一幀的渲染步驟中進行重繪(也即一些介紹重繪機制中說的 invalidating),JS 更改樣式致使某一片區域的樣式做廢,從而在一下幀中重繪 invalidating 的區域。

可是!有一個很是關鍵的行爲就是:重繪是以合成層爲單位的,也即 invalidating 的既不是整個文檔也不是單個元素,而是這個元素所在的合成層。固然這也是將渲染過程拆分爲 Paint 和 Compositing 的初衷之一:

Since painting of the layers is decoupled from compositing, invalidating one of these layers only results in repainting the contents of that layer alone and recompositing.

使用 transform 或者 opacity 來實現動畫效果

修改一些 CSS 屬性如 width、float、border、position、font-size、text-align、overflow-y etc. 會觸發重排、重繪和合成,修改另外一些屬性如 color、background-color、visibility、text-decoration etc. 則不會觸發重排,只會重繪和合成。

image_1c84lrbps1ntk1hjl10eg9371sfp1g.png-49.3kB

接下來不少文章裏就會說,修改 opacity、transform 這兩個屬性僅僅會觸發合成,不會觸發重繪,因此必定要用這兩個屬性來實現動畫,沒有重繪重排,效率很高…… 然而事實並非這樣,只有一個元素在被提高爲合成層以後,上述狀況才成立

最後一句話:合成層提高並不是銀彈!

相關文章
相關標籤/搜索