Android WebView簡要介紹和學習計劃

我們通?;嵩贏pp的UI中嵌入WebView,用來實現某些功能的動態更新。在4.4版本之前,Android WebView基于WebKit實現。不過,在4.4版本之后,Android WebView就換成基于Chromium的實現了?;贑hromium實現,使得WebView可以更快更流暢地顯示網頁。本文接下來就介紹Android WebView基于Chromium的實現原理,以及制定學習計劃。

       通過前面幾個系列文章的學習,我們知道,Chromium的實現是相當復雜的。這種復雜可以體現在編譯出來的動態庫的大小上,帶符號版本1.3G左右,去掉符號后還有27M左右。編譯過AOSP源碼的都知道,在整個編譯過程中,Chromium庫占用了大部分的時間,尤其是在生成上述1.3G文件時,電腦幾乎卡住了。因此,在使用Chromium實現WebView時,第一個要解決的問題就是它的動態庫加載問題。

       Android系統的動態庫是ELF文件結構。ELF文件由ELF Header、Program Header Table、Section以及Section Header Table組成。ELF Header位于文件的開頭,它同時描述了Program Header Table和Section Header Table在文件中的偏移位置。Section Header Table描述了動態庫由哪些Section組成,典型的Section有.text、.data、.bss和.rodata等。這些Section描述的要么是程序代碼,要么是程序數據。Program Header Table描述了動態庫由哪些Segment組成。一個Segment又是由一個或者若干個Section組成的。Section信息是在程序鏈接期間用到的,而Segment信息是在程序加載期間用到的。關于ELF文件格式的更詳細描述,可以參考Executable and Linkable Format。

       對于動態庫來說,它的程序代碼是只讀的。這意味著一個動態庫不管被多少個進程加載,它的程序代碼都是只有一份。但是,動態庫的程序數據,鏈接器在加載的時候,會為每一個進程都獨立加載一份。對于Chromium動態庫來說,它的程序代碼占據了95%左右的大小,也就是25.65M左右。剩下的5%,也就是1.35M,是程序數據。假設有N個App使用了WebView,并且這個N個App的進程都存在系統中,那么系統就需要為Chromium動態庫分配(25.65 + 1.35 × N)M內存。這意味著,N越大,Chromium動態庫就占用越多的系統內存。

       在Chromium動態庫1.35M的程序數據中,大概有1.28M在加載后經過一次重定位操作之后就不會發生變化。這些數據包含C++虛函數表,以及所有指針類型的常量。它們會被鏈接器放在一個稱為GNU_RELRO Section中,如圖1所示:


圖1 App進程間不共享GNU_RELRO Section

       如果我們將Chromium動態庫的GNU_RELRO Section看成是普通的程序數據,那么Android系統就需要為每一個使用了WebView的App進程都分配1.28M的內存。這將會造成內存浪費。如果我們能App進程之間共享Chromium動態庫的GNU_RELRO Section,那么不管有多少個App進程使用了WebView,它占用的內存大小都是1.28M,如圖2所示:


