關於BeesAndroid項目
javascript
BeesAndroid項目提供了一系列的工具、理論分析與方法論,旨在下降Android系統源碼的閱讀門檻,讓讀者更好的理解Android系統的設計與實現。第一次閱覽本系列文章,請參見導讀,更多文章請參見文章目錄。css
今天咱們來聊一聊Chromium的渲染機制,這也是渲染機制系列的第二篇,最近大半年的工做都和H5容器有關,於是花了點時間學習了下Chromium項目,這裏着重去分析一下它的渲染機制。
從開發者的角度,當咱們去看一個H5容器的時候,和它一塊兒工做的有如下角色:
html
以下所示:
前端
能夠看到,頁面在渲染以前還有須要工做須要處理,容器的啓動也是個耗時的操做,爲何會特意聊聊容器啓動呢,由於這個也是H5頁面體驗的重要組成部分,由於是Native的關係,前端同窗可能會關注不到。並且容器導航階段是重要的預加載時機,咱們能夠在這裏作不少事情,例如:java
- 接口預加載
- HTML文檔預加載
- 資源預加載
- 導航的時候建立一個JS Engine,能夠提早執行JS邏輯,把導航預加載這個能力開放給前端
言歸正傳,咱們接着來聊聊渲染機制。
node
瀏覽器的渲染過程就是把網頁經過渲染管道渲染成一個個像素點,最終輸出到屏幕上。這裏面就涉及3個角色
android
什麼是輸入端(Content)?
git
咱們在Chromium這個項目裏會頻繁的看到Content這個概念,那麼Content究竟是什麼呢。Content是渲染網頁內容的區域,在Java層對應AwContent,底層有WebContents表示,以下所示:github
content在代碼由content::WebContents來描述,它在獨立的Render進程由Blink建立。具體說來Content對應着前端開發中涉及的HTML、CSS、JS、image等,以下所示:web
什麼是渲染管線(Rendering Pipeline)?
渲染管線能夠理解爲對渲染流程的拆解,向工廠流水線同樣,上一個車間生成的半成品送到下一個車間繼續裝配。拆解渲染流程有助於把渲染流程簡單化,提升渲染效率。
渲染時動態的,內容發生變化時,就會觸發渲染,更新像素點,和Android的繪製系統同樣,觸發繪製也是由invalidate機制觸發的,觸發渲染後,執行整個渲染管線是很是昂貴的,於是Blink也在想法設法減小沒必要要的渲染動做,提升渲染效率。
- 觸發的條件以下所示:
- scrolling
- zooming
- animations
- incremental loading
- javascript
- 各個流程的觸發方法以下:
- Style:Node::SetNeedsStyleRecalc()
- Layout:LayoutObject::SetNeedsLayout()
- Paint:PaintInvalidator::InvalidatePaint()
- RasterInvalidator::Generate()
渲染管道把網頁轉換爲繪製指令後,它並不能直接把繪製指令變成像素點(光柵化)顯示在屏幕上(Window),這個時候就須要藉助操做系統本身的能力(底層的圖形庫),在圖形界面這一塊大部分平臺都遵循OpenGL標準化的API。例如Windows上的DirectX,Android上的Vulcan。以下圖所示:
**
咱們先來講結構
從上到下,分層來講:
這裏面還提到了每一個層級向上輸出的產物幀,幀(Frame)描述了渲染流水線下級模塊向上級模塊輸出的繪製內容相關數據的封裝。
整個渲染水流水線的調度基於請求和狀態機響應,調度的中樞運行在Browser UI線程,它按照顯示器的VSync信號向Layer Compositor發出輸出下一幀的請求,而Layer Compositor根據自身的狀態機的狀態決定是否須要Blink輸出下一幀。而Layer Compositor和Display Compositor是生成者和消費者的關係,Display Compositor持有一個Compositor Frame隊列不斷的進行取出和繪製,輸出的頻率取決於 Compositor Frame的輸入幀率和自身GL Frame的繪製頻率。
咱們再來講流程
咱們來分別看具體的流程。
注:Rendering Pipeline裏的圖片來自於Chromium工程師的ppt Life of a Pixel的截圖。
相關文檔
相關源碼
當咱們從服務器上下載了一份HTML文檔,第一步就是解析,HTML解析器接收標籤和文本流(HTML是純文本格式)把HTML文檔解析成DOM樹。DOM(Document Object Model)即文檔對象模型,DOM及時頁面的內部表示,也爲JavaScript暴露了API接口(V8 DOM API),可讓JavaScript程序改變文檔的結構、樣式和內容。
它是一個樹狀結構,咱們在後續的渲染流程中還會看到不少樹形結構(例如佈局樹、屬性樹等)由於它們都是基於DOM樹的結構(HTML的結構)而來的。
注:HTML文檔中可能包含多棵DOM樹,由於HTML支持自定義元素,這種樹一般被稱爲Shadow Tree。
解析HTML生成DOM樹流程以下:
DOM樹(DOM Tree)做爲後續繪製流程的基礎, 還會基於它生產各類類型的樹,具體說來,主要會經歷以下轉換:
對象轉換
- DOM Tree -> Render Tree -> Layer Tree
- DOM node -> RenderObject -> RenderLayer
DOM Tree(節點是DOM node)
當加載一個HTML時,會對他進行解析,生成一棵DOM樹。DOM樹上的每個節點都對應這網頁裏面的每個元素,網頁能夠經過JavaScript操做這棵DOM樹。
Render Tree(節點是RenderObject)
可是DOM樹自己並不能直接用於排版和渲染,所以內核會生成Render Tree,它是DOM Tree和CSS相結合的產物,二者的節點幾乎是一一對應的。Render Tree是排版引擎和渲染引擎之間的橋樑。
Layer Tree(節點是RenderLayer)
渲染引擎並非直接使用Render Tree進行繪製的,爲了更加方便的處理定位、裁剪、業內滾動等操做,渲染引擎會生成一棵Layer Tree。渲染引擎會爲一些特定的RenderObject生成相應的RenderLayer,不過該RenderObject的子節點沒有相應的RenderLayer,那麼它就從屬於父節點的RenderLayer。渲染引擎會遍歷每個RenderLayer,再遍歷從屬於這個RenderLayer的RenderObject,將每個RenderObject繪製出來。
能夠這麼理解,Layer Tree決定了網頁的繪製順序,從屬於RenderLayer的RenderObject決定了這個Layer的繪製內容。
什麼樣的RenderObject會成爲RenderLayer呢。GPU Accelerated Compositing in Chrome是這樣定義的:
- It's the root object for the page
- It has explicit CSS position properties (relative, absolute or a transform)
- It is transparent
- Has overflow, an alpha mask or reflection
- Has a CSS filter
- Corresponds to
- Corresponds to a
對上面的流程不瞭解也不要緊,咱們下面會一一解釋。
當DOM樹生成之後,就須要爲每一個元素設置一個樣式,有的樣式只是會影響某個節點,有的樣式會影響整個節點下面的整個DOM子樹的渲染(例如,節點的旋轉變換)。
相關源碼
樣式通常都是樣式渲染器共同做用的結果,它有複雜的優先級語義和渲染過程,過程總體分爲三步:
1 收集、劃分和索引全部樣式表中樣式規則。
計算並應用了每一個DOM節點的樣式之後,就須要決定每一個DOM節點的擺放位置。DOM節點都是基於盒模型擺放(一個矩形),佈局就是計算這些盒子的座標。
|-------------------------------------------------|
| |
| margin-top |
| |
| |---------------------------------------| |
| | | |
| | border-top | |
| | | |
| | |--------------------------|--| | |
| | | | | | |
| | | padding-top |##| | |
| | | |##| | |
| | | |----------------| |##| | |
| | | | | | | | |
| ML | BL | PL | content box | PR |SW| BR | MR |
| | | | | | | | |
| | | |----------------| | | | |
| | | | | | |
| | | padding-bottom | | | |
| | | | | | |
| | |--------------------------|--| | |
| | | scrollbar height ####|SC| | |
| | |-----------------------------| | |
| | | |
| | border-bottom | |
| | | |
| |---------------------------------------| |
| |
| margin-bottom |
| |
|-------------------------------------------------|
複製代碼
相關文檔
相關源碼
基於DOM Tree會生成Layout Tee,生成每一個節點的佈局信息。佈局的過程就是遍歷整個Layout Tree進行佈局操做。
DOM Tree和Layout Tree也不老是一一對應的,若是咱們再標籤裏設置dispaly:none,它就不會建立一個佈局對象(LayoutObject)。
在Layout操做完成之後,理論上就能夠開始Paint操做了,可是咱們以前提過,若是直接開始Paint操做,繪製整個界面,代價是很是昂貴的。所以便引入了一個圖層合成加速的概念。
什麼是圖層合成加速(Compositing Layer)?
圖層合成加速基本思想是把整個頁面按照必定規則分紅多個圖層(就像Photoshop的圖層那樣),在渲染時只須要操做必要的圖層,其餘圖層只須要參與合成就好了,以此提升渲染效率。完成這個工做的線程叫Compositor Thread,值得一提的是Compositor Thread還具有處理輸入事件的能力(例如滾動事件),可是若是在JavaScript註冊了事件監聽,它會把輸入事件轉發給主線程處理。
具體說來是爲某些RenderLayer擁有本身獨立的緩存,它們被稱爲合成圖層(Compositing Layer),內核會被這些RenderLayer建立對應的GraphicsLayer。
- 擁有本身的GraphicsLayer的RenderLayer在繪製的時候就會繪製在本身的緩存裏面。
- 沒有本身的GraphicsLayer的RenderLayer會向上查找父節點的GraphicsLayer,直到RootRenderLayer(它老是會有本身的GraphicsLayer)爲止,而後繪製在有GraphicsLayer的父節點的緩存裏。
這樣就造成了與RenderLayer Tree對應的GraphicsLayer Tree。當Layer的內容發生變化時,只須要更新所屬的GraphicsLayer便可,而單一緩存架構下,就會更新整個圖層,會比較耗時。這樣就提升了渲染的效率。可是過多的GraphicsLayer也會帶來內存的消耗,雖然減小了沒必要要的繪製,但也可能由於內存問題致使總體的渲染性能下賤。於是圖層合成加速追求的是一個動態的平衡。
什麼樣的RenderLayer會被建立GraphicsLayer呢,GPU Accelerated Compositing in Chrome是這樣定義的:
- Layer has 3D or perspective transform CSS properties
- Layer is used by
- Layer is used by a
- Layer is used for a composited plugin
- Layer uses a CSS animation for its opacity or uses an animated webkit transform
- Layer uses accelerated CSS filters
- Layer has a descendant that is a compositing layer
- Layer has a sibling with a lower z-index which has a compositing layer (in other words the layer overlaps a composited layer and should be rendered on top of it)
圖層化的決策是由Blink來負責(將來可能會轉移到Layer Compositor決策),根據DOM樹生成一個圖層樹,並以DisplayList記錄每一個圖層的內容。
瞭解了圖層合成加速的概念之後,咱們再來看看發生在Layout操做以後的Compositing update(合成更新),合成更新就是爲特定的RenderLayer(建立規則咱們已經描述過了)建立GraphicsLayer的過程,以下所示:
什麼是屬性樹?
在描述屬性的層次結構這一塊,以前的方式是使用圖層樹的方式,若是父圖層具備矩陣變換(平移、縮放或者透視)、裁剪或者特效(濾鏡等),須要遞歸的應用到子節點,時間複雜度是O(圖層數),這在極端狀況下會有性能問題。
所以引入了屬性樹的概念,合成器提供了變換樹、裁剪樹、特效樹等。每一個圖層都由若干節點id,分別對應不一樣屬性樹的矩陣變換節點、裁剪節點和特效節點。這樣的時間複雜度就是O(要變化的節點),以下所示:
Prepaint的過程就是構建屬性樹的過程,以下所示:
建立完屬性樹(Prepaint)之後,就開始進入Paint階段了。
相關文檔
相關源碼
Paint操做會將佈局樹(Layout Tree)中的節點(Layout Object)轉換成繪製指令(例如繪製矩形、繪製字體、繪製顏色,這有點像繪製API的調用)的過程。而後把這些操做封裝在Dsipaly Item中,這些Dsipaly Item存放在PaintArtifact中。PaintArtifact就是是Paint階段的輸出。
到目前爲止,咱們創建了能夠重放的繪製操做列表,但沒有執行真正的繪製操做。
注:重放(replay),如今圖形系統大都採用recrod & replay機制,採集繪製指令與執行繪製指令相互分離,提升渲染效率
Paint操做最終會在Layout Tree的基礎上生成一棵Paint Tree。
Paint階段完成之後,進入Commit階段。該階段會更新圖層和屬性樹的副本到合成器線程,以匹配提交的主線程狀態。說的通俗點,就是把主線程裏Paint階段的數據(layers and properties)拷貝到合成器線程,供合成器線程使用。
可是合成器線程接收到數據後,並不會當即開始合成,而是進行圖層分塊,這裏又涉及一個分塊渲染的技術。
什麼是分塊渲染?
分塊渲染(Tile Rendering)就是把網頁的緩存分爲一格一格的小塊,一般爲256x256或者512x512,而後分塊進行渲染。
分塊渲染主要基於兩個方面的考慮:
- GPU合成一般是使用OpenGL ES貼圖實現的,這時候的緩存實際就是紋理(GL Texture),不少GPU對紋理的大小是有限制的,好比長寬必須是2的冪次方,最大不能超過2048或者4096等。沒法支持任意大小的緩存。
- 分塊緩存,方便瀏覽器使用統一的緩衝池來管理緩存。緩衝池的小塊緩存由全部WebView共用,打開網頁的時候向緩衝池申請小塊緩存,關閉網頁是這些緩存被回收。
圖塊(tiling)是柵格化工做的基本單位。 柵格化會根據圖塊與可見視口的距離安排優先順序進行柵格化。離得近的會被優先柵格化,離得遠的會降級柵格化的優先級。這些圖塊拼接在一塊兒,就造成了一個圖層,以下所示:
圖層分塊完成之後,接着就會進行柵格化(Raster)。
什麼是光柵化(柵格化)?
光柵化(Raterization),又稱柵格化,它用於執行繪圖指令生成像素的顏色值,光柵化策略分爲兩種:
- 同步光柵化:光柵化和合成在同一線程,或者經過線程同步的方式來保證光珊化和合成
- 直接光柵化:直接將全部可見圖層的eDisplayList中的可見區域的繪圖指令進行執行,在目標Surface的像素緩衝區上生成像素的顏色值。固然若是是徹底的直接光柵化,就不涉及圖層合併了,也就不須要後面的合成了。
- 間接光柵化:容許爲指定圖層分配額外的緩衝區,該圖層的光柵化會先寫入自身的像素緩衝區,渲染引擎再將這些圖層的像素緩衝區(Android裏能夠調用View.setLayerType容許應用爲View分配像素緩衝區)經過合成輸出大歐姆表Surface的像素緩衝區。Android和Flutter主要使用直接光柵化的測量,同時也支持間接光柵化。
- 異步分塊光柵化
1 老版本的調用採用這種方式,Skia運行在Renderer Process,負責產生GL指令,GPU有單獨的GPU Process,這種模式下Skia沒法直接進行渲染系統調用,在初始化Skia的時候回給它一個函數指針表(指向了GL API,但不是真正的OpenGL API,而是Chromium提供的代理),函數指針錶轉換爲真正的OpenGL API的過程稱爲命令緩衝區(GpuChannelMsg_FlushCommandBuffers),
單獨的GPU進程有利於隔離GL操做,提高穩定性和安全性,這種模式也稱爲沙箱機制(不安全的操做運行在獨立的進程裏)。
在Commit以後,Draw以前有一個Activate操做。Raster和Draw都發生在合成器線程裏的Layer Tree上,可是咱們知道Raster操做是異步的,有可能須要執行Draw操做的時候,Raster操做還沒完成,這個時候就須要解決這個問題。
它將Layer樹分爲:
這個拷貝的過程就稱爲Activate,以下所示:
主線程的圖層樹由LayerTreeHost擁有,每一個圖層以遞歸的方式擁有其子圖層。Pending樹、Active樹、Recycle樹都是LayerTreeHostImpl擁有的實例。這些樹被定義在cc/trees目錄下。之因此稱之爲樹,是由於早期它們是基於樹結構實現的,目前的實現方式是列表。
11 Draw
當每一個圖塊都被光柵化之後,合成器線程會爲每一個圖塊生成draw quads(在屏幕指定位置繪製圖塊的指令,包含了屬性樹裏面的變換、特效等操做),這些draw quads指令被封裝在CompositorFrame對象中,CompositorFrame對象也是Render Process的輸出產物。它會被提交到GPU Process中。咱們平時提到的60fps輸出幀率裏面的幀指的就是Compositor Frame。
Draw操做就是柵格化的圖塊生成draw quads的過程。
相關文檔
Draw操做完成之後,就生成了Compositor Frame,它們會被輸出到GPU Process。 它會從多個來源的Render Process接收Compositor Frame。
Viz是VIsual的縮寫,它是Chromium總體架構轉向服務化的一個重要組成部分,包含Compositing、GL、Hit Testing、Media、VR/AR等衆多功能。
VIz也是雙緩衝輸出的,它會在後臺緩衝區繪製draw quads,而後執行交換命令最終讓它們顯示在屏幕上。
什麼是雙緩衝機制?
在渲染的過程當中,若是隻對一塊緩衝區進行讀寫,這樣會致使一方面屏幕要等到去讀,而GPU要等待去寫,這樣要形成性能低下。一個很天然的想法是把讀寫分開,分爲:
- 前臺緩衝區(Front Buffer):屏幕負責從前臺緩衝區讀取幀數據進行輸出顯示。
- 後臺緩衝區(Back Buffer):GPU負責向後臺緩衝區寫入幀數據。
這兩個緩衝區並不會直接進行數據拷貝(性能問題),而是在後臺緩衝區寫入完成,前臺緩衝區讀出完成,直接進行指針交換,前臺變後臺,後臺變前臺,那麼何時進行交換呢,若是後臺緩存區已經準備好,屏幕尚未處理完前臺緩衝區,這樣就會有問題,顯然這個時候須要等屏幕處理完成。屏幕處理完成之後(掃描完屏幕),設備須要從新回到第一行開始新的刷新,這期間有個間隔(Vertical Blank Interval),這個時機就是進行交互的時機。這個操做也被稱爲垂直同步(VSync)。
到這裏,整個渲染流程就結束了,前端的代碼變成了能夠與用戶交互的像素點。