美文网首页Android Android进阶伪程序员
破译Android性能优化中的16ms问题

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

作者: 工程师milter | 来源:发表于2016-08-23 18:18 被阅读6721次

    声明:本篇文章已授权微信公众号 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。如果你觉得这篇文章对你有帮助,就点个赞鼓励下我吧!

    相关文章

      网友评论

      • 请叫我大苏:但老师,我有个疑问:
        即使我们布局优化得很彻底,已经保证了布局的绘制控制在16ms内。
        但如果主线程此时有个耗时的操作的话,即使这个操作跟ui没关,那么也会造成卡顿的吧
        张旭童:是的呀,木桶效应。要想达到60fps,就需要方方面面都做好。避免在主线程执行耗时操作是一方面,优化布局是另一方面,只有所有方面都做到最好,才能在宝贵都16ms内完成新一帧的渲染。
      • 请叫我大苏:非常棒,刚好解答了我的疑问
      • KennethYo:16ms这里讲的不错
      • 黄光华:今天重温文章,刚好看到今天有更正 :smile:
        工程师milter: @黄光华 看来重温是必要的
      • Bxtyfuffff:super.onDraw方法,而在这个方法中会执行绘制background的操作???
        super.onDraw默认为空实现,背景是在draw方法里的。
        工程师milter:@Bxtyfuffff 感谢指正!已经在原文中修正!
      • hackware:亲,onDraw默认是不干事的,背景是在draw里面画的,准确的说是在drawBackground里
        工程师milter:@hackware 感谢指正!已经在原文中修正!
      • minjie0128:有个问题没搞明白,这个16ms里到底做了什么,难道每个16ms内都会执行 onMeasure,onLayout ,onDraw这几个方法吗?
        工程师milter: @minjie0128 只有应用的界面在执行一个动画的时候,才会有16ms性能问题,因为需要保证动画视觉上的连贯顺畅。有的动画需要重新layout和measure,有的不需要,要根据情况具体分析。
      • Jenson_:老师有时间讲解一下线程同步和锁方面吧。感谢
        工程师milter: @Jenson_ 好的,回头写一篇
      • 成长的亚当:特别支持这位兄弟!老师,和榜样。 我也是一位大龄转行的android开发者。 但水平比你差多了。希望能通过读你(已经其他分享者们)的文章你更快速的提升自己的技术水平! :smile:
        工程师milter: @成长的亚当 谢谢!知音啊!
      • breakingbad:支持大叔😁
        工程师milter: @breakingbad 感谢!
      • KingJA:6岁孩子标识看不懂
        工程师milter:@KingJA 那就等7岁,哈哈
      • 矮油哥哥:赞你,希望看到更好的文章
        工程师milter:@矮油哥哥 我会加油的
      • moxun:简单易懂,好文
        工程师milter:@moxun 好文就要多多分享给其他人哈! :smile:
      • chocolatezhu:开个异步线程算不算解决16ms卡顿的一个方法啊,哈哈
        工程师milter:@ppzhu UI线程才能更新界面的
      • Ojie:好!!
        工程师milter:@jieVp 谢谢!
      • f464bc828e7d:好文
        工程师milter: @李长城 谢谢!
      • jkyeo:很赞,讲的很清楚。作者和 GPU,你们辛苦了。
        工程师milter: @jkyeo 与大家分享,其乐无穷
      • 自己找知己:从源码的角度解析一下...这样虽易懂但总觉得单薄
        工程师milter: @喜欢而非坚持 源码角度解析帮助不大的,要讲透绕不开opengl,GPU栅格化原理,我觉得这些不适合大部分开发者,就没讲,是一种折中吧
      • stareme:太笼统
      • dfe147f4a102:先mark明天看
      • b729533a3e16:我听懂了。。
        工程师milter: @李小Bo 谢谢鼓励!觉得好请多多分享

      本文标题:破译Android性能优化中的16ms问题

      本文链接:https://www.haomeiwen.com/subject/hyhnsttx.html