天天看點

Chromium網頁渲染機制簡要介紹和學習計劃

       作為一個浏覽器,快速地将網頁渲染出來是最重要的工作。Chromium為了做到這一點,費盡了心機,做了大量優化工作。這些優化工作是卓有成效的,代表了當今最先進的網頁渲染技術。值得一提的是,這些渲染技術不僅适用于網頁渲染,也可以應用在原生系統的UI渲染上。例如,在Android系統上,我們就可以看到兩者在渲染技術上的相似之處。本文接下來就對Chromium的網頁渲染機制進行簡要介紹,并且制定學習計劃。

老羅的新浪微網誌:http://weibo.com/shengyangluo,歡迎關注!

《Android系統源代碼情景分析》一書正在進擊的程式員網(http://0xcc0xcd.com)中連載,點選進入!

       Chromium的網頁渲染機制可以用八個字來描述:縱向分層,橫向分塊。其中,分層是由WebKit完成的,就是把網頁抽象成一系列的Tree。Tree由Layer組成,Layer就是我們所說的層。從前面Chromium網頁加載過程簡要介紹和學習計劃這個系列的文章可以知道,WebKit為網頁依次建立了DOM Tree、Render Object Tree、Render Layer Tree和Graphics Layer Tree四棵Tree。其中,與渲染相關的是後面三棵Tree。将網頁進行分層,好處有兩個。一是減少不必要的繪制操作,二是利用硬體加速渲染動畫。

       第一個好處得益于WebKit将網頁一幀的渲染分為繪制和合成兩個步驟。繪制是将繪圖操作轉化為圖像的過程,合成是将所有圖像混合在一起後顯示在螢幕上的過程。注意,對于螢幕來說,不管它某一個區域的内容是否發生變化,在它的下一幀顯示中,總是需要進行重新整理的。這意味着系統總是需要給它一個完整的螢幕内容。考慮這樣的一個網頁全屏顯示的場景,并且網頁被抽象為兩個層。在下一幀顯示中,隻有一個層的内容發生了變化。這時候,隻需要對内容發生變化的層執行繪制操作即可,然後與另一個層上一幀已經繪制得到的圖像進行合成就可以得到整個螢幕的内容。這樣就避免了不必要的繪制操作,額外付出的代價是一個合成操作。但是請注意,相對于繪制來說,合成是一個很輕量級的操作,尤其是對硬體加速渲染來說,它僅僅就是一個貼紋理的過程,并且紋理内容本身已經是Ready好了的。第二個好處将某些動畫單獨放在一層,然後對這個層施加某種變換,進而形成動畫。某些變換,例如平移、旋轉、縮放,或者Alpha漸變,對硬體加速來說,是輕易實作的。

       與分層相比,分塊是一個相對微觀的概念,它是針對每一個層而言的。一般來說,一個網頁的内容要比螢幕大很多,是以,使用者會經常性地進行滾動或者縮放浏覽。這種情況在移動裝置上表現尤其特出。如果所有内容都是可見的一刻再進行繪制,那麼就會讓使用者覺得很卡頓。另一方面,如果一開始就對網頁所有的内容,不管可見還是不可見,都進行繪制,那麼就會讓使用者覺得網頁加載很慢,而且會非常耗費資源。這兩種方案都不合适。最理想的方式是盡快顯示目前可見的内容,并且在有富餘勞動力的時候,預先繪制那些接下來最有可能可見的内容。這意味着要賦予一個層的不同區域賦予不同的繪制優先級。每一個區域就是一個塊(Tile),每一個層都由若幹個塊組成。其中,位于目前可見區域中的塊的優先級最高的,它們需要最優先進行繪制。

       有時候,即使隻繪制那些優先級最高的塊,也要耗費不少的時間。這裡面有一個很關鍵的因素是紋理上傳。這裡我們隻讨論硬體加速渲染的情況。與一般的GPU指令相比,紋理上傳操作是一個很慢的過程。為了解決這個問題,Chromium首先按照一定的比例繪制網頁的内容,例如0.5的比例。這樣就可以減少四分之三的紋理大小。在将0.5比例的内容顯示出來的同時,繼續繪制正常比例的網頁内容。當正常比例的網頁内容繪制完成後,再替換掉目前顯示的低分辨率内容。這種方式盡管讓使用者在開始時看到的是低分辨率的内容,但是也比使用者在開始時什麼也看不到要好。

       以上就是網頁分層和分塊的概念。概念上是不難了解的,但是在實作上,它們是相當複雜的,而且會摻雜其它的優化點。例如,每一個網頁會有兩個線程一起協作完成整個渲染過程。這樣就可以充分利用現代CPU的多核特性。不過這也會給網頁的渲染過程帶來複雜性,因為這會涉及到線程同步問題。這種渲染方式也是以稱為線程化渲染。我們回過頭來看Android應用程式UI的渲染方式,也是從單線程逐漸演變為多線程渲染。在5.0之前,一個Android應用程式程序隻有一個線程是負責UI渲染的,這個線程就是主線程,也稱為UI線程。到了5.0,增加了一個線程,稱為Render線程,它與UI線程一起完成Android應用程式UI的渲染。這一點可以參考前面Android應用程式UI硬體加速渲染技術簡要介紹和學習計劃這一系列的文章。

       為了更好地支援線程化渲染,Chromium中負責渲染網頁的CC(Chromium Compositor)子產品,會建立三棵Tree,與WebKit建立的Graphics Layer Tree相對應,如圖1所示:

Chromium網頁渲染機制簡要介紹和學習計劃

圖1 CC子產品中的Layer Tree、Pending Layer Tree和Active Layer Tree

       其中,Layer Tree由Render程序中的Render Thread(也稱為Main Thread)維護,Pending Layer Tree和Active Layer Tree由Render程序中的Compositor Thread(也稱為Impl Thread)維護。在需要的時候,Layer Tree會與Pending Layer Tree進行同步,也就是在Render Thread與Compositor Thread之間進行UI相關的同步操作。這個操作由Compositor Thread執行。在執行期間,Render Thread處于等待狀态。執行完成後,Compositor Thread就會對Pending Layer Tree中的Layer進行分塊管理,并且對塊進行光栅化操作,也就是将繪制指令變成一張圖像。當Pending Layer Tree完成光栅化操作之後,它就會變成Active Layer Tree。Active Layer Tree是Chromium目前正在顯示給使用者浏覽的Tree。

       我們通過圖2可以更直覺地看到Render程序中的Render Thread和Compositor Thread的協作過程,如下所示:

Chromium網頁渲染機制簡要介紹和學習計劃

圖2 Render Thread和Compositor Thread的協作過程

       Render Thread繪制完成Layer Tree的第N幀之後,就同步給Compositor Thread的Pending Layer Tree。同步完成之後,Render Thread就去繪制Layer Tree的第N + 1幀了。與此同時,Compositor Thread也在抓緊時間對第N幀進行光栅化等操作。從這裡就可以看到,第N+1幀的繪制操作與第N幀的光栅化操作是同步進行的,是以它們可以充分利用CPU的多核特性,進而提高網頁渲染效率。

       Compositor Thread完成光栅化操作之後,就會得到一系列的紋理,這些紋理最終會在GPU程序中進行合成。合成之後使用者就可以看到網頁的内容了。Render程序是通過Command Buffer請求GPU程序執行GPU指令,以及傳遞紋理資源等資訊的,這些可以參考前面Chromium硬體加速渲染機制基礎知識簡要介紹和學習計劃這個系列的文章。

       從圖3我們就可以看到,網頁的一次渲染,實際上涉及到的核心線程有三個,除了Render程序中的Render Thread和Compositor Thread外,還有GPU程序中的GPU Thread,并且這三個線程是可以做到并行執行的,又進一步地利用了CPU的多核特點。

       為了更好地管理Layer Tree,Render Thread建立了一個LayerTreeHost對象。同樣,為了更好地管理Pending Layer Tree和Active Layer Tree,Compositor Thread建立了一個LayerTreeHostImpl對象。LayerTreeHost對象和LayerTreeHostImpl對象之間的通信就代表了Render Thread與Compositor Thread之間的協作。

       LayerTreeHost對象和LayerTreeHostImpl對象并不是直接地進行通信的,而是通過一個Proxy對象進行,如圖3所示:

Chromium網頁渲染機制簡要介紹和學習計劃

圖3 LayerTreeHost對象和LayerTreeHostImpl對象通過Proxy對象通信

       LayerTreeHost對象和LayerTreeHostImpl對象之是以要通過Proxy對象進行通信,是為了能夠同時支援線程化渲染和單線程渲染兩種機制。對于單線程渲染機制,使用的Proxy對象實際上是一個Single Thread Proxy對象,如圖4所示:

Chromium網頁渲染機制簡要介紹和學習計劃

圖4 單線程渲染

       在單線程渲染機制中,LayerTreeHost對象和LayerTreeHostImpl對象實際上都是運作在Render Thread中,是以Single Thread Proxy的實作就很簡單,它通過layer_tree_host_和layer_tree_host_impl_兩個成員變量分别引用了LayerTreeHost對象和LayerTreeHostImpl對象。在LayerTreeHost對象和LayerTreeHostImpl對象需要互相通信的時候,就通過這兩個成員變量進行直接調用即可。

       對于線程化渲染機制,使用的Proxy對象實際上是一個Threaded Proxy對象,如圖5所示:

Chromium網頁渲染機制簡要介紹和學習計劃

圖5 線程化渲染

       Threaded Proxy内部有一個成員變量impl_task_runner_,它指向一個SingleThreadTaskRunner對象。這個SingleThreadTaskRunner對象描述的是Compositor Thread的消息循環。當LayerTreeHost對象需要與LayerTreeHostImpl對象通信時,它就會通過上述Threaded Proxy中的SingleThreadTaskRunner對象向Compositor Thread發送消息,以請求LayerTreeHostImpl對象在Compositor Thread中執行某一個操作。但是Threaded Proxy并不會馬上将該請求發送給LayerTreeHostImpl對象執行,而是會根據LayerTreeHostImpl對象的目前狀态決定是否要向它送出請求。這樣可以獲得一個好處,就是CC子產品可以平滑地處理各項渲染工作。例如,如果目前LayerTreeHostImpl對象正在光栅化上一幀的内容,這時候LayerTreeHost對象又請求繪制下一幀的内容,那麼繪制下一幀内容的請求就會被延後,直到上一幀内容光栅化操作完成之後。這樣就會避免狀态混亂。

       Threaded Proxy是通過一個排程器(Scheduler)來安排LayerTreeHostImpl對象什麼時候該執行什麼操作的。排程器又是通過一個狀态機(State Machine)來記錄LayerTreeHostImpl對象的狀态流轉的,進而為排程器提供排程安排。典型地,在一個網頁的浏覽期間,排程器會被依次排程執行以下操作:

       1. 如果還沒有建立繪圖表面,那麼排程器就會發出一個ACTION_BEGIN_OUTPUT_SURFACE_CREATE的操作,這時候LayerTreeHostImpl對象就會為網頁建立一個繪圖表面。關于網頁的繪圖表面及其建立過程,可以參考前面Chromium硬體加速渲染的OpenGL上下文繪圖表面建立過程分析一文。

       2. 當Layer Tree發生變化需要重繪時,排程器就會發出一個ACTION_SEND_BEGIN_MAIN_FRAME操作,這時候LayerTreeHost對象就會對網頁進行重繪。這裡有兩點需要注意。第一點是這裡所說的繪制,實際上隻是将繪圖指令記錄在了一個指令緩沖區中。第二點是繪制操作是在Render Thread中執行的。

       3. LayerTreeHost對象重繪完Layer Tree之後,Render Thread會處于等待狀态。接下來排程器會發出一個ACTION_COMMIT操作,通知LayerTreeHostImpl對象将Layer Tree的内容同步到Pending Layer Tree中去。這個同步操作是在Compositor Thread中執行的。同步完成之後,Render Thread就會被喚醒,而Compositor Thread繼續對Pending Layer Tree中的分塊進行更新,例如更新分塊的優先級。

       4. 對Pending Layer Tree中的分塊進行更新之後,排程器發出一個ACTION_MANAGE_TILES操作,通知LayerTreeHostImpl對象對Pending Layer Tree中的分塊進行光栅化。

       5. Pending Layer Tree完成光栅化操作之後,排程器繼續發出一個ACTION_ACTIVATE_PENDING_TREE操作,這時候Pending Layer Tree就變成Active Layer Tree。

       6. Pending Layer Tree就變成Active Layer Tree之後,排程器再發出一個ACTION_DRAW_AND_SWAP_FORCED,這時候LayerTreeHostImpl對象就會将已經光栅化好的分塊資訊收集起來,并且發送給Browser程序,以便Browser程序可以将這些分塊合成在浏覽器視窗中顯示。這一點可以參考前面Chromium硬體加速渲染的UI合成過程分析一文。

       其中,第2到第6個操作就是網頁一幀的完整渲染過程,這個過程在網頁的浏覽期間不斷地重複進行着。

       在網頁的渲染過程中,最重要的事情就是對分塊的管理。分塊是以層為機關進行管理的。這裡涉及到兩個重要的術語:Tile和Tiling。它們的關系如圖6所示:

Chromium網頁渲染機制簡要介紹和學習計劃

圖6 Tile和Tiling

      Tiling是由具有相同縮放比例因子的Tile組成的一個區域。在Chromium源代碼中,Tiling通過類PictureLayerTiling描述。一個層可能會按照不同的縮放因子進行分塊,如圖7所示:

Chromium網頁渲染機制簡要介紹和學習計劃

圖7 不同縮放因子的Tiling

       在圖7中,分塊的大小設定為256x256px。對于縮放因子為1.0的Tiling,分塊中的1個像素就對應于層空間的1個像素。對于縮放因子為0.67的Tiling,分塊中的1個像素就對應于層空間的1.5個像素。同理可以知道,對于縮放因子為0.5的Tiling,分塊中的1個像素就對應于層空間的2個像素。

       為什麼一個層要按照不同的縮放因子進行分塊呢?前面提到,主要是為了在滾動或者縮放網頁時,可以盡快地将網頁内容顯示出來,盡管顯示出來的内容是低分辨率的,如圖8所示:

Chromium網頁渲染機制簡要介紹和學習計劃

圖8 網頁放大過程

       圖8顯示的是一個網頁被放大的過程。開始的時候,較低縮放因子的分塊會被放大,用來填充可見區域。這時候與實際放大因子相同的分塊正在背後悄悄地進行建立以及光栅化。等到這些操作完成之後,它們就會替換掉較低縮放因子的分塊顯示在可見區域中。是以,我們在放大網頁的時候,首先會看到模糊的内容,接着很快又會看到清晰的内容。

       一個層的内容占據的區域可能是非常大的,超過了螢幕的大小。這時候我們不希望對整個層的内容都進行分塊,因為這會浪費資源。同時,我們又希望對盡可能多的内容進行分塊,這樣當目前不可見的分塊變為可見時,就會快速得到顯示。CC子產品将一個層的内容大緻分為三個區域,如圖9所示:

Chromium網頁渲染機制簡要介紹和學習計劃

圖9 層區域劃分

      從圖9可以看到,有Viewport、Skewport和Eventually Rect三個區域,CC子產品隻對它們進行分塊。其中,Viewport描述的是目前可見的區域,Skewport描述的是順着使用者的滑動方向接下來可見的區域,Eventually Rect是在Viewport的四周增加一個薄邊界形成的一個區域,這個區域的内容我們認為最終會有機會得到顯示。很顯然,從重要程度來看,Viewport > Skewport > Eventually Rect。是以,CC子產品優先光栅化Viewport中的分塊,接着是Skewport中的分塊,最後是Eventually Rect中的分塊。

      确定了哪些區域需要分塊,以及分塊的光栅化順序之後,接下來最核心的操作就是執行光栅化操作,如圖10所示:

Chromium網頁渲染機制簡要介紹和學習計劃

圖10 分塊光栅化過程

       網頁按照Graphics Layer進行繪制,它們被繪制一個Picture中。這個Picture實際上隻是儲存了繪制指令。将這些繪制指令變成像素就是光栅化所做的事情。光栅化可以通過GPU完成,也可以通過CPU完成。

       如果是通過GPU完成,那麼儲存在Picture的繪制指令就會轉化為OpenGL指令執行,最終繪制出來的像素就直接儲存在一個GPU紋理中。是以這種光栅化方式又稱為Direct Raster。

       如果是通過CPU完成光栅化,那麼儲存在Picture的繪制指令就會轉化為Skia指令執行。Skia是一個2D圖形庫,它通過CPU來執行繪制操作。最終繪制出來的像素儲存在一個記憶體緩沖區中。在Android平台上,CC子產品提供了三種CPU光栅化方式。

       第一種方式稱為Image Raster,光栅化後的像素儲存Android平台特有的一種Native Buffer中。這種Native Buffer即可以被CPU通路,也可以被GPU當作紋理通路。這樣就可以在光栅化操作完成之後避免執行一次紋理上傳操作,可以很好地解決紋理上傳速度問題。是以這種光栅化方式又稱為Zero Copy Texture Upload。

       第二種方式稱為Image Copy Raster,光栅化後的像素同樣是儲存在Android Native Buffer中。不過,儲存在Android Native Buffer中的像素會被拷貝到另外一個标準的GPU紋理中去。為什麼要這樣做呢?因為Android Native Buffer資源有限,是以在光栅化完成之後就釋放,可以降低資源需求。是以這種光栅化方式又稱為One Copy Texture Upload。

       第三種方式稱為Pixel Buffer Raster,光栅化後的像素儲存在OpenGL中的Pixel Buffer中,然後這種Pixel Buffer再作為紋理資料上傳到GPU中。與前面兩種CPU光栅化方式相比,它的效率是最低下的,因為涉及到臭名昭著的紋理上傳問題。

       為什麼光栅化的像素最終都要上傳到GPU裡面去呢?因為我們隻讨論硬體加速渲染的情況,是以無論是GPU光栅化,還是CPU光栅化,光栅化後的像素都要儲存在GPU中,後面才可以通過硬體加速将它們渲染在螢幕上。

       以上就是Chromium在渲染網頁的時候涉及到的重要概念以及關鍵流程。在實作過程中,這些概念和流程要更加複雜。接下來,我們就按照以下七個情景更詳細地對Chromium的網頁渲染機制進行分析:

       1. Layer Tree建立過程;

       2. 排程器的執行過程;

       3. Output Surface建立過程;

       4. 網頁繪制過程;

       5. Layer Tree與Pending Layer Tree同步過程;

       6. Tile光栅化過程;

       7. Pending Layer Tree激活為Active Layer Tree過程;

       了解了上述七個情景之後,我們對Chromium的網頁渲染機制就會有深刻的了解了,敬請關注!更多的資訊也可以關注老羅的新浪微網誌:http://weibo.com/shengyangluo。