天天看點

Android圖形顯示系統——上層顯示1:界面繪制大綱Android顯示之應用界面繪制

越到上層,跟業務關聯越直接,代碼就越繁雜,android上層顯示的代碼正是如此。此外,java語言本身繁複的特點(比c語言多了滿屏的try-catch,比c++少了析構處理的優雅簡潔,和更進階的語言scala、python等就别比了),更加劇了這一現象。

直接去看代碼,往往會看得一頭霧水,知其然而不知其是以然。在這時候,就要把代碼扔掉,仔細去理清需要實作什麼,怎麼實作,畫一幅架構設計圖出來,然後再跟代碼去對比。android這部分代碼并不是聖經,有很多待商榷的地方,心中要有主見,批判性地看。

由于中間各種事耽擱,加上懶,一直沒空寫長篇博文,間隔了很長一段時間,請讀者先回顧顯示概述與下層顯示:

<a href="http://blog.csdn.net/jxt1234and2010/article/details/44164691">http://blog.csdn.net/jxt1234and2010/article/details/44164691</a>

另外,由于android顯示還是有不少人寫的,某些子產品有寫得比較好的文章我就直接上連結,不自己寫了,見諒。

下層顯示關鍵詞:surfaceflinger

上層顯示關鍵詞:view

初步章節安排:

1、界面繪制

2、布局計算

3、硬體加速下層實作

4、典型控件

5、資源管理

使用者是很懶的,其實程式員也一樣。 讓應用開發者直接使用opengl去開發界面,無異于讓他們赤手空拳打坦克。即便是使用圖形引擎的接口,也已經相當繁瑣了。

最理想的情形,是由編輯器搞定界面,所見即所得,配配參數就ok,如unity3d。

一般都會提供足夠多的預設控件,但如果應用有更絢麗的效果要求,也會提供接口實作。

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

第一個重要的點是下層圖形引擎的選用

圖形引擎的高效反映在兩個方面:單體性能和複合性能。單體性能即渲染單個物體的性能,複合性能則是指在多個物體一起渲染的性能(多個物體一起渲染,有一些優化手段,比如作遮擋判斷,消除非必要渲染,又比如作區域分劃,多線程繪制各區域上的物體)。

圖形引擎可以基于cpu渲染,也可以基于gpu渲染。就一般的ui渲染而言,cpu圖形引擎優化得足夠,倒也能滿足要求,不會比gpu引擎差多少。

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

比較理想的ui引擎的設計結構如下圖:

Android圖形顯示系統——上層顯示1:界面繪制大綱Android顯示之應用界面繪制

應用開發者可以在三個層次上去實作ui效果。從上往下,自由度越來越高,開發難度也會越來越大。

Android圖形顯示系統——上層顯示1:界面繪制大綱Android顯示之應用界面繪制

android并沒有開發新的界面語言,而是采用xml+java的形式。由xml檔案确定大緻布局,java代碼中做控制和微調。android沒有明确的ui解析引擎,ui解析反映在view、layout等類的實作中。

應用開發者使用view的api(ui接口)、canvas的api(引擎api)進行開發。

android的控件和布局管理都抽象為view。部分view用于布局解析(各種layout),部分view用于管理(複合view),部分view是實際的控件(textview、imageview、webview等)。

具體的渲染流程完全取決于應用所選擇的view的子類。

所有view組成一個樹,布局時逐層建立樹節點,渲染時逐級渲染。當調用invalidate重新整理view時,由下往上逐層上報dirty區域。

具體可看這篇文章,寫得比較清楚:

<a href="http://blog.csdn.net/xu_fu/article/details/7829721">http://blog.csdn.net/xu_fu/article/details/7829721</a>

一個view無論其渲染流程怎樣,都必須保證其繪制内容固定在螢幕的指定範圍,這是android上層顯示的設計原則。對于使用系統的圖形引擎的應用,這可以通過在大圖層上劃分一塊區域,設定裁剪範圍而實作。但如果不使用系統圖形引擎,就隻好建立一個圖層,并将主圖層對應位置挖洞。

在view的invalidate函數中,将需要重繪的view作标志。并将其區域與上一級view的髒區域作合并,最終反映到viewrootimpl的mdirty中來。

invalidate順着view樹脈絡,一層一層往上重新整理。

Android圖形顯示系統——上層顯示1:界面繪制大綱Android顯示之應用界面繪制

invalidate之後,該view即需要繪制,即是dirty的。

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 -&gt; performtraversals。

詳情參見:

<a href="http://blog.csdn.net/farmer_cc/article/details/18619429">http://blog.csdn.net/farmer_cc/article/details/18619429</a>

注:

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

Android圖形顯示系統——上層顯示1:界面繪制大綱Android顯示之應用界面繪制

1、計算總大小,建立一個surface用于存儲渲染結果。

2、進行布局測量,算出每個view的範圍。

3、進行layout,執行個體化所有子view。

4、一切就緒,執行渲染。

詳細的看這篇文章吧:

<a href="http://blog.csdn.net/aaa2832/article/details/7849400">http://blog.csdn.net/aaa2832/article/details/7849400</a>

由這條繪制主線我們可以看出,跟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開頭)。

Android圖形顯示系統——上層顯示1:界面繪制大綱Android顯示之應用界面繪制

hwui内部機理是将文字解析到一個大的texture上,渲染具體文本時計算對應文字範圍,取此texture中的一部分。是以有一個寬/高的設定,不像skia裡面是一維的大小。

關于為什麼要有字型緩存,可以看一下這篇文章:

<a href="http://mobile.51cto.com/abased-442805.htm">http://mobile.51cto.com/abased-442805.htm</a>

另外,在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的疊加。

用法參考:

<a href="http://blog.csdn.net/ithomer/article/details/7280968">http://blog.csdn.net/ithomer/article/details/7280968</a>

其中,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的發展也有些年頭了,圖形顯示部分更是一改再改,幾乎面目全非,總算是滿足了手機硬體發展的需求,實作了一個比較高效,對開發者相對友好的界面繪制系統,相對于其他系統來說,其實也算優秀了。然而,作為一個逐漸演進的複雜系統,背負着不少曆史的包袱,總會有各種各樣的不合理,這裡就來吐槽一下:

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的一個結果

Android圖形顯示系統——上層顯示1:界面繪制大綱Android顯示之應用界面繪制

由上圖可以看出,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。

繼續閱讀