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效果。
從上往下。自由度越來越高。開發難度也會越來越大。
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之后,該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方法。
軟件渲染流程中,布局、渲染、事件響應全部集中在主線程,比較easy造成堵塞。
硬件渲染
為何要有硬件渲染這套流程。而不是僅改造圖形引擎為用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內部機理是將文字解析到一個大的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操作和繪制集中在主線程,即使是硬件加速,也須要在主線程創建顯示列表,做動畫時,easy堵塞事件響應。
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方法更新顯示列表。但顯示列表中的命令仍然一個不落的在全屏幕上運行了一遍。
一個比較easy想到的優化方案就是為主流程中的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。