圖2 App進程間共享GNU_RELRO Section

       這是有可能做到的,畢竟它與程序代碼類似,在運行期間都是只讀的。不同的地方在于,程序代碼在加載后自始至終都不用修改,而GNU_RELRO Section的內容在重定位期間,是需要進行一次修改的,之后才是只讀的。但是修改的時候,Linux的COW(Copy On Write)機制就會使得它不能再在各個App進程之間實現共享。

       為了使得Chromium動態庫能在不同的App進程之間共享,Android系統執行了以下操作:

       1. Zygote進程在啟動的過程中,為Chromium動態庫保留了一段足夠加載它的虛擬地址內間。我們假設這個虛擬地址空間的起始地址為gReservedAddress,大小為gReservedSize。

       2. System進程在啟動的過程中,會請求Zygote進程fork一個子進程,并且在上述保留的虛擬地址空間[gReservedAddress, gReservedAddress + gReservedSize)中加載Chromium動態庫。Chromium動態庫的Program Header指定了它的GNU_RELRO Section的加載位置。這個位置是相對基地址gReservedAddress的一個偏移量。我們假設這個偏移量為gRelroOffset。上述子進程完成Chromium動態庫的GNU_RELRO Section的重定位操作之后,會將它的內容寫入到一個RELRO文件中去。

       3. App進程在創建WebView的時候,Android系統也會在上述保留的虛擬地址空間[gReservedAddress, gReservedAddress + gReservedSize)中加載Chromium動態庫,并且會直接將第2步得到的RELRO文件內存映射到虛擬地址空間[gReservedAddress + gRelroOffset,  gReservedAddress + gRelroOffset + 1.28)去。

       關于Zygote進程、System進程和App進程的啟動過程,可以參考Android系統進程Zygote啟動過程的源代碼分析和Android應用程序進程啟動過程的源代碼分析這兩篇文章。

       Android 5.0的Linker提供了一個新的動態庫加載函數android_dlopen_ext。這個函數不僅可以將一個動態庫加載在指定的虛擬地址空間中,還可以在該動態庫重定位操作完成后,將其GNU_RELRO Section的內容寫入到指定的RELRO文件中去,同時還可以在加載一個動態庫時,使用指定的RELRO文件內存映射為它的GNU_RELRO Section。因此,上述的第2步和第3步可以實現。函數android_dlopen_ext還有另外一個強大的功能,它可以從一個指定的文件描述符中讀入要加載的動態庫內容。通常我們是通過文件路徑指定要加載的動態庫,有了函數android_dlopen_ext之后,我們就不再受限于從本地文件加載動態庫了。

       接下來,我們進一步分析為什么上述3個操作可以使得Chromium動態庫能在不同的App進程之間共享。

       首先,第2步的子進程、第3步的App進程都是由第1步的Zygote進程fork出來的,因此它們都具有一段保留的虛擬地址空間[gReservedAddress, gReservedAddress + gReservedSize)。

       其次,第2步的子進程和第3步的App進程,都是將Chromium動態庫加載在相同的虛擬地址空間中,因此,可以使用前者生成的RELRO文件內存映射為后者的GNU_RELRO Section。

       第三,所有使用了WebView的App進程,都將相同的RELRO文件內存映射為自己加載的Chromium動態庫的GNU_RELRO Section,因此就實現了共享。

       App進程加載了Chromium動態庫之后,就可以啟動Chromium渲染引擎了。Chromium渲染引擎實際上是由Browser進程、Render進程和GPU進程組成的。其中,Browser進程負責將網頁的UI合成在屏幕上,Render進程負責加載和渲染網頁的UI,GPU進程負責執行Browser進程和Render進程發出的GPU命令。由于Chromium也支持單進程架構(在這種情況下,它的Browser進程、Render進程和GPU進程都是通過線程模擬的),因此接下來我們將它的Browser進程、Render進程和GPU進程統稱為Browser端、Render端和GPU端。相應地,啟動Chromium渲染引擎,就是要啟動它的Browser端、Render端和GPU端。

       對于Android WebView來說,它啟動Chromium渲染引擎的過程如圖3所示:


