Android圖形顯示系統——上層顯示1:界面繪製大綱

Android顯示之應用界面繪製

越到上層,跟業務關聯越直接,代碼就越繁雜,Android上層顯示的代碼正是如此。此外,java語言本身繁複的特點(比C語言多了滿屏的try-catch,比C++少了析構處理的優雅簡潔,和更高級的語言scala、python等就別比了),更加劇了這一現象。
直接去看代碼,往往會看得一頭霧水,知其然而不知其所以然。在這時候,就要把代碼扔掉,仔細去理清需要實現什麼,怎麼實現,畫一幅架構設計圖出來,然後再跟代碼去對比。Android這部分代碼並不是聖經,有很多待商榷的地方,心中要有主見,批判性地看。

由於中間各種事耽擱,加上懶,一直沒空寫長篇博文,間隔了很長一段時間,請讀者先回顧顯示概述與下層顯示:
http://blog.csdn.net/jxt1234and2010/article/details/44164691
另外,由於Android顯示還是有不少人寫的,某些模塊有寫得比較好的文章我就直接上鍊接,不自己寫了,見諒。
下層顯示關鍵詞:SurfaceFlinger
上層顯示關鍵詞:View

初步章節安排:
1、界面繪製
2、佈局計算
3、硬件加速下層實現
4、典型控件
5、資源管理

UI引擎設計原則

易用性

用戶是很懶的,其實程序員也一樣。 讓應用開發者直接使用OpenGL去開發界面,無異於讓他們赤手空拳打坦克。即便是使用圖形引擎的接口,也已經相當繁瑣了。
最理想的情形,是由編輯器搞定界面,所見即所得,配配參數就ok,如Unity3D。
一般都會提供足夠多的默認控件,但如果應用有更絢麗的效果要求,也會提供接口實現。

高效性

作爲UI引擎,掌控着足夠完整的渲染流程,優化空間是相當大的。相對而言,難度也更大。這個高效反映在兩方面,一是圖形引擎的高效,一是髒區域識別的高效。

圖形引擎的高效

第一個重要的點是下層圖形引擎的選用
圖形引擎的高效反映在兩個方面:單體性能和複合性能。單體性能即渲染單個物體的性能,複合性能則是指在多個物體一起渲染的性能(多個物體一起渲染,有一些優化手段,比如作遮擋判斷,消除非必要渲染,又比如作區域分劃,多線程繪製各區域上的物體)。
圖形引擎可以基於CPU渲染,也可以基於GPU渲染。就一般的UI渲染而言,CPU圖形引擎優化得足夠,倒也能滿足要求,不會比GPU引擎差多少。

識別髒區域

與遊戲界面的實時變換不同,對普通應用UI界面的渲染而言,大部分情況下一個頁面的大部分面積處於不變狀態。變化的區域又稱髒區域。如何儘可能多地識別不變的部分,並作渲染規避,是UI引擎需要完成的很重要的工作。

比較理想的UI引擎的設計結構如下圖:
UI引擎
應用開發者可以在三個層次上去實現UI效果。從上往下,自由度越來越高,開發難度也會越來越大。

Android的設計

UI引擎Android
Android並沒有開發新的界面語言,而是採用xml+java的形式。由xml文件確定大致佈局,java代碼中做控制和微調。Android沒有明確的UI解析引擎,UI解析反映在View、Layout等類的實現中。
應用開發者使用View的API(UI接口)、Canvas的API(引擎API)進行開發。

View

Android的控件和佈局管理都抽象爲View。部分View用於佈局解析(各種Layout),部分View用於管理(複合View),部分View是實際的控件(TextView、ImageView、WebView等)。
具體的渲染流程完全取決於應用所選擇的View的子類。
所有View組成一個樹,佈局時逐層創建樹節點,渲染時逐級渲染。當調用invalidate刷新View時,由下往上逐層上報dirty區域。
具體可看這篇文章,寫得比較清楚:
http://blog.csdn.net/xu_fu/article/details/7829721
一個View無論其渲染流程怎樣,都必須保證其繪製內容固定在屏幕的指定範圍,這是Android上層顯示的設計原則。對於使用系統的圖形引擎的應用,這可以通過在大圖層上劃分一塊區域,設置裁剪範圍而實現。但如果不使用系統圖形引擎,就只好新建一個圖層,並將主圖層對應位置挖洞。
在View的invalidate函數中,將需要重繪的View作標誌。並將其區域與上一級View的髒區域作合併,最終反映到ViewRootImpl的mDirty中來。
invalidate順着View樹脈絡,一層一層往上刷新。
Invalidate
invalidate之後,該View即需要繪製,即是dirty的。

