在 Android 开发过程中不免面临一个把应用做出来,再到把它做成牛逼的应用的过程,其中非常直观的一点就是应用 UI 的流畅度。
这里对一些性能相关的知识进行了小结~
一、UI卡顿的原因
首先,我们评价UI的时候经常会遇到这几个说法:
1.“这动画30帧都不到,卡成狗”
2.“这帧率明显都到50多了怎么还是卡卡的感觉”
3.“拖动的时候感觉在抖”
这里其实有两个问题:
A1. 平均帧率不足
A2. 平均帧率上去了,但是掉帧
帧率不足很好理解,掉帧的场景大概是这样的
每一列色块表示一帧的绘制时间,时间越久色块越宽,那么可以发现,下面一列帧率达到40的动画在中间有一段漏了一帧(可能这个时候CPU阻塞了没空画图),在后面有一段绘制特别长(可能在绘制一个很耗时的东西)。那么整体表现出来的就是下面的动画在播放过程中会明显卡两次,性能表现上还不如上面FPS30的动画。
小结:
所以当评价动画时,会发现帧率高是动画流畅的必要条件,
但还不是充分条件,
平均帧率 决定了动画体验的 上限,
但是卡顿感往往是最低帧率决定的,一个80ms的卡顿就会毁了你看似达到了60FPS的动画。
Ps. 其他卡顿原因
除了这两个最主要的,还有一些个人遇到的小的细节也会有影响:
- 动画首帧响应不及时引起的动画“不贴手”的感觉
- 使用不合理的的动画曲线/插值器
二、如何查看目前动画的表现
觉得动画比较卡这个事情实在太主观了,我们需要有一点直观的东西来支撑确认。
常见的手机屏幕的刷新率都是60Hz的,这样就决定了我们的动画的上限是60FPS,再高也意义不大了。
那么每秒绘制60帧,每帧的时间就是 1000 / 60 = 16 ms
接下来看看这个
工具1: gfxinfo
这个可以用来查看最近绘制的每帧耗时,4.1以上的机器可以直接在【开发者选项 - GPU呈现模式分析】直接查看。4.1以下的就只能通过adb 去拉数据咯。
通过这个直观的查看,我们很快可以发现一些问题:
这条绿色的横线表示16ms,柱状图高过绿线的说明这一帧绘制时间超过了标准。
柱状图分为三部分,
蓝色【Update DisplayList】
红色【Process DisplayList】
黄色【Swap Buffers】
如果有某一条或者多条柱状图超过了标准很多,那么这个动画就可能存在比较大的性能问题(图中的应用虽然有所超出,但是超出得不是很多,所以影响还不是很大)
另外一个当然就是在代码中直接打点,计算动画过程中View的onDraw的次数,再算出帧率,这个也是比较直观的衡量。
三、针对不同的卡顿实施有针对性的方案
1. 启用硬件加速
Android从3.0(API Level 11)开始,在绘制View的时候支持硬件加速,
充分利用GPU的特性,使得绘制更加平滑,但是会多消耗一些内存。
2. 降低平均单帧绘制时间,优化绘制方案
对于我们A1里面提到的平均帧率不足的情况,多数是单帧的绘制时间过长,比如UI布局太复杂,树结构太深。
这里推荐几种优化的方式:
2.1 减少视图树层级
工具2: HierarchyViewer
该工具只能在开启了ViewServer的机器上才能用,普通的商业手机会连接不上。只有像Google的官方机子,一些工程机(之前见过一台小米的工程机)能被识别。通过某些途径也可以给普通手机开启ViewServer,不过自己试了失败了。
该工具可以很直观的看到 UI 的树结构,哪个地方叠了太多无谓的层级基本上一目了然,你会发现的App总是在不经意间多嵌套了一个FrameLayout,可以使用TextView的LeftDrawable的时候想当然地用成了一个LinearLayout + ImageView + TextView 的组合。总之我们的目的就是让视图树变得扁平,没有太深的结构。
有一些文章提倡在复杂的场景下使用RelativeLayout去进行布局,个人感觉弊端是会让 UI 的代码变得较难阅读,需要不断去查看布局各个View之间的关系。所以建议只在真正必要优化的时候才进行,前期开发还是以易用为重。
2.2 减少OverDraw(重复绘制)
赶紧先拿起你的App,打开 开发者模式-调试GPU过渡绘制, 再回到你的应用,好好看看哪些地方在渲染的时候会被重复绘制多次。
一个好的App应该是不会有太多红色的区域出现的。
//TO DO : 待填坑
2.3 避免在关键路径上处理过多事情
这种事情经常出现在我们不经意写出的代码里:
@Override
protected void onDraw(Canvas canvas) {
Paint paint = new Paint();
paint.setTextSize(15);
canvas.drawText("重复创建paint对象", 0, 0, paint);
}
在这种Measure/Layout/Draw/getView等频繁调用的路径上需要尽量避免创建新的对象,进行 IO 处理等等耗时操作。频繁的创建对象除了带来频繁执行的耗时,还会产生大量的内存碎片,在某一时刻触发GC的时候又会引起不必要的麻烦。
尽管道理大家都明白,但是实际需求中总会有一些不经意的时候踩入这些坑。
e.g. 某ListView在getView的时候需要去取一些用来显示的Data,getView经过层层请求,过了Controller,最后在Model处访问了数据库,于是列表的滑动就变得很坑爹了。
解决思路1:增加一个内存cache,缓存部分数据,减少IO操作。
2.3 有时候绘制Bitmap会比绘制一堆复杂的东西来得更快。
我们知道,一个Viiew到显示需要经历Measure-Layout-Draw的过程,那么一个结构较为复杂的ViewGroup在这个过程就会消耗较长的时间,导致绘制耗时较长;此外,一个自定义的View如果在Draw的过程中绘制了许多文字(文字的绘制要比基本图形和贴图耗时得多),也会有绘制的性能问题。
这个时候,如果能拿一张bitmap直接贴上去,减少了MeasureLayout或渲染文字的时间,那将大大提高效率(详细参见后面的 预绘制)
2.4 绘制Bitmap的时候可以通过牺牲颜色来换取速度 (以及减少内存)
在创建一个Bitmap的时候,可以选择多种属性:
Bitmap bm = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
RGB_565 //表示分别用5位,6位,5位来记录红,绿,蓝的颜色值,没有透明度
//一个像素所占内存 5+6+5 = 16bit = 2Bytes
ARGB_4444 //表示各用4位来记录红,绿,蓝和透明度
ARGB_8888 //表示各用8位来记录红,绿,蓝和透明度
可见 ARGB_8888的显示效果最好,可以表现2^16种颜色,但是相应的弊端就是所占的内存会比前两者大一倍;更大的内存除了意味着内存爆表,对绘制的性能也会有很大影响。
故我们提倡,在使用大图进行动画时(截屏大小的),尽可能的在显示效果妥协的情况下,使用RGB_565(无透明)和ARGB_4444(有透明)进行图片的decode,对动画帧率会有显著的提升。
2.5 预先准备好动画需要的内容
这里分两种
1. 预先加载资源 / 数据
2. 预绘制
//TO DO : 待填坑
2.6 透明的贴图,颜色等会更加消耗绘制性能
我们经常拿到设计师妹子给过来的UI标注是这样的:
也许设计师在设计的时候确实是用纯黑再叠加一定的透明度来调整这个灰色的效果的,但是实际上这块UI的展现并不需要透出底下的背景,那么系统在渲染的过程中就白白浪费了一次透明混合的计算。在这种场景下,最好的方式自然是让设计师直接给出准确的颜色值(使用#FFBFBFBF 而不是 #BF000000)
3. 找到让你动画掉帧的关键路径!
在第 2 点中我们解决了整个动画平均帧率不足的问题,但是还存在我们的动画整体很流畅(FPS>40),却在动画过程中卡了一下的情况,我们需要寻找到在这一掉帧的瞬间发生了什么。
工具3 :traceView
//TO DO : 挖坑 待写
这里用一个实际分析卡顿的例子来看看这个工具的简单用法。。
e.g. Android 5.0 刚出时,在Nexus 5上发现阅读器的某翻页动画非常卡,于是我们先开一下gfxinfo 看一下大概情况:
呃。。。很明显这些顶天的柱状条便是造成我们卡顿的原因,这些单帧已经远远超出了绿色标注线16ms,所以我们下一步需要找到引起这些问题的具体代码;
TraceView可以记录在一段时间内,你的代码里各个函数块的执行时间,也可以看到系统的一些关键方法的执行时间,可以将这些时间进行降序,找到最耗时的代码块。
使用方法可参照 这里
可以先关注这两个属性:
Incl Cpu Time:某函数占用的CPU时间,包含内部调用其它函数的CPU时间
Excl Cpu Time:某函数占用的CPU时间,但不含内部调用其它函数所占用的CPU时间
上方红箭头所示的绿色色块就代表下面红框的drawPosText方法的耗时,可以看到大部分时间都花在了这个函数的执行上,导致到上面3000ms ~ 4000ms之间只有十几帧~ 我们理想的效果是这样的:单次耗时短,1s内画得多
好那么我们就可以从下面drawPosText的Caller一层层向上找到我们自己的代码,定位到问题。
这里很幸运,往上追溯一层就是我自己的代码了。原因确实是drawPosText的绘制耗时。
再往后就是针对这个特定的问题去查阅文档,最后发现是AndroidL的preview版本在开启硬件加速时这个 API 不支持,所以只要换掉这个 API 就ok了 (当然,现在的5.0版本已经修复了这个bug,是可以正常使用的了)。
从这里例子我们可以简单看到使用 TraceView 在性能分析的过程中起的作用,能够很快定位到耗时操作,协助开发者解决问题。使用TraceView可以通过在Eclipse里直接startTrace,也可以在代码里通过Debug.startMethodTracing()来打点;
前者方便易用,缺点是开启了tracing的手机本来就比较卡了,会影响实际的判断。后者好处是更准确,就是使用会相对麻烦一点。
p.s. 另外从这里我们可以看到在 三.1 里提到的开启硬件加速带来的弊端:某些API会在某些系统版本,某些特定Rom上失效,需要考虑各种兼容性问题。
4.其他一些零碎的优化点:
- 避免和系统状态栏,虚拟按键,输入法一起做动画;
这个不太懂根本原因,但是尽可能避免这种场景。
e.g. 应用在切换到全屏状态同时要做一个动画,比如弹一个菜单面板。如果这个动画不要求一定马上出来,那不妨稍等200ms,等顶部状态栏的收起动画做完再做菜单面板动画,会流畅得多。
e.g.2 输入法顶部要挂一个扩展槽/类似QQ的输入框;
由于这个界面是随着输入法的弹出一起弹出的,那么这个界面的淡入动画就会比较卡。同上,我们稍等多150ms,等输入法显示完毕后再开始我们自己的动画,效果就比较好了。
- 减少动画的主体对象和背景之间的视觉差异,颜色越接近,感觉越流畅;
这是QQ浏览器的小说阅读器,它的页边的阴影效果非常淡,由于这里动画的主体是阴影,那么它与后面背景视觉差越小,阴影越透明,那么就会显得越流畅。
- 不同动画曲线的视觉效果
在相同的帧率动画下,不同的动画曲线展现出来的视觉效果相差还是比较大的。在常见的位移动画中,AccelerateDeccelerate 先加速后减速的效果通常状况下会显得更加自然。Animation/Animator的默认插值器都是Linear的,不妨试试换一个自然的曲线来体验一下不同的效果。
当然,你自己也可以写一个Interpolator,实现它的getInterpolation方法便可自由定义自己的动画插值器。
另外,Android L 上已经给开发者们提供了一个非常有用的 PathInterpolator ,可以很方便的定义特殊的动画曲线。
再另外,推荐一个可以预览不同动画曲线的效果的小工具:动画曲线工具
你可以很方便地调节出一条曲线对应直观的动画效果是怎样的,而如何把曲线实现在Interpolator上就需要研究一下贝塞尔曲线或者PathInterpolator咯。
- 改变动画的初始位置 / 初始值,来欺骗视觉
这是一个比较tricky的方法,而且在某些时候会有掉帧的现象,个人不是很推荐,但是在特定场景下还是有用的。
e.g. 做一个渐变的动画,200ms内从无到有的显示:
由于我们对透明度非常低的时候的感知非常微弱,可以让这个动画不从0%开始渐变,而是30%开始,即我们在200ms内完成从30%到100%的效果,这样动画会更加平滑,缺点是阈值控制不好的话大家会发现那个从 0%alpha 到 30%alpha 的跳跃。
- 帧率已经很难再提升了,换动画方案:
不同类型的动画到达看起来差不多“同程度流畅”的帧率要求是不一样的。同样的时间内移动的越短自然越滑~
所以位移动画对帧率的要求最高,旋转/缩放动画次之,渐变动画对帧率要求最低。
如果你发现一个底部弹出菜单的效果优化到头痛都比较卡,或者是在低端机上表现不好,那非常推荐把弹出动画改成一个渐变的淡出动画,尽管帧率可能差不多,但是视觉效果确实非常有效的。