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

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