×

破译Android性能优化中的16ms问题

96
milter 595a1b60 08f6 4beb 998f 2bf55e230555
2016.08.23 18:18* 字数 1844

声明:本篇文章已授权微信公众号 guolin_blog(郭霖)独家发布!
当你不能向六岁的儿童讲清楚一件事的时候,说明你还没有真正理解这件事。

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

一、屏幕刷新频率

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

lcd_pixels.jpg

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

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

switch-buffer.png

三、丢帧是怎么发生的?

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

ball-bump.png

很简单,也就是让红色的小球向上移动了一段距离。但由于你的应用没能在16ms内完成界面更新,导致你的用户盯着第一个屏幕看了32ms,然后发现小球“”到了一个新的高度,而不是平滑地移动到了新的高度。

上面所说的情况称作“丢帧”。

四、作为开发者,怎样优化应用避免丢帧?

作为应用开发者,为了让用户有流畅的动画体验,我们优化的目标就是不要丢帧,也就是在动画进行的过程中,我们要确保更新一帧的时间不要超过16ms。那么,怎样做才能尽可能接近这个目标呢?有如下几个tips:

  1. 减少视图层次,尽量使用扁平化的视图布局,如使用RelativeLayout代替多层嵌套的LinearLayout。
  2. 减少不必要的View的invalidate调用。
  3. 去除View中不必要的background,因为许多background并不会显示在最终的屏幕上。比如ImageView, 假如它显示的图片填满了它的空间,你就没有必要给它设置一个背景色。

以上是三个操作性很强的建议。好奇的你可能会问,这样做的理由是什么?

前面说过,系统将Back Buffer 交给你的应用填充数据,实际过程是将Back Buffer锁定后,将一个指向它的引用交给你的应用,这个引用就是一个Canvas对象。你的应用获取这个Canvas对象后,会按照视图层次从上往下遍历传给每一个View,View在onDraw方法中接收到的canvas对象就是它,如下:

proteced void onDraw(Canvas canvas)

View用这个canvas对象完成自己的绘制。每个View都完成自己的绘制后,才算完成了一帧的绘制。

减少视图层次,可以减少传递canvas对象时间。

2016.10.14更正
感谢Bxtyfuffff
hackware
指出本文的错误之处!

在View的draw方法中,会调用View的私有方法drawBackground(Canvas canvas),这个方法中会执行绘制background的操作,如果这个background最终不会显示,绘制它显然是在浪费时间。

关于第二点,减少不必要的invalidate调用,一方面是为了减少重绘,同时,也是为了配合GPU,最大限度地利用好缓存,这里涉及到GPU的工作细节,不展开了。

明白了原理,该怎么做你心里就会有数,比如在onDraw方法中,减少创建对象,尤其是复杂的对象等,都是为了缩短绘制的时间。

最后,你还应当明白,这16ms不是全给你绘制界面的,还有layout、measure呢,Android的一些子系统也要占用这宝贵的16ms完成一些自己的任务,真正留给你绘制自己的界面的时间肯定是少于16ms,你能做的就是尽可能减少自己的绘制时间。

好了,这篇文章中,我没有涉及GPU工作的细节,目的是在屏蔽底层技术实现的同时让每一个层次的Android开发者都能从整体上理解把握所谓的16ms。如果你觉得这篇文章对你有帮助,就点个赞鼓励下我吧!

Android开发
Web note ad 1