Canvas

Canvas是Android系統提供的圖形引擎API,由於早期Android的圖形渲染由Skia完成,Canvas接口也與Skia的API非常像。
絕大部分控件使用Canvas的API進行界面渲染,如TextView、ImageView及用戶自定義,重載onDraw(Canvas canvas)的View。
比較特殊的是WebView,它不使用Canvas的API渲染,而是由Canvas獲取Surface信息後,走web引擎渲染。

繪製主線

觸發

衆所周知,ViewRootImpl類的performTraversals方法,是所有界面佈局、繪製的入口,但這個方法是怎麼觸發的呢?
在應用初起、View更新(觸發invalidate)、動畫、創建新Surface等情形下,會通過 scheduleTraversals 方法,向 Choreographer 類註冊一個回調,Choreographer 類是用來接受vsync信號的,這樣,在LCD發出vsync信號之後(也即新一幀開啓),該回調被執行,即doTraversal -> performTraversals。
詳情參見:
http://blog.csdn.net/farmer_cc/article/details/18619429

注:
1、performTraversals的調用是應用級的,也就是說,有可能會有多個應用去調這個函數。

主流程

流程
1、計算總大小,創建一個Surface用於存儲渲染結果。
2、進行佈局測量,算出每個View的範圍。
3、進行layout,實例化所有子View。
4、一切就緒,執行渲染。
詳細的看這篇文章吧:
http://blog.csdn.net/aaa2832/article/details/7849400
由這條繪製主線我們可以看出,跟View相關的一切操作,佈局,初始化,渲染,全部在一個線程(事實上是主線程)完成,如果在這個過程中,其他線程修改了View的屬性值,便會造成佈局計算後的結果與後面實際渲染的需求不一致。
Android裏面對此的解決方案是限制,即衆所周知的只能在主線程更新UI。

渲染流程

軟件渲染

drawSoftware
簡潔明快的流程:
1、調 surface.lockCanvas,取得渲染入口Canvas。
2、從頂層View開始,按樹遞歸調用View的draw方法。在draw方法中,所有View中的onDraw實現被調用。
3、調 surface.unlockCanvasAndPost
第1步對應的下層邏輯還是有點複雜的:
(1)dequeueBuffer獲取一塊新GraphicBuffer。
(2)將新GraphicBuffer鎖定(lock),指明爲CPU所訪問。
(3)優化:如果存在上一幀所渲染的GraphicBuffer,且長寬與當前窗口一致,那麼複製上一幀非dirty區域的內容到新一幀。如果不存在,將dirty區域設爲全屏(即所有區域都要渲染)。
(4)將GraphicBuffer映射爲一個SkBitmap,對應創建一個SkCanvas與之綁定,SkCanvas設置裁剪區域爲第(3)步得到的dirty區域。
(5)SkCanvas包裝爲上層的Canvas傳回。
第3步對應的下層邏輯就是 queueBuffer。
請注意,不是隻需要繪製dirty的View的,因爲View有可能會重疊,發生透明度混合,重疊部分影響到非dirty的View時,也應該繪製,Android並沒有計算哪些View需要重繪,就籠統地讓所有View執行onDraw方法。

軟件渲染流程中,佈局、渲染、事件響應全部集中在主線程,比較容易造成阻塞。

硬件渲染

