破译Android性能优化中的16ms问题
前言
Android应用有一个明显的趋势—越来越多地使用动画效果来提升用户体验。但任何事情都是有代价的,丰富复杂的动画提升用户体验的同时,性能问题像隐形的恶魔一样,逐渐地侵蚀着你的应用。动画不流畅、界面卡顿开始困扰着你,逼着你进行性能优化。在这个优化过程中,最理想的标准就是绘制一帧的时间不要超过16ms。这是什么意思?让我们一探究竟。
屏幕刷新频率
我们知道,手机屏幕是由许多的像素点组成的,如下图所示:
通过让每一个像素点显示不同的颜色,可以组合成各种各样的图像。这些像素点的颜色数据从哪里来?
答案是:
在GPU控制的一块缓冲区中,这块缓冲区叫做 Frame Buffer(也就是帧缓冲区)。你可以把它简单理解成一个二维数组,数组中的每一个元素对应着手机屏幕上的一个像素点,元素的值代表着屏幕上对应的像素点要显示的颜色。
Frame Buffer 中的数据是不断变化的,为了应对这种变化,手机屏幕的逻辑电路会定期用Frame Buffer 中的数据刷新屏幕上的像素点。目前,主流的刷新频率是60次/秒,折算出来就是16ms刷新一次。
Frame Buffer中的数据怎么来
GPU除了 Frame Buffer,用以交给手机屏幕进行绘制外,还有一个缓冲区,叫 Back Buffer,这个 Back Buffer 用以交给你的应用,让你往里面填充数据。GPU会定期交换 Back Buffer 和 Frame Buffer,也就是让 Back Buffer 变成 Frame Buffer 交给屏幕进行绘制,让原先的 Frame Buffer 变成 Back Buffer 交给你的应用进行绘制。交换的频率也是 60次/秒,这就与屏幕硬件电路的刷新频率保持了同步。如下图所示:
丢帧是怎么发生的
上面说GPU会定期交换 Back Buffer 和 Frame Buffer,但有一个例外情况,当你的应用正在往 Back Buffer 中填充数据时,系统会将 Back Buffer 锁定。如果到了GPU交换两个 Buffer 的时间点,你的应用还在往 Back Buffer 中填充数据,GPU会发现 Back Buffer 被锁定了,它会放弃这次交换,后果就是手机屏幕仍然显示原先的图像。
最不幸的情况是,GPU刚刚放弃这次交换,你的应用就完成了对Back Buffer的数据填充。可怜的你必须等待下一个16ms时间,才能看到这次数据填充的效果。
在这种情况下,从 Back Buffer 锁定开始,也就是你的应用开始往 Back Buffer 中填充数据,到填充后的数据展示到屏幕上,需要的时间是32ms。
我们知道,所谓的应用往 Back Buffer 中填充数据,其实就是更新你的应用的Activity的界面。我们假设更新前后的界面是这样的:
很简单,也就是让红色的小球向上移动了一段距离。但由于你的应用没能在16ms内完成界面更新,导致你的用户盯着第一个屏幕看了32ms,然后发现小球“跳”到了一个新的高度,而不是平滑地移动到了新的高度。
上面所说的情况称作“丢帧”。
怎样优化应用避免丢帧
作为应用开发者,为了让用户有流畅的动画体验,我们优化的目标就是不要丢帧,也就是在动画进行的过程中,我们要确保更新一帧的时间不要超过16ms。那么,怎样做才能尽可能接近这个目标呢?有如下几个tips:
-
减少视图层次,尽量使用扁平化的视图布局,如使用 RelativeLayout 代替多层嵌套的 LinearLayout。
-
减少不必要的 View 的 invalidate 调用。
-
去除 View 中不必要的 background,因为许多 background 并不会显示在最终的屏幕上。比如 ImageView, 假如它显示的图片填满了它的空间,你就没有必要给它设置一个背景色。
以上是三个操作性很强的建议。好奇的你可能会问,这样做的理由是什么?
前面说过,系统将 Back Buffer 交给你的应用填充数据,实际过程是将 Back Buffer 锁定后,将一个指向它的引用交给你的应用,这个引用就是一个 Canvas对象。你的应用获取这个 Canvas对象 后,会按照视图层次从上往下遍历传给每一个View,View 在 onDraw方法 中接收到的 canvas对象 就是它,如下:
proteced void onDraw(Canvas canvas)
View 用这个 canvas对象 完成自己的绘制。每个View都完成自己的绘制后,才算完成了一帧的绘制。
减少视图层次,可以减少传递canvas对象时间。
同时,Android提供的所有控件以及你自定义的控件,在 onDraw方法 中都会调用 super.onDraw方法,而在这个方法中会执行绘制 background 的操作,如果这个background 最终不会显示,绘制它显然是在浪费时间。
关于第二点,减少不必要的 invalidate 调用,一方面是为了减少重绘,同时,也是为了配合GPU,最大限度地利用好缓存,这里涉及到GPU的工作细节,不展开了。
明白了原理,该怎么做你心里就会有数,
比如 在 onDraw方法 中,减少创建对象,尤其是复杂的对象等,都是为了缩短绘制的时间。
最后,你还应当明白,这16ms不是全给你绘制界面的,还有 layout、measure 呢,Android的一些子系统也要占用这宝贵的16ms完成一些自己的任务,真正留给你绘制自己的界面的时间肯定是少于16ms,你能做的就是尽可能减少自己的绘制时间。
好了,这篇文章中,我没有涉及GPU工作的细节,目的是在屏蔽底层技术实现的同时让每一个层次的Android开发者都能从整体上理解把握所谓的16ms。