版權聲明:本文出自汪磊的博客,未經作者允許禁止轉載。
本篇博客主要記錄一些工作中常用的UI渲染性能優化及調試方法,理解這些方法對於我們編寫高質量代碼也是有一些幫助的,主要內容包括介紹CPU,GPU的職責,UI的overdraw,Hierarchy View工具的使用以及canvas.clipRect()方法防止View的重疊繪制,都是一些老生常談的玩意,只是為了自己記錄一下才寫出來,如果您已經掌握,直接跳過就可以了。
一、CPU,GPU的職責介紹
對於大多數手機的屏幕刷新頻率是60hz,也就是如果在1000/60=16.67ms內沒有把這一幀的任務執行完畢,就會發生丟幀的現象,丟幀是造成界面卡頓的直接原因,渲染操作通常依賴於兩個核心組件:CPU與GPU。CPU負責包括Measure,Layout等計算操作,GPU負責Rasterization(柵格化)操作(所謂柵格化就是將矢量圖形轉換為位圖的過程,手機上顯示是按照一個個像素來顯示的,柵格化再普通一些的說法就是將一個Button,TextView等組件拆分到一個個像素上去顯示)。
UI渲染優化的目的就是減輕CPU,GPU的壓力,除去不必要的操作,保證每幀16ms以內處理完所有的CPU與GPU的計算,繪制,渲染等等操作,使UI順滑,流暢的展示出來。
二、查找Overdraw
Overdraw(過度繪制)描述的是屏幕上的某個像素在同一幀的時間內被繪制了多次。在重疊的UI布局中,如果不可見的UI也在做繪制的操作或者后一個控件將前一個控件遮擋,會導致某些像素區域被繪制了多次,從而增加了CPU,GPU的壓力。
那么我們找出布局中Overdraw的地方呢?很簡單,一般手機里面開發者選項都有調試GPU過度繪制的開關,打開即可。
以小米4手機為例,依次找到設置->更多設置->開發者選項->調試GPU過度繪制開關,打開就可以了。
打開調試GPU過度繪制開關之后,再次回到自己開發的應用發現界面怎么多了一些花花綠綠的玩意,沒錯,不同的顏色代表過度繪制的程度,具體如下表:
藍色,淡綠,淡紅,深紅代表了4種不同程度的Overdraw情況,1x,2x,3x,4x分別表示同一像素上同一幀的時間內被繪制了多次,1x就表示一次最理想情況,4x表示4次最差的情況,我們要做的就是盡量減少3x,4x的情況出現。
下面我們以一個簡單demo來進一步說明一下,比如我們開發好一個界面,如下:
很簡單的功能,功能做完了,我們看看能不能做下優化呢?打開OverDraw功能,再次查看界面,如下;
咦?怎么大部分都是淺綠色呢?也就是說同一像素上同一幀的時間內被繪制了2次,這是怎么回事?這時我們需要看下UI布局了,看哪些地方可以優化一下。
主界面布局如下:
1 <?xml version="1.0" encoding="utf-8"?> 2 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 xmlns:tools="http://schemas.android.com/tools" 4 android:layout_width="match_parent" 5 android:layout_height="match_parent"> 6 7 <ListView 8 android:id="@+id/list_view" 9 android:layout_width="match_parent" 10 android:layout_height="match_parent" 11 android:divider="#F1F1F1" 12 android:dividerHeight="1dp" 13 android:background="@android:color/white" 14 android:scrollbars="vertical"> 15 </ListView> 16 17 </RelativeLayout>
ListView每個條目布局如下:
1 <?xml version="1.0" encoding="utf-8"?> 2 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 android:layout_width="match_parent" 4 android:layout_height="52dp" 5 android:background="@drawable/ts_account_list_selector"> 6 7 <TextView 8 android:id="@+id/ts_item_has_login_account" 9 android:layout_width="wrap_content" 10 android:layout_height="wrap_content" 11 android:layout_marginLeft="10dp" 12 android:layout_marginTop="4dp" 13 android:gravity="center" 14 android:text="12345678999" 15 android:textColor="@android:color/black" 16 android:textSize="16sp" /> 17 18 <LinearLayout 19 android:layout_width="wrap_content" 20 android:layout_height="20dp" 21 android:layout_alignParentBottom="true" 22 android:layout_marginBottom="3dp" 23 android:layout_marginLeft="10dp" 24 android:gravity="center_vertical" > 25 26 <ImageView 27 android:id="@+id/ts_item_time_clock_image" 28 android:layout_width="12dp" 29 android:layout_height="12dp" 30 android:src="@mipmap/ts_login_clock" /> 31 32 <TextView 33 android:id="@+id/ts_item_last_login_time" 34 android:layout_width="wrap_content" 35 android:layout_height="wrap_content" 36 android:layout_marginLeft="5dp" 37 android:layout_toRightOf="@id/ts_item_time_clock_image" 38 android:text="上次登錄" 39 android:textColor="@android:color/darker_gray" 40 android:textSize="11sp" /> 41 42 <TextView 43 android:id="@+id/ts_item_login_time" 44 android:layout_width="wrap_content" 45 android:layout_height="wrap_content" 46 android:layout_marginLeft="5dp" 47 android:layout_toRightOf="@id/ts_item_last_login_time" 48 android:text="59分鍾前" 49 android:textColor="@android:color/darker_gray" 50 android:textSize="11sp" /> 51 </LinearLayout> 52 53 <TextView 54 android:id="@+id/ts_item_always_account_image_tips" 55 android:layout_width="wrap_content" 56 android:layout_height="13dp" 57 android:layout_alignParentRight="true" 58 android:layout_marginTop="2dp" 59 android:background="@mipmap/ts_always_account_bg" 60 android:gravity="center" 61 android:text="常用" 62 android:textColor="@android:color/white" 63 android:textSize="9sp" /> 64 65 <ImageView 66 android:id="@+id/ts_item_delete_account_image" 67 android:layout_width="12dp" 68 android:layout_height="12dp" 69 android:layout_alignParentRight="true" 70 android:layout_marginTop="2dp" 71 android:layout_marginRight="13dp" 72 android:layout_centerVertical="true" 73 android:src="@mipmap/ts_close" /> 74 75 </RelativeLayout>
發現哪里的問題了嗎?這里我就直接說了,問題在於ListView多余設置了背景:android:background="@android:color/white",設置此背景對於我們這個需求根本就沒有用,顯示不出來並且增加GPU額外壓力,去掉ListView背景之后再次觀察如下:
渲染性能提升了一個檔次,在實際工作中情況會復雜很多,為了實現一個效果會不得不犧牲性能,這就需要自己團隊權衡了,好了OverDraw到此為止。
三、clipRect來解決自定義View的OverDraw
我們平時寫自定義View的時候有時會重寫onDraw方法,但是Android系統是無法檢測onDraw里面具體會執行什么操作,從而系統無法為我們做一些優化。這樣對編程人員要求就高了,如果我們自己寫的View有大量重疊的地方就造成了CPU,GPU資源的浪費,但是我們可以通過canvas.clipRect()來幫助系統識別那些可見的區域。這個方法可以指定一塊矩形區域,只有在這個區域內才會被繪制,其他的區域會被忽視,下面我們通過谷歌提供的一個小demo進一步說明。實現效果如下:
主要就是卡片重疊效果,優化前代碼實現如下:
DroidCard類封裝要繪制的一個個卡片的信息:
1 public class DroidCard { 2 3 public int x;//左側繪制起點 4 public int width; 5 public int height; 6 public Bitmap bitmap; 7 8 public DroidCard(Resources res,int resId,int x){ 9 this.bitmap = BitmapFactory.decodeResource(res,resId); 10 this.x = x; 11 this.width = this.bitmap.getWidth(); 12 this.height = this.bitmap.getHeight(); 13 } 14 }
DroidCardsView為真正的自定義View:
1 public class DroidCardsView extends View { 2 //圖片與圖片之間的間距 3 private int mCardSpacing = 150; 4 //圖片與左側距離的記錄 5 private int mCardLeft = 10; 6 7 private List<DroidCard> mDroidCards = new ArrayList<DroidCard>(); 8 9 private Paint paint = new Paint(); 10 11 public DroidCardsView(Context context) { 12 super(context); 13 initCards(); 14 } 15 16 public DroidCardsView(Context context, AttributeSet attrs) { 17 super(context, attrs); 18 initCards(); 19 } 20 /** 21 * 初始化卡片集合 22 */ 23 protected void initCards(){ 24 Resources res = getResources(); 25 mDroidCards.add(new DroidCard(res,R.drawable.alex,mCardLeft)); 26 27 mCardLeft+=mCardSpacing; 28 mDroidCards.add(new DroidCard(res,R.drawable.claire,mCardLeft)); 29 30 mCardLeft+=mCardSpacing; 31 mDroidCards.add(new DroidCard(res,R.drawable.kathryn,mCardLeft)); 32 } 33 34 @Override 35 protected void onDraw(Canvas canvas) { 36 super.onDraw(canvas); 37 for (DroidCard c : mDroidCards){ 38 drawDroidCard(canvas, c); 39 } 40 invalidate(); 41 } 42 43 /** 44 * 繪制DroidCard 45 */ 46 private void drawDroidCard(Canvas canvas, DroidCard c) { 47 canvas.drawBitmap(c.bitmap,c.x,0f,paint); 48 } 49 }
代碼不是本篇重點,不過也不難,自行查看就可以了。我們打開overdraw開關,效果如下:
淡紅色區域明顯被繪制了三次(三張圖片重合的地方),其實下面的圖片完全沒必要完全繪制,只需要繪制三分之一即可,接下來我們就需要對其優化,保證最下面兩張圖片只需要回執其三分之一最上面圖片完全繪制出來就可。
DroidCardsView代碼優化為:
1 public class DroidCardsView extends View { 2 3 //圖片與圖片之間的間距 4 private int mCardSpacing = 150; 5 //圖片與左側距離的記錄 6 private int mCardLeft = 10; 7 8 private List<DroidCard> mDroidCards = new ArrayList<DroidCard>(); 9 10 private Paint paint = new Paint(); 11 12 public DroidCardsView(Context context) { 13 super(context); 14 initCards(); 15 } 16 17 public DroidCardsView(Context context, AttributeSet attrs) { 18 super(context, attrs); 19 initCards(); 20 } 21 /** 22 * 初始化卡片集合 23 */ 24 protected void initCards(){ 25 Resources res = getResources(); 26 mDroidCards.add(new DroidCard(res, R.drawable.alex,mCardLeft)); 27 28 mCardLeft+=mCardSpacing; 29 mDroidCards.add(new DroidCard(res, R.drawable.claire,mCardLeft)); 30 31 mCardLeft+=mCardSpacing; 32 mDroidCards.add(new DroidCard(res, R.drawable.kathryn,mCardLeft)); 33 } 34 35 @Override 36 protected void onDraw(Canvas canvas) { 37 super.onDraw(canvas); 38 for (int i = 0; i < mDroidCards.size() - 1; i++){ 39 drawDroidCard(canvas, mDroidCards,i); 40 } 41 drawLastDroidCard(canvas,mDroidCards.get(mDroidCards.size()-1)); 42 invalidate(); 43 } 44 45 /** 46 * 繪制最后一個DroidCard 47 * @param canvas 48 * @param c 49 */ 50 private void drawLastDroidCard(Canvas canvas,DroidCard c) { 51 canvas.drawBitmap(c.bitmap,c.x,0f,paint); 52 } 53 54 /** 55 * 繪制DroidCard 56 * @param canvas 57 * @param mDroidCards 58 * @param i 59 */ 60 private void drawDroidCard(Canvas canvas,List<DroidCard> mDroidCards,int i) { 61 DroidCard c = mDroidCards.get(i); 62 canvas.save(); 63 canvas.clipRect((float)c.x,0f,(float)(mDroidCards.get(i+1).x),(float)c.height); 64 canvas.drawBitmap(c.bitmap,c.x,0f,paint); 65 canvas.restore(); 66 } 67 }
主要就是使用Canvas的clipRect方法,繪制之前裁剪出一個區域,這樣繪制的時候只在這區域內繪制,超出部分不會繪制出來。
重新執行程序,效果如下:
處理后性能就提升了一絲絲,此外我們還可以使用canvas.quickReject方法來判斷是否沒和某個矩形相交,從而跳過那些非矩形區域內的繪制操作。
四、Hierarchy Viewer的使用
Hierarchy Viewer可以很直觀的呈現布局的層次關系。我們可以通過紅,黃,綠三種不同的顏色來區分布局的Measure,Layout,Executive的相對性能表現如何,
提升布局性能的關鍵點是盡量保持布局層級的扁平化,避免出現重復的嵌套布局。如果我們寫的布局層級比較深會嚴重增加CPU的負擔,造成性能的嚴重卡頓,關於Hierarchy Viewer的使用舉例這里就不列舉了,我覺得大部分安卓開發人員都會使用了,不知道為什么我這電腦Hierarchy Viewer工具突然出問題了,緊急搶救中。。。
五、內存抖動現象
在我們優化過view的樹形結構和overdraw之后,可能還是感覺自己的app有卡頓和丟幀,或者滑動慢:卡頓還是存在。這時我們就要查看一下是否存在內存抖動情況了
Android有自動管理內存的機制,但是對內存的不恰當使用仍然容易引起嚴重的性能問題。在同一幀里面創建過多的對象是件需要特別引起注意的事情,在同一幀
里創建大量對象可能引起GC的不停操作,執行GC操作的時候,所有線程的任何操作都會需要暫停,直到GC操作完成。大量不停的GC操作則會顯著占用幀間隔時間。
如果在幀間隔時間里面做了過多的GC操作,那么自然其他類似計算,渲染等操作的可用時間就變得少了,嚴重時可能引起卡頓:
導致GC頻繁操作有兩個主要原因:
一是內存抖動,所謂內存抖動就是短時間產生大量對象又在短時間內馬上釋放。
二是短時間產生大量對象超出閾值,內存不夠,同樣會觸發GC操作。
觀察內存抖動我們可以借助android studio中的工具,3.0以前可以使用android monitor,3.0以后被替換為android Profiler。
如果工具里面查看到短時間發生了多次內存的漲跌,這意味着很有可能發生了內存抖動,如圖:
為了避免發生內存抖動,我們需要避免在for循環里面分配對象占用內存,需要嘗試把對象的創建移到循環體之外,自定義View中的onDraw方法也需要引起注意,每次屏幕發生繪制以及動畫執行過程中,onDraw方法都會被調用到,避免在onDraw方法里面執行復雜的操作,避免創建對象。對於那些無法避免需要創建對象的情況,我們可以考慮對象池模型,通過對象池來解決頻繁創建與銷毀的問題,但是這里需要注意結束使用之后,需要手動釋放對象池中的對象。
好了,關於UI渲染性能優化介紹到此為止,本篇大量參考谷歌官方發布的性能優化資料,都是一些老玩意,主要用於個人記錄。