转自:http://www.jianshu.com/p/a769a6028e51

前言

Android 应用有一个明显的趋势—越来越多地使用动画效果来提升用户体验。但任何事情都是有代价的,丰富复杂的动画提升用户体验的同时,性能问题像隐形的恶魔一样,逐渐地侵蚀着你的应用。动画不流畅、界面卡顿开始困扰着你,逼着你进行性能优化。在这个优化过程中,最理想的标准就是绘制一帧的时间不要超过 16ms。这是什么意思?让我们一探究竟。

屏幕刷新频率

我们知道,手机屏幕是由许多的像素点组成的,如下图所示:

image

通过让每一个像素点显示不同的颜色,可以组合成各种各样的图像。这些像素点的颜色数据从哪里来?

答案是:在 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 次/秒,这就与屏幕硬件电路的刷新频率保持了同步。如下图所示:

image

丢帧是怎么发生的

上面说 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 的界面。我们假设更新前后的界面是这样的:

image

很简单,也就是让红色的小球向上移动了一段距离。但由于你的应用没能在 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。