圖3 Android WebView啟動Chromium渲染引擎的過程

       當我們在App的UI中嵌入一個WebView時,WebView內部會創建一個WebViewChromium對象。從名字就可以看出,WebViewChromium是基于Chromium實現的WebView。Chromium里面有一個android_webview???。這個??樘峁┝肆礁隼郃wBrowserProcess和AwContents,分別用來封裝Chromium的Content層提供的兩個類BrowserStartupController和ContentViewCore。

       從前面Chromium硬件加速渲染的OpenGL上下文繪圖表面創建過程分析一文可以知道,通過Chromium的Content層提供的BrowserStartupController類,可以啟動Chromium的Browser端。不過,對于Android WebView來說,它并沒有單獨的Browser進程,它的Browser端是通過App進程的主線程(即UI線程)實現的。      

       Chromium的Content層會通過BrowserStartupController類在App進程的UI線程中啟動一個Browser Main Loop。以后需要請求Chromium的Browser端執行某一個操作時,就向這個Browser Main Loop發送一個Task即可。這個Browser Main Loop最終會在App進程的UI線程中執行請求的Task。

       從前面Chromium網頁Frame Tree創建過程分析一文可以知道,通過Chromium的Content層提供的ContentViewCore類,可以創建一個Render進程,并且在這個Render進程中加載指定的網頁。不過,對于Android WebView來說,它是一個單進程架構,也就是它沒有單獨的Render進程用來加載網頁。這時候ContentViewCore類將會創建一個線程來模擬Render進程。也就是說,Android WebView的Render端是通過在App進程中創建的一個線程實現的。這個線程稱為In-Process Renderer Thread。

       現在,Android WebView具有Browser端和Render端了,它還需要有一個GPU端。從前面Chromium的GPU進程啟動過程分析一文可以知道,在Android平臺上,Chromium的GPU端是通過在Browser進程中創建一個GPU線程實現的。不過,對于Android WebView來說,它并沒有一個單獨的GPU線程。那么,Chromium的GPU命令由誰來執行呢?

       我們知道,從Android 3.0開始,App的UI就支持硬件加速渲染了,也就是支持通過GPU來渲染。到了Android 4.0,App的UI默認就是硬件加速渲染了。這時候App的UI線程就是一個OpenGL線程,也就是它可以用來執行GPU命令。再到Android 5.0,App的UI線程只負責收集UI渲染操作,也就是構建一個Display List。保存在這個Display List中的操作,最終會交給一個Render Thread執行。Render Thread又是通過GPU來執行這些渲染操作的。這時候App的UI線程不再是一個OpenGL線程,Render Thread才是。Android WebView的GPU命令就是由這個Render Thread執行的。當然,在Android 4.4時,Android WebView的GPU命令還是由App的UI線程執行的。這里我們只討論Android 5.0的情況,具體可以參考Android應用程序UI硬件加速渲染技術簡要介紹和學習計劃這個系列的文章。

       Chromium在android_webview??櫓刑峁┝艘桓鯠eferredGpuCommandService服務。當Android WebView的Browser端和Render端需要執行GPU命令時,它們就會通過DeferredGpuCommandService服務提供的RequestProcessGL接口向App的Display List增加一個類型為DrawFunctorOp的操作。當Display List從UI線程同步給Render Thread的時候,它里面包含的DrawFunctorOp操作就會被執行。這時候Android WebView的Browser端和Render端請求的GPU命令就會在App的Render Thread中得到執行了。

       Chromium為Android WebView獨特的GPU命令執行方式(既不是在單獨的GPU進程中執行,也不是在單獨的GPU線程中執行)提供了一個稱為In-Process Command Buffer GL的接口。顧名思義,In-Process Command Buffer GL與Command Buffer GL接口一樣,要執行的GPU命令都是先寫入到一個Command Buffer中。然而,這個Command Buffer會提交給App的Render Thread進程執行,過程如圖4所示:


