Android中app卡顿原因分析示例

作者:朱才
链接:http://www.zhihu.com/question/24541467/answer/29020774
来源:知乎

上次主要针对题主说的具体问题“刷微博在苹果和Android手机上的差异”在具体的设备上做实验后进行了分析。
今天我对“Android的流畅性”做个更进一步的讨论。加在后面。

===============================================================

我来说下我所知道的事情。我不知道iOS为什么流畅,但我知道一些Android为什么不流畅的原因。

首先,就题主所说的问题,我用iPad和小米Pad对比了一下微博滑动滚屏这件事情(2014年8月10日目前微博app最新版本,打开的是“首页”和“发现”栏)。正如题主所说,直观感受上明显感觉iOS要流畅、舒服。

在这件事情上我认为主要是这三个原因:

  1. 速度曲线。
    当你滑动界面然后松手,这时界面会继续滑动,然后速度减小,直到速度为0时停止。iOS下速度减小的这个过程比较慢,尤其是快要停的时候是慢慢停的,视觉上有种很顺滑的感觉;Android下则从松手到停要快很多,相比之下有种戛然而止的感觉。
    从数据/技术角度来看这个事情,我们滑动界面的最终目的不是为了“动”,而是为了“停”,因此只要平滑的到达目的地,似乎越快完成这个过程越好,所以Android的选择是理所当然的。但事实是,大家普遍更喜欢iOS的方式,这样做显得更顺滑、更优雅。

  2. 帧率。
    绝大部分时间两者都能保持60FPS左右的满帧率。但都会有偶尔的掉帧。并且Android上要比iOS上严重很多。(好吧,比起前两年,已经好太多了。)我前前后后滑动了几十次,iOS在前面遇到1次掉帧,后面就很稳定了。而Android几乎每滑动一次都会伴随一次掉帧。这完全就是真真实实的卡顿,用户必然会感觉到那一刻的不流畅。Android掉帧的原因我后面再详细分析。

  3. 触摸响应速度。
    从手指碰到触摸屏,到屏幕上显示处理这次触摸产生的画面,是需要时间的。时间越短感觉越跟手。据说iOS的触摸屏的处理时间要比一般的Android手机快,这不是我的专长,不知道怎么验证。但在软件系统层面,Android的显示机制是app-->SurfaceFlinger-->Display,这比传统的app-->Display多了一步,主要基于这个原因,画面最终输出到屏幕要比传统的方式慢一帧(16.7ms)。


我觉得第1点造成的影响最大,恰恰却是最技术/设备无关的。如果微博app或者Android系统要改变,很容易就可以调得跟iOS一模一样。但正是由于这是产品形态上的差别而不是纯粹技术上的优劣,反倒成了Android最不太可能改变的。

第2点的影响其次,当然是指在目前这个大部分时候都能满帧的情况下。这方面是Android从早期到现在进步最明显的方面,使用了很多方法来优化帧率。但就算现在Android进化了很多,硬件性能也进化了很多,却仍旧不可能彻底消灭掉帧的情况。

第3点通俗的讲就是跟手性,跟手性的重要性不言而喻,但现在的差别比较细微,且具体数据我也不清楚。
我想过一个办法让桌面、微博这种内容和手一起动的应用绘制到屏幕的速度快一帧(16.7ms),其实就是抵消之前提到的慢的那一帧,需要framework层和app层一起配合改动,目前已申请了专利但代码还没进,将来有时间了应该会进到MIUI。感兴趣的可以看看专利:滑动操作响应方法、装置及终端设备

===============================================================

最后我来用专业技术分析一下微博app在Android里掉帧的原因。非编程人员可以跳过。(这个过程我使用的是小米3高通版+最新版微博app。)

最初,我认为这种现象很像GC(垃圾回收)导致的,于是打开logcat观察每次卡顿的时候有没有GC发生。结果发现并没有。停下来的时候才会有GC,这说明微博app在滑动过程中控制得不错,在停下来的一刻才去分配内存,使GC不影响帧率。

然后我打开“开发者选项”->“GPU呈现模式分析”->“在屏幕上显示为条形图”(好像是4.4才有这个选项,之前的只能在dumpsys里看),这会在屏幕上直观的显示每帧绘制花费的时间。屏幕上有条基准线大概是16ms,如果超过这条线则很有可能掉帧了。如果下面的蓝色部分很长则说明是软件draw的部分太费时,那么可以通过traceview来继续分析draw的java代码。(我假定了现在的app都是硬件加速的方式。)如果中间红色部分很长则说明是OpenGL ES绘制过程太费时。可以用gltrace来分析OpenGL ES的调用过程。
打开选项后使用微博app,发现虽然偶尔有超基准线,但并不严重,并且卡顿的时候并没有明显异常。

于是我开始使用systrace,这下就很明显了:
可以发现每隔几帧,就会有一次大间隙,图中我圈了红圈圈的两个地方都明显超过正常的耗时时间。现在问题是,这些地方的代码在干什么呢?是什么花费了这么长时间?
放大后发现都是这两个:obtainView和setupListItem。它们都是在draw之前调用的,这也解释了通过上一步的方法(“GPU呈现模式分析”)为什么没有显示出来,因为那个方法只展示draw里面的时间消耗。
通过在源码里搜索obtainView和setupListItem,可以发现是AbsListView的obtainView方法和ListView的setupChild方法打下的这个log。