爲何要有硬件渲染這套流程,而不是僅改造圖形引擎爲用gpu的呢?
這是因爲直接按軟件渲染那套流程走下來,是不適合用gpu渲染的,強行換用OpenGL實現,效率會低得可憐。
硬件加速中draw的實現在ThreadedRenderer.java之中(這是5.0的,不同版本可能有不同,重點看原理)。
1、把創建好的Surface扔給硬件加速的Renderer,供其初始化(eglCreateWindowSurface要用)。
2、更新顯示列表(updateRootDisplayList):創建一個記錄命令的Canvas,將View中對Canvas的draw操作變成記錄命令,非dirty的View不需要重新記錄。
3、執行渲染(nSyncAndDrawFrame)。這一步是放渲染線程裏面發一個任務,讓其做一次繪製,一般不需要等渲染線程繪製完成。
具體實現在 DrawTask的drawFrame函數,後續章節詳述:
frameworks/base/libs/hwui/renderthread/DrawFrameTask.cpp

從設計而言,硬件加速的渲染流程要比軟件渲染流程好一些,顯示列表的存在,給複合優化帶來可能,即使不用gpu加速,也都有優勢。

關於硬件加速幾個常見問題和誤區:
1、爲何開啓硬件加速要額外的內存?
很多文章裏面將其誤認爲是開啓OpenGL所需要的額外內存,其實不然。OpenGL上下文的內存消耗不會達到MB級,這個額外內存是hwui引擎所需要的緩存,大頭是字體。具體大小可以通過設置系統屬性修改。通過 adb shell getprop,可查看相關的屬性(ro.hwui開頭)。
HWUI
hwui內部機理是將文字解析到一個大的texture上,渲染具體文本時計算對應文字範圍,取此texture中的一部分。因此有一個寬/高的設置,不像skia裏面是一維的大小。
關於爲什麼要有字體緩存,可以看一下這篇文章:
http://mobile.51cto.com/abased-442805.htm
另外,在Android系統內存不足時,會去部分回收這個Cache。
2、顯示列表機制是否顯著提升了UI渲染性能?
顯著提升渲染性能靠的是GPU,顯示列表機制是將GPU用上的一種方法。
由於Android早期API全部基於CPU渲染,因此在UI渲染時所有資源(最主要還是圖像Bitmap)都在CPU所能訪問的內存中。GPU渲染時,必須要把對應的資源複製到顯存中。這一個複製的過程,自然不希望每一幀時都做一遍。
保存所有命令及相應資源到一個顯示列表上,然後回放,是一個可取的方案,其最大的好處是應用開發者仍然可以按原先的API進行開發,只需要打開一個開關就能用到硬件加速。
3、硬件加速是否可以使所有的界面繪製都用上GPU?
答案是否。請看下面的“非主線渲染”。

非主線渲染

就View層級設計而言,Android希望一個應用只有一個圖層,並在這個圖層上佈局所有的控件,並且應用不用感知這個圖層的內存所在,最多調Canvas接口即可,系統幫忙搞定圖形渲染、Buffer循環、送顯合成等繁瑣事務。
但很可惜,這種方案不能滿足所有需求:
1、對視頻、照相等應用而言,它們需要直接訪問物理內存(主要是硬件解碼器和ISP等需要),把它們的顯示放到一個圖層的部分區域,不太現實。
2、所有UI操作和繪製集中在主線程,即使是硬件加速,也需要在主線程創建顯示列表,做動畫時,容易阻塞事件響應。
3、這種方案下,應用開發者無法自定義渲染流程,直接使用OpenGL等圖形API進行開發,這樣意味着使用不了遊戲引擎。

SurfaceView應運而生,它的原理,就是打洞覆蓋:另起一個圖層(即新建一個Surface),並把主圖層的相應區域置爲透明,然後渲染就發生在新圖層中,最終顯示效果自然是依賴SurfaceFlinger的疊加。
用法參考:
http://blog.csdn.net/ithomer/article/details/7280968
其中,SurfaceHolder往下會對應着一個Buffer循環隊列,這個是物理共享內存的抽象,因此可以做爲視頻、相機預覽流的指定輸入。
網上的教程中,SurfaceView的用法都在另一個線程中,先lockCanvas,調用Canvas的接口繪製畫面之後,調unlockCanvasAndPost。這種方式,便是典型的調CPU引擎-Skia渲染的方式。
儘管應用開發者可以用SurfaceView直接開發基於OpenGL渲染的程序(SurfaceHolder可以用於創建OpenGL上下文),Google還是很仁慈地提供了GLSurfaceView,這個類幫開發者創建好了上下文和相應的渲染線程,開發者可以直接在回調函數中使用OpenGL,簡單很多。