圖4 Android WebView通過In-Process Command Buffer GL接口執行GPU命令的過程

       In-Process Command Buffer GL接口與前面Chromium硬件加速渲染的OpenGL命令執行過程分析一文分析的Command Buffer GL接口一樣,都是通過GLES2Implementation類描述的,區別在于前者通過一個InProcessCommandBuffer對象執行GPU命令,而后者通過一個CommandBufferProxyImpl對象執行GPU命令。更具體來說,就是CommandBufferProxyImpl對象會將要執行的GPU命令提交給Chromium的GPU進程/線程處理,而InProcessCommandBuffer對象會將要執行的GPU命令提交給App的Render Thread處理。

       GLES2Implementation類是通過InProcessCommandBuffer類的成員函數Flush請求它處理已經寫入在Command Buffer中的GPU命令的。InProcessCommandBuffer類的成員函數Flush又會請求前面提到的DeferredGpuCommandService服務調度一個Task。這個Task綁定了InProcessCommandBuffer類的成員函數FlushOnGpuThread,意思就是說要在App的Render Thread線程中調用InProcessCommandBuffer類的成員函數FlushOnGpuThread。InProcessCommandBuffer類的成員函數FlushOnGpuThread在調用的過程中,就會執行已經寫入在Command Buffer中的GPU命令。

       DeferredGpuCommandService服務接收到調度Task的請求之后,會判斷當前線程是否允許執行GL操作,實際上就是判斷當前線程是否就是App的Render Thread。如果是的話,那么就會直接調用請求調度的Task綁定的InProcessCommandBuffer類的成員函數FlushOnGpuThread。這種情況發生在Browser端合成網頁UI的過程中。Browser端合成網頁UI的操作是由App的Render Thread主動發起的,因此這個合成操作就可以在當前線程中直接執行。

       另一方面,DeferredGpuCommandService服務接收到調度Task的請求之后,如果發現當前不允許執行GL操作,那么它就會將該Task保存在內部一個Task隊列中,并且通過調用Java層的DrawGLFunctor類的成員函數requestDrawGL請求App的UI線程調度執行一個GL操作。這種情況發生在Render端渲染網頁UI的過程中。從前面Chromium網頁渲染機制簡要介紹和學習計劃這個系列的文章可以知道,Render端渲染網頁UI的操作發生在內部的一個Compositor線程中。這個Compositor線程不是一個OpenGL線程,它不能直接執行GL的操作,因此就要請求App的UI線程調度執行GL操作。

       從前面Android應用程序UI硬件加速渲染技術簡要介紹和學習計劃這個系列的文章可以知道,App的UI線程主要是負責收集當前UI的渲染操作,并且通過一個DisplayListRenderer將這些渲染操作寫入到一個Display List中去。這個Display List最終會同步到App的Render Thread中去。App的Render Thread獲得了這個Display List之后,就會通過調用相應的OpenGL函數執行這些渲染操作,也就是通過GPU來執行這些渲染操作。這個過程稱為Replay(重放) Display List。重放完成之后,就可以生成App的UI了。

       DrawGLFunctor類的成員函數requestDrawGL請求App的UI線程調度執行的GL操作是一個特殊的渲染操作,它對應的是一個GL函數,而一般的渲染操作是指繪制一個Circle、Rect或者Bitmap等基本操作。GL函數在執行期間,可以執行任意的GPU命令。App的UI線程會調用DisplayListRenderer類的成員函數callDrawGLFunction將請求調度執行的GL操作封裝成一個DrawFunctionOp操作,并且保存在App UI的Display List中。當這個Display List被App的Render Thread重放時,它里面包含的DrawFunctionOp操作就會被OpenGLRenderer類的成員函數callDrawGLFunction執行。

       OpenGLRenderer類的成員函數callDrawGLFunction在執行DrawFunctionOp操作時,會調用在Chromium的android_webview??櫓惺迪值囊桓鯣L函數。這個GL函數又會找到一個HardwareRenderer對象。這個HardwareRenderer對象是Android WebView的Browser端用來合成網頁的UI的。有了這個HardwareRenderer對象之后,上述GL函數就會調用它的成員函數DrawGL,以便請求前面提到的DeferredGpuCommandService服務執行保存在它內部的Task,這是通過調用它的成員函數PerformIdleTask實現的。

       DeferredGpuCommandService類的成員函數PerformIdleTask會依次執行保存在內部Task隊列的每一個Task。從前面的分析可以知道,這時候Task隊列中存在一個Task。這個Task綁定了InProcessCommandBuffer類的成員函數FlushOnGpuThread。因此,這時候InProcessCommandBuffer類的成員函數FlushOnGpuThread就會在當前線程中被調用,也就是在App的Render Thread中被調用。

       前面提到,InProcessCommandBuffer類的成員函數FlushOnGpuThread在調用的過程中,會執行之前通過In-Process Command Buffer GL接口寫入在Command Buffer中的GPU命令。InProcessCommandBuffer類的成員函數FlushOnGpuThread又是通過內部的一個GpuScheduler對象和一個GLES2Decoder對象執行這些GPU命令的。其中,GpuScheduler對象負責將GPU命令調度在它們對應的OpenGL上下文中執行,而GLES2Decoder對象負責將GPU命令翻譯成OpenGL函數執行。關于GpuScheduler類和GLES2Decoder類執行GPU命令的過程,可以參考前面前面Chromium硬件加速渲染的OpenGL命令執行過程分析一文。

       通過以上描述的方式,Android WebView的Browser端和Render端就可以執行任意的GPU命令了。其中,Render端執行GPU命令是為了渲染網頁的UI,而Browser端執行GPU命令是為了將Render端渲染出來的網頁UI合成在App的UI中,以便最后可以顯示在屏幕中。這個過程就是Android WebView硬件加速渲染網頁的過程,如圖5所示:


