相信很多人都有这种经历,在使用app的过程中,突然间发现程序虽然在运行,但是这里停顿一下,那里停顿一下的卡顿现象,就像看上网看视频一样,缓冲不过来,视频很卡,不能连续的看下去。造成这样原因有很多,其中一种就是UI被过度绘制了。
UI过度绘制简单的来说是指在一个界面中有很多元素,但是我们只需要更新某一小块的元素,app却把所有的元素都刷新一遍,这就造成过度绘制。
过度绘制造成UI卡顿的原因是因为它浪费大量的CPU以及GPU资源。手机原本为了保持视觉的流畅度,其屏幕刷新频率是60hz,即在1000/60=16.67ms内更新一帧。如果没有完成任务,就会发生掉帧的现象,也就是我们所说的卡顿。
这其中的原理比较复杂,大家可以看看大神是怎么说的,这里给个胡凯大神的文章地址:
Android性能优化之渲染篇
http://hukai.me/android-performance-render/
debug GPU overdraw
有问题就必然有解决办法,在Android系统内部也有一个神器可以查看app的UI的过度绘制情况,在开发者选项中有个debug GPU overdraw(调试GPU过度绘制),打开之后有off(关闭),show overdraw areas(显示过度绘制区域),show areas for Deuteranomaly(为红绿症患者显示过度绘制区域)
我们选择show overdraw areas,发现整个手机界面的颜色变了,在打开过度绘制选项后,其中的蓝色,淡绿,淡红,深红代表了4种不同程度的Overdraw情况,我们的目标就是尽量减少红色Overdraw,看到更多的蓝色区域。
Profile GPU rendering
其次android系统还内置了Profile GPU rendering工具,这个工具也是在开发者选项中打开,它能够以柱状图的方式显示当前界面的渲染时间
- 蓝色代表测量绘制的时间,或者说它代表需要多长时间去创建和更新你的DisplayList.在Android中,一个视图在可以实际的进行渲染之前,它必须被转换成GPU所熟悉的格式,简单来说就是几条绘图命令,复杂点的可能是你的自定义的View嵌入了自定义的Path. 一旦完成,结果会作为一个DisplayList对象被系统送入缓存,蓝色就是记录了需要花费多长时间在屏幕上更新视图(说白了就是执行每一个View的onDraw方法,创建或者更新每一个View的Display List对象).
- 橙色部分表示的是处理时间,或者说是CPU告诉GPU渲染一帧的地方,这是一个阻塞调用,因为CPU会一直等待GPU发出接到命令的回复,如果柱状图很高,那就意味着你给GPU太多的工作,太多的负责视图需要OpenGL命令去绘制和处理.
- 红色代表执行的时间,这部分是Android进行2D渲染 Display List的时间,为了绘制到屏幕上,Android需要使用OpenGl ES的API接口来绘制Display List.这些API有效地将数据发送到GPU,最总在屏幕上显示出来.
在这里也放一个大神关于Profile GPU rendering的介绍
http://androidperformance.com/2015/04/19/Android-Performance-Patterns-4.html
下面我们开始对UI多度绘制开始实战吧!
实战项目地址
初始界面的问题
刚打开这个项目,我们就发现了在第一个有过度绘制问题,效果如下
存在问题
- 在按钮overdraw上面就有个红色的过度绘制区域
- 在文本框This is test的布局中也是红色过度绘制区域
解决方法
-
要解决这个问题,我们首先需要分析这是怎么引起的。分析到activity_main.xml的布局文件时,发现这里使用了多个嵌套的LinearLayout布局,而且每个LinearLayout都会使用一次android:background设置一次自己的背景颜色,他们造成了过度绘制。
仔细分析在其中一个嵌套ImageView的LinearLayout布局背景颜色与最外层的背景颜色是一样的,属于不需要的背景色,因此将这个LinearLayout中的android:background属性删除,这时发现文本框布局已经不再是红色了
- 咋看之下一切都很完美,但其实整个ui其实还含有一个隐含的绘制效果,那边是在activity中,使用setContentView(R.layout.activity_main)设置布局的时候,android会自动填充一个默认的背景,而在这个UI中,我们使用了填充整个app的背景,因此不需要默认背景,取消也很简单,只需要在activity中的onCreate方法中添加这么一句就行了
getWindow().setBackgroundDrawable(null);
现在看最终优化效果
OVERDRAWVIEW页面的问题
在overdrawviewactivity中只有一个自定义的图案,而这个自定义的图案引起了过度绘制的问题
解决方法
- 首先这个也是填充了整个ui界面的绘制图片,因此我们也在activity中的onCreate方法中添加getWindow().setBackgroundDrawable(null);取消默认绘制。
- 继续研究,发现过度绘制问题是由于OverDrawView类中的ondraw方法中多次绘制了矩形导致的,代码如下:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
mPaint.setColor(Color.GRAY);
canvas.drawRect(0, 0, width, height, mPaint);
mPaint.setColor(Color.CYAN);
canvas.drawRect(0, height/4, width, height, mPaint);
mPaint.setColor(Color.DKGRAY);
canvas.drawRect(0, height/3, width, height, mPaint);
mPaint.setColor(Color.LTGRAY);
canvas.drawRect(0, height/2, width, height, mPaint);
}
通过分析得知,颜色为GRAY的矩形的高度其实不需要设置为整个屏幕的高度,它的高度只需要设置为它所显示范围的高度就可以了,因此可以设为height/4。
其他的矩形也是同样的道理,因此更改这里的代码为:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
mPaint.setColor(Color.GRAY);
canvas.drawRect(0, 0, width, height/4, mPaint);
mPaint.setColor(Color.CYAN);
canvas.drawRect(0, height/4, width, height/3, mPaint);
mPaint.setColor(Color.DKGRAY);
canvas.drawRect(0, height/3, width, height/2, mPaint);
mPaint.setColor(Color.LTGRAY);
canvas.drawRect(0, height/2, width, height, mPaint);
}
优化的界面
BUSYONDRAW频繁绘制
当我们点击BUSYONDRAW按钮的时候,我们发现明显的卡顿现象。在开发者选项中打开Profile GPU rendering选项,然后在次点击BUSYONDRAW按钮,发现这个页面绘制渲染时间已经突破天际了!
在初始的时候,蓝色绘制时间占满整个屏幕高度,这是造成卡顿的重要原因。
解决方法
- 首先卡顿现象是由ondraw方法中的for循环中的打印字符串引起的
for (int i = 0; i < 1000; i++) {
System.out.println("canvas = [" + canvas + "]" + i);
}
我们将其提取出来放在另一个线程中运行。
先是创建一个线程池:
private ExecutorService pool = Executors.newCachedThreadPool();
将要运行的耗时操作封装成Runable对象并通过一个方法获取:
@NonNull
private Runnable getCommand(final Canvas canvas) {
return new Runnable() {
@Override
public void run() {
for (int i = 0; i &lt; 1000; i++) {
System.out.println("canvas = [" + canvas + "]" + i);
}
}
};
}
最后在onDraw里运行放在子线程里运行:
pool.execute(getCommand(canvas));
现在进入也不会卡顿了。
其次在ondraw中也不宜创建Paint()对象,因为app会频繁调用ondraw对象,会造成内存泄漏,因此需要将其提取为全局变量。
最后绘制了多个圆形图案也造成了一定程度的卡顿,但由于功力不够,暂时无法优化。