Android視圖的繪制流程
Android中視圖的繪制會經歷三個階段即onMeasure()、onLayout()和onDraw()
一、OnMeasure()
Measure是測量的意思,測量視圖的大小。Viewd的繪制流程會從ViewRoot的performTraversals()方法中開始,在其內部調用View的measure()方法。measure()方法接收兩個參數
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}```
widthMeasureSpec和heightMeasureSpec,這兩個值分別用于確定視圖的寬度和高度的規格和大小。MeasureSpec的值由specSize和specMode共同組成的,其中specSize記錄的是大小,specMode記錄的是規格。specMode一共有三種類型,如下所示:
* 1、 EXACTLY表示父視圖希望子視圖的大小應該是由specSize的值來決定的,系統默認會按照這個規則來設置子視圖的大小。
* 2、AT_MOST表示子視圖最多只能是specSize中指定的大小,開發人員應該盡可能小得去設置這個視圖,并且保證不會超過specSize。系統默認會按照這個規則來設置子視圖的大小。
* 3、 UNSPECIFIED表示開發人員可以將視圖按照自己的意愿設置成任意的大小,沒有任何限制。這種情況比較少見,不太會用到。
###二、onLayout()
measure過程結束后,視圖的大小就已經測量好了,接下來就是layout的過程了。正如其名字所描述的一樣,這個方法是用于給視圖進行布局的,也就是確定視圖的位置。ViewRoot的performTraversals()方法會在measure結束后繼續執行,并調用View的layout()方法來執行此過程。
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom){
super.onLayout(changed, left, top, right, bottom);
}
layout()方法接收四個參數,分別代表著左、上、右、下的坐標,當然這個坐標是相對于當前視圖的父視圖而言的
###三、onDraw()
measure和layout的過程都結束后,接下來就進入到draw的過程了。同樣,根據名字你就能夠判斷出,在這里才真正地開始對視圖進行繪制。ViewRoot中的代碼會繼續執行并創建出一個Canvas對象,然后調用View的draw()方法來執行具體的繪制工作。
@Overrideprotected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
通過上面的分析可以得知,view視圖的測量繪制可以總結成如下的流程
##Android視圖刷新機制
###一、渲染原理
App的卡頓最主要的根源是渲染性能的問題,APP有更多的動畫和圖片的時候,但是Android系統很有可能無法及時完成那些復雜的界面渲染操作。Android系統每隔16ms發出VSYNC信號,觸發對UI進行渲染,如果每次渲染都成功,這樣就能夠達到流暢的畫面所需要的60fps,為了能夠實現60fps,這意味著程序的大多數操作都必須在16ms內完成。

如果某個操作花費時間是24ms,系統在得到VSYNC信號的時候就無法進行正常渲染,這樣就發生了丟幀現象。那么用戶在32ms內看到的會是同一幀畫面。

用戶容易在UI執行動畫或者滑動ListView的時候感知到卡頓不流暢,是因為這里的操作相對復雜,容易發生丟幀的現象,從而感覺卡頓。有很多原因可以導致丟幀,也許是因為你的layout太過復雜,無法在16ms內完成渲染,有可能是因為你的UI上有層疊太多的繪制單元,還有可能是因為動畫執行的次數過多。這些都會導致CPU或者GPU負載過重。我們可以通過一些工具來定位問題,比如可以使用HierarchyViewer來查找Activity中的布局是否過于復雜,也可以使用手機設置里面的開發者選項,打開Show GPU Overdraw等選項進行觀察。你還可以使用TraceView來觀察CPU的執行情況,更加快捷的找到性能瓶頸。
###二、過度繪制
過度繪制描述的是屏幕上的某個像素在同一幀的時間內被繪制了多次。在多層次的UI結構里面,如果不可見的UI也在做繪制的操作,這就會導致某些像素區域被繪制了多次。這就浪費大量的CPU以及GPU資源。當設計上追求更華麗的視覺效果的時候,我們就容易陷入采用越來越多的層疊組件來實現這種視覺效果的怪圈。這很容易導致大量的性能問題,為了獲得最佳的性能,我們必須盡量減少Overdraw的情況發生。我們可以通過手機設置里面的開發者選項,打開Show GPU Overdraw的選項,可以觀察UI上的Overdraw情況。藍色,淡綠,淡紅,深紅代表了4種不同程度的過度繪制情況,我們的目標就是盡量減少紅色Overdraw,看到更多的藍色區域。
過度繪制有時候是因為你的UI布局存在大量重疊的部分,還有的時候是因為非必須的重疊背景。例如某個Activity有一個背景,然后里面的Layout又有自己的背景,同時子View又分別有自己的背景。僅僅是通過移除非必須的背景圖片,這就能夠減少大量的紅色Overdraw區域,增加藍色區域的占比。這一措施能夠顯著提升程序性能。
###三、VSYNC(垂直同步)
要理解App如何渲染的,就必須了解手機硬件是如何工作的,那么就必須理解什么是VSYNC。在講解VSYNC之前,我們需要了解兩個相關的概念:Refresh Rate:代表了屏幕在一秒內刷新屏幕的次數,這取決于硬件的固定參數,例如60Hz。Frame Rate:代表了GPU在一秒內繪制操作的幀數,例如30fps,60fps。GPU會獲取圖形數據進行渲染,然后硬件負責把渲染后的內容呈現到屏幕上,他們兩者不停的進行協作。刷新頻率和幀率并不是總能夠保持相同的節奏。
**幀率比刷新率快的情況**
如果發生幀率比刷新頻率快的情況,就會容易出現Tearing的現象(畫面上下兩部分顯示內容發生斷裂,來自不同的兩幀數據發生重疊)。出現撕裂現象的原因是因為,當你的顯卡正在使用,一個內存區正在寫入幀數據(用來顯示一幀的一個Buffer),從頂部開始,新的一幀覆蓋前一幀,并立刻輸出一行內容。現在,當屏幕開始刷新時,實際上并不知道緩沖區是什么狀態(即不知道緩沖區中的一幀是否繪制完畢,即存在只繪制了一半的情況,另一半還是之前的那幀),因此它從GPU中抓住的幀肯可能并不是完全完整的。
**屏幕刷新率比幀速率快**
如果屏幕刷新率比幀速率快,屏幕會在兩幀中顯示同一個畫面,當這種斷斷續續的情況發生時,你就遇到麻煩了。比如你的幀速率比屏幕刷新率高的時候,用戶看到的是非常流暢的畫面,但是幀速率降下來的時候(GPU繪制太多東西的時候),用戶將會很明顯地察覺到動畫卡住了或者掉幀,然后又恢復了流暢。這通常會被描述為閃屏,跳幀,延遲。
###四、GPU渲染分析工具
使用Peofile GPU Rendering tool,你可以在手機上就可以看到究竟是什么導致你的應用程序出現卡頓,變慢的情況。這個工具在設置-開發者選項-Profile GPU rendering選項,打開后選擇on screen as bars然后手機屏幕上就會出現三個顏色組成的小柱狀圖,以及一條綠線這個工具會在屏幕上顯示經過分析后的圖形數據,最底部的圖顯示的是Navigation的相關信息。最上面顯示的是Notification的相關信息。中間的圖顯示的是當前應用程序的圖。當你的應用程序在運行時,你會看到一排柱狀圖在屏幕上,從左到右動態地顯示,每一個垂直的柱狀圖代表一幀的渲染,越長的垂直柱狀圖表示這一幀需要渲染的時間越長。隨著需要渲染的幀數越來越多,他們會堆積在一起,這樣你就可以觀察到這段時間幀率的變化。
**綠線**
下圖中的綠線代表16ms,要確保一秒內打到60fps,你需要確保這些幀的每一條線都在綠色的16ms標記線之下。任何時候你看到一個豎線超過了綠色的標記現,你就會看到你的動畫有卡頓現象產生。
**柱狀圖**
每一條柱狀圖都由三種顏色組成: 藍-紅-黃。這些線直接和Androi的渲染流水線和他實際運行幀數的時間關聯。
**藍色**
藍色代表測量繪制的時間,或者說它代表需要多長時間去創建和更新你的DisplayList。在Android中,一個視圖在可以實際的進行渲染之前,它必須被轉換成GPU所熟悉的格式,簡單來說就是幾條繪圖命令,復雜點的可能是你的自定義的View嵌入了自定義的Path。一旦完成,結果會作為一個DisplayList對象被系統送入緩存,藍色就是記錄了需要花費多長時間在屏幕上更新視圖(說白了就是執行每一個View的onDraw方法,創建或者更新每一個View的Display List對象)。
當看到藍色的線很高的時候,有可能是因為一堆視圖突然變得無效了(即需要重新繪制),或者你的幾個自定義視圖的onDraw函數過于復雜。
**紅色**
紅色代表執行的時間,這部分是Android進行2D渲染 Display List的時間,為了繪制到屏幕上,Android需要使用OpenGl ES的API接口來繪制Display List。這些API有效地將數據發送到GPU,最總在屏幕上顯示出來。 當你看到紅色的線非常高的時候,這些復雜的自定義View就是罪魁禍首。 上面圖中紅色線較高的一種可能性是因為重新提交了視圖而導致的。這些視圖并不是失效的視圖,但是有些時候發生了某些事,例如視圖旋轉,我們需要重新清理這個區域的視圖,這樣可能會影響這個視圖下面的視圖,因為這些視圖都需要進行重新的繪制操作。
**橙色**
橙色部分表示的是處理時間,或者說是CPU告訴GPU渲染一幀的地方,這是一個阻塞調用,因為CPU會一直等待GPU發出接到命令的回復,如果柱狀圖很高,那就意味著你給GPU太多的工作,太多的負責視圖需要OpenGL命令去繪制和處理。
##引起卡頓的因素
###一、布局層次太深
解決方案: 布局優化的思想就是盡量減少布局文件的層級,布局中層級少了,Android的繪制工作量就少了,程序的性能就會提高。
首先要刪除無用的控件和層級,其次選擇使用性能較低的ViewGroup,例如RelativeLayout。如果使用LinearLayout實現布局需要大量嵌套的時候就需要改成RelativeLayout,因為布局的嵌套就是增加了布局的層級,同樣會降低性能。
另外一種就是需要采用<include>標簽、<merge>標簽和ViewStub。<include>標簽主要用于布局重用,<merge>標簽一般和<include>配合使用,它可以降低減少布局的層級,而ViewStub則提供了按需加載的功能,只有需要的時候采用將ViewStub布局加載到內存中,這樣也就提高了程序的初始化效率。
**<include>**
<include>布局可以將指定的布局加載到當前的布局文件中,一般用在titlebar,如果每個布局都需要這樣的titlebar,則可以將其獨立出來,然后通過<include>標簽將titlebar添加到需要的布局中,<include>起到復用布局的特性。
**<merge>**
<merge>標簽一般和<include>標簽一起使用從而減少布局的層級。如果當前的布局是一個豎直方向的LinearLayout,而其包含的布局也是豎直方向的LinearLayout,則被包含的布局中的LinearLayout是多余的,通過<merger>標簽就可以去掉多余的那一層LinearLayout。
**ViewStub**
ViewStub繼承了View,它非常輕量級且寬/高都是0,因此它本身不參與任何的布局和繪制過程。ViewStub的意義在于按需加載所需要的布局文件,在實際開發中,有很多布局文件在正常情況下不會顯示,不如網絡異常時的界面,這個時候就沒有必要再整個界面初始化的時候將其加載進來,通過ViewStub就可以做到在使用的時候再加載,提高了程序初始化時的性能。
當ViewStub通過setVisibility或者inflate方法加載后,ViewStub就會被它內部的布局替換掉,這個時候ViewStub就不再是整個布局結構中的一部分了。另外,目前ViewStub還不支持<merge>標簽。
**ViewStub特點:**
* 1、ViewStub只能Inflate一次,之后ViewStub對象會被置為空。按句話說,某個被ViewStub指定的布局被Inflate后,就不能夠再通過ViewStub來控制它了。所以它不適用 于需要按需顯示隱藏的情況。
* 2、ViewStub只能用來Inflate一個布局文件,而不是某個具體的View,當然也可以把View寫在某個布局文件中。如果想操作一個具體的view,還是使用visibility屬性吧。
* 3、VIewStub中不能嵌套merge標簽。
###二、繪制優化繪制優化
解決方案:在onDraw()方法中避免執行大量的操作,主要體現在兩個方面
* 1、onDraw()中不要創建新的局部對象,因為onDraw方法可能會被頻繁調用,這樣就會在一瞬間產生大量的臨時文件,這不僅占用了過多的內存而且還會導致系統更加頻繁的gc,降低了程序的執行效率。
* 2、onDraw方法中不要做耗時的操作,也不能執行過多的循環,大量的循環會搶占CPU的資源這樣就會造成View的繪制卡頓。按照Google官方給出的性能優化典范中的標準,View的繪制幀率保持在60fps是最佳的,這就要求每幀的繪制時間不超過16ms,雖然程序很難保證16ms這個時間,但是盡量降低onDraw方法的復雜度方向是對的。