版权声明:本文出自汪磊的博客,未经作者允许禁止转载。
本篇博客主要记录一些工作中常用的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渲染性能优化介绍到此为止,本篇大量参考谷歌官方发布的性能优化资料,都是一些老玩意,主要用于个人记录。