請注意:
1、SurfaceView不會自動起一個單獨的線程去渲染,只是這個View上面的渲染可以在任意線程完成。開發者執意在主線程去渲染這個View,也是可以的,就像以前QQ某一版的引導頁一樣,CPU差一點的機器滑都滑不動(淨給我們這些做系統優化的出難題)。
2、SurfaceView雖然可以把渲染流程移到另一個線程執行,但它的存在同時增加了SurfaceFlinger的合成負擔(圖層數增加),不要以爲這就是一個很高效的View,如果是出於提升性能的目的而使用,請仔細權衡一下得失。
3、硬件加速屬性不影響SurfaceView的渲染方式,lockCanvas必然得到用CPU繪製的Canvas。要在SurfaceView中用上GPU渲染,只好自已建上下文或用GLSurfaceView,接入3D引擎。補充,2015.8.14之後,Android提供了一個lockHardwareCanvas方法,用此方法可以得到硬件加速的Canvas,Android 6.0上已經可以使用,這可是個大福音
4、SurfaceView系列的渲染流程不在performTraversals主線中,因此一般也不受vsync限制(當然,可以設計流程使之受限),也不會像主線渲染必須由invalidate觸發。不過,如果渲染太快,在下層顯示的窗口管理模塊,可以使之阻塞在申請Buffer的步驟上。

Android的設計吐槽

Android的發展也有些年頭了,圖形顯示部分更是一改再改,幾乎面目全非,總算是滿足了手機硬件發展的需求,實現了一個比較高效,對開發者相對友好的界面繪製系統,相對於其他系統來說,其實也算優秀了。然而,作爲一個逐漸演進的複雜系統,揹負着不少歷史的包袱,總會有各種各樣的不合理,這裏就來吐槽一下:
1、主線程單一管理界面
個人認爲的最大槽點,沒有之一。所有UI操作集中到一個線程後無法並行,而measure/layout/draw都是耗時大戶。在應用啓動、屏幕旋轉、列表滑動等場景,屢屢出現性能問題。ART模式開啓,加快了java代碼執行效率後,好了一些,但指標仍然不好看。

2、髒區域識別之後並沒有充分地優化
軟件渲染時,儘管限制了渲染區域,但所有View的onDraw方法一個不丟的執行了一遍。
硬件渲染時,避免了沒刷新的View調onDraw方法更新顯示列表,但顯示列表中的命令仍然一個不落的在全屏幕上執行了一遍。
一個比較容易想到的優化方案就是爲主流程中的View建立一個R-Tree索引,invalidate這一接口修改爲可以傳入一個矩形範圍R,更新時,利用R-Tree索引找出包含R的所有葉子View,令這些View在R範圍重繪一次即可。

這個槽點其實影響倒不是很大,大部分情況下View不多,且如果出現性能問題,基本上都是一半以上的屏幕刷新。

3、圖層分配方案比較浪費內存和內存傳輸帶寬(DDR帶寬)
下圖是對小米平板上相機應用 dumpsys SurfaceFlinger的一個結果
小米相機
由上圖可以看出,SurfaceView的Layer(相機的預覽Surface)和com.android.camera的Layer(主渲染流程的Surface)是一樣大的,都差不多佔了全屏。
但實際上,com.android.camera只有幾個圖標,這個Layer絕大部分是透明的。考慮到TrippleBuffer機制,按透明部分約爲1024*2048的大小算,就浪費了1024*2048*4*3=24M的內存。
而且在SurfaceFlinger作合成時,透明部分也要參與,按最省內存傳輸帶寬的在線合成(只需要一讀)方式,預覽幀按30fps算,透明部分所需要的DDR帶寬就是8M*30/s = 240M/s。一般手機上的DDR帶寬才800M/s(高端手機應該有1600),這就佔用了幾乎1/3。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章