接下来,我们用traceview来看看这两个方法里面究竟调用了什么代码导致耗时过长。
跟踪obtainView最终到了这里:
代码混淆了,看不太出来了,也懒得再跟下去了。但随便点了点然后看到:
TextView.setText用了挺多的时间。感觉Android团队应该优化优化这个方法。
然后再来跟踪setupChild方法:
都是measure占用的时间,并且调用次数比较多。这应该说明了View的数量不少。用hierarchyviewer看了下,的确不少,一条微博有50多个View:

结论:我们最终大致找到了耗时的代码及部分原因。这两个耗时方法应该都是有可能减下来的,但应该不那么容易。

===============================================================

具体问题分析完了,这里讲讲整体Android的流畅问题。

先聊聊目前最高票数的答案 邑封:Android 的屏幕滚动操作不如 iPhone 流畅顺滑,是什么原因导致的?。
总的来讲,前面的内容都没问题:Android从最初就对Window级别的动画用了硬件加速(在SurfaceFlinger用OpenGL ES绘制)。3.0开始View也可用硬件加速来绘制。

随后这个答案里说多个窗口的不同进程中的GL上下文切换代价很高。

  • 首先,现在的GPU一般都有MMU,可以方便的切换上下文。

  • 其次,大部分的情况下,Android都只有一个需要GL绘制的窗口,就是当前使用的app窗口。其他显示的窗口如状态栏,在没有内容更新的时候是不需要绘制的,SurfaceFlinger上已经存储着上一次绘制的结果。
    答案里提到了窗口内容“最后的组合”,这个是每帧都要做的,只是,SurfaceFlinger在大部分情况下是用的Hardware Composer以overlay的方式来合成的,并不需要OpenGL ES,在不满足Hardware Composer合成条件的情况下才会用OpenGL ES合成。Hardware Composer比OpenGL ES的方式性能更好、耗电更低。(我们有过测试,我们的数据是耗电大概少20%)。
    这里再说一下究竟哪些情况是可以用Hardware Composer来合成的,Android对厂商的建议是有虚拟键的应支持4层合成(常规的桌面:壁纸+桌面+状态栏+虚拟键),没有虚拟键的是3层。这可以看出在桌面上放一个第三方app的悬浮窗口对性能及耗电是有不小影响的。


随后答案提到渲染2.5次的数据。是的,我曾在某Google工作人员博客或是官方文档里看到这样的说法,建议我们每个窗口渲染的像素数不要超过窗口大小的2.5倍。
于是我写了个程序测试每帧绘制全屏图片N次时的帧率。
测试程序:files.cnblogs.com/zhuca
源码:files.cnblogs.com/zhuca
大家都可以下载这个程序测试自己手机的表现结果。主要测试像素渲染性能、纹理加载性能。(点击“开始测试”后,中间的数字是最近1秒绘制的帧数,通常越大越好。)
我的测试结果是在米2上可以绘制18次全屏图像仍然满帧率。在米4上绘制21次仍然是满帧率。
可以看到现在的GPU渲染性能是非常好的。单纯的渲染像素数量不会是瓶颈。(当然,可以的话还是渲染内容越少越好。)

最后答案说:后台程序优先级低,它们满打满算也无法占用超过 10% 的 CPU 资源。
于是我又写了个测试程序:files.cnblogs.com/zhuca
源码:files.cnblogs.com/zhuca
(点击“启动线程”后按HOME键退出,看其他app是否使用流畅。按Back退出也行,不过会有内存泄露。)
测试结果发现后台进程的CPU占用的确被限制了,对其他app的UI流畅性影响很小。
不过可惜,Android的设计一直是本着开放的态度,它允许我们自己把自己声明为前台进程(测试程序中点击“启动前台服务”),虽然被切换到后台了,可还是按前台进程来对待。经测试,这时候其他app的界面卡得一塌糊涂。桌面滑动非常卡。
顺便说一下,这种把自己声明为前台进程的程序,第三方app没ROOT的话是杀不死它的。在MIUI里,则可以被“最近任务”界面的上划图标或一键清理给杀死的。(想到了MIUI 6安全中心的一句台词:系统级的安全)


对于Android系统的流畅性,Android确实为之做了很多努力,如:窗口级动画早就是硬件加速,Android3.0开始View级绘制也支持硬件加速,Android4.1使用VSync和3缓冲,SurfaceFlinger的overlay机制,等等。

但Android的坑也很多:

  • 垃圾回收时会暂停所有线程,碰到几乎必然卡帧。(最近改善:4.4允许图片在不同大小时重用;ART改善垃圾回收。)

  • 上面说了渲染像素是非常快的,可是纹理加载则是非常慢的。上面提到米2可以绘制18次全屏图像保持满帧率,可如果每帧都修改则只能承受一张的全屏图像,当有2张全屏图像每帧都修改的话就达不到满帧率了。(测试程序选上“每帧修改图片”可以测试这个。)基于这个原因,频繁修改的软件层View慎用。

  • 设置View的alpha会导致每次发生离屏缓冲。(最近改善:大概是4.2开始可以设置为alpha不导致离屏缓冲)

  • Canvas的saveLayer不加CLIP_TO_LAYER_SAVE_FLAG则会有个Texture Copy方法非常耗时。某些情况下设置alpha等也会碰到。

  • 某些情况下碰到quickReject失效时会尝试绘制不在屏幕上的元素。

  • 等等。


总之,在Android上实现功能比较容易,但如果默认实现有了性能问题,要想解决就不好说了,有时候要绕很远,有时候甚至绕不过去。

===============================================================

注:第一个测试程序里“图片数量”大于一定值(我的米2是13)的时候帧率会大幅降低,这是因为超过了纹理缓存最大值。


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值