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

xiaoxiao2021-02-28  12

今日科技快讯

近两年,科技公司冠名职业体育运动成为一时风潮,比较知名的有广州恒大淘宝队、北京国安乐视队两支,分别被淘宝和乐视冠名。而就在最近,上海男篮宣布,哔哩哔哩(bilibili)将冠名上海男篮,球队正式更名为“上海哔哩哔哩篮球队”,在2016-2017赛季CBA联赛当中将使用新冠名队服。

对于此次冠名,我们来看看网友是怎么评论的:“CBA冲破次元壁的历史性时刻!” “央视转播的时候念球队名字会不会笑场” “这个球进的23333!” “不知道姚明是怎么想的。。。”

作者简介

本篇是 milter 的第二篇投稿,给大家分析了丢帧卡屏的原因,并给出了一些预防性的措施。祝大家在7天班之后,过一个顺畅的周末!

milter 的博客地址:

http://www.jianshu.com/users/511ba5d71aef

前言

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。

更多

如果你有好的技术文章想和大家分享,欢迎向我的公众号投稿,投稿具体细节请在公众号主页点击“投稿”菜单查看。

欢迎长按下图 -> 识别图中二维码或者扫一扫关注我的公众号:

文章精彩不?

赞赏

人赞赏

转载请注明原文地址: https://www.6miu.com/read-1250339.html

最新回复(0)