圖5 Android WebView硬件加速渲染網頁過程

       從前面Android應用程序UI硬件加速渲染技術簡要介紹和學習計劃這個系列的文章可以知道,App從接收到VSync信號開始渲染一幀UI。兩個VSync信號時間間隔由屏幕刷新頻率決定。一般的屏幕刷新頻率是60fps,這意味著每一個VSync信號到來時,App有16ms的時間繪制自己的UI。

       這16ms時間又劃分為三個階段。第一階段用來構造App UI的Display List。這個構造工作由App的UI線程執行,表現為各個需要更新的View的成員函數onDraw會被調用。第二階段App的UI線程會將前面構造的Display List同步給Render Thread。這個同步操作由App的Render Thread執行,同時App的UI線程會被阻塞,直到同步操作完成為止。第三階段是繪制App UI的Display List。這個繪制操作由App的Render Thread執行。

       由于Android WebView是嵌入在App的UI里面的,因此它每一幀的渲染也是按照上述三個階段進行的。從前面Chromium網頁渲染機制簡要介紹和學習計劃這個系列的文章可以知道,網頁的UI最終是通過一個CC Layer Tree描述的,也就是Android WebView的Render端會創建一個CC Layer Tree來描述它正在加載的網頁的UI。與此同時,Android WebView還會為Render端創建一個Synchronous Compositor,用來將網頁的UI渲染在一個Synchronous Compositor Output Surface上。渲染得到的最終結果通過一個Compositor Frame描述。這意味網頁的每一幀都是通過一個Compositor Frame描述的。

       Android WebView的Browser端負責合成Render端渲染出來的UI。為了完成這個合成操作,它同樣會創建一個CC Layer Tree。這個CC Layer Tree只有兩個節點,一個是根節點,另外一個是根節點的子節點,稱為Delegated Renderer Layer。這個Delegated Renderer Layer的內容來自于Render端的渲染結果,也就是一個Compositor Frame。與此同時,Android WebView會為Browser端創建一個Hardware Renderer,用來將它的CC Layer Tree渲染在一個Parent Output Surface上,實際上就是將網頁的UI合成在App的窗口上。

       在第一階段,Android WebView的成員函數onDraw會在App的UI線程中被調用。在調用期間,它又會調用為Render端創建的Synchronous Compositor的成員函數DemandDrawHw,用來渲染Render端的CC Layer Tree。渲染完成后,為Render端創建的Synchronous Compositor Output Surface的成員函數SwapBuffers就會被調用,并且交給它一個Compositor Frame。這個Compositor Frame描述的就是網頁當前的UI,它最終會被保存在一個SharedRendererState對象中。這個SharedRendererState對象是用來描述網頁的狀態的。

       在第二階段,保存在上述SharedRendererState對象中的Compositor Frame會被提取出來,并且同步給Browser端的CC Layer Tree,也就是設置為該CC Layer Tree的Delegated Renderer Layer的內容。

       在第三階段,Android WebView為Browser端創建的Hardware Renderer的成員函數DrawGL會在App的Render Thread中被調用,用來渲染Browser端的CC Layer Tree。渲染完成后,為Browser端創建的Parent Output Surface的成員函數SwapBuffers就會被調用,這時候它就會將得到的網頁UI繪制在App的窗口上。

       以上就是Android WebView以硬件加速方式將網頁UI渲染在App窗口的過程。當然,Android WebView也可用軟件方式將網頁UI渲染在App窗口。不過,在這個系列的文章中,我們只要關注硬件加速渲染方式,因為這種渲染方式效率會更高,而且Android系統從4.0版本之后,默認已經是使用硬件加速方式渲染App的UI了。

       接下來,我們就結合源碼,按照以下四個情景,詳細分析Android WebView硬件加速渲染網頁UI的過程:

       1. Android WebView加載Chromium動態庫的過程;

       2. Android WebView啟動Chromium渲染引擎的過程;

       3. Android WebView執行OpenGL命令的過程;

       4. Android WebView硬件加速渲染網頁UI的過程。

來源:CSDN

上一篇: Google 將于 2017 年結束支持 Android 2.3/3.0

下一篇: 小米在印度狂飆突進的同時也遭瘋狂吐槽

分享到: 更多
在线21点游戏平台 特马彩图 河北11选5计划软件下载 mg游戏是指 重庆时时单双技巧 北京pk10赛车公式 腾讯分分计划软件下载 凯尔特人 重庆时时开彩龙虎和 福山东时时 稳赚包平特一肖三期 pk106码那怎么倍投 888sl备用网址 山东时时开奖号码 幸运飞艇彩票计划软件下载 三公怎么玩法介绍