摆脱Android的“好朋友”卡顿ANR、不流畅、死机!

RhoScroggin 8年前
   <h2><strong>背景介绍</strong></h2>    <p>Android性能测试一直存在测试维度少,测试数据难收集,已收集数据难量化的特点,这些特点是因为Android手机版本碎片化、硬件多样化、App功能复杂造成的。</p>    <p>移动端性能测试的特性,总的来说,可以分为卡顿ANR测试、流畅度测试、电量测试、流量测试。一个APP为什么需要性能测试,总的来说就是存在一些不严谨的代码,在低端机型造成卡顿,对手机上有限电量的浪费,昂贵流量的浪费,造成用户流失。测试人员需要通过性能测试来地发现和定位上述问题。</p>    <p>在稳定性测试章节中介绍的smartmonkey不仅是个稳定性测试工具,还集成了多个性能测试工具,下面重点介绍下卡顿ANR测试和流量度测试两个工具的原理和应用。</p>    <h2><strong>卡顿ANR</strong></h2>    <p>卡顿ANR与Android就是天生的朋友,从Android第一天诞生直到现在的8核CPU,Android还是未能摆脱页面不流畅,卡,死机的诟病,所以个人认为卡顿ANR测试是性能测试最主要的一块。</p>    <p>卡顿简单的来说,就是手机没有及时响应、页面延迟,出现丢帧的现象,或者点击无响应。绝大多数的卡顿,稍等片刻系统就会恢复正常,但假如超过5S,就可能会引发手机ANR,造成更高级别的警告。如图所示:</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/362a907a7c9f1c6948ad64b8a3e0b950.jpg"></p>    <h2><strong>什么会引发ANR?</strong></h2>    <p>在Android里,应用程序的响应性是由ActivityManager和WindowManager系统服务监视的。当它监测到以下情况中的一个时,Android就会针对特定的应用程序显示ANR:</p>    <p>ANR 一般有三种类型:</p>    <p>1.KeyDispatchTimeout(5 seconds) -- 主要类型按键或触摸事件在特定时间内无响应</p>    <p>2.BroadcastTimeout(10 seconds) --BroadcastReceiver 在特定时间内无法处理完成</p>    <p>3.ServiceTimeout(20 seconds) -- 小概率类型 Service 在特定的时间内无法处理完成</p>    <p>这三种原因都会造成 ANR ,但是第一种情况基本上占了所有 ANR 的百分之九十以上,第三种的情况微乎其微。这三种 ANR 不是孤立的,有可能会相互影响。例如一个应用程序进程中同时有一个正在显示的 Activity 和一个正在处理消息的 BroadcastReceiver ,它们都运行在这个进程的主线程中。如果 BR 的 onReceive 函数没有返回,此时用户点击屏幕,而 onReceive 超过 5 秒仍然没有返回,主线程无法处理用户输入事件,就会引起第 1 种 ANR 。如果继续超过 10 秒没有返回,又会引起第 2 种 ANR 。发生 ANR 的主要原因是潜在的耗时操作,例如网络或数据库操作,或者高耗时的计算如改变位图尺寸,应该在子线程里(或者以数据库操作为例,通过异步请求的方式)来完成。然而,不是说你的主线程阻塞在那里等待子线程的完成——也不是调用 Thread.wait() 或是 Thread.sleep() 。替代的方法是,主线程应该为子线程提供一个 Handler ,以便完成时能够提交给主线程。以这种方式设计你的应用程序,将能保证你的主线程保持对输入的响应性并能避免由于 5 秒输入事件的超时引发的 ANR 对话框。</p>    <p>三种 ANR 发生时都会在 log 中输出错误信息,你会发现各个应用进程和系统进程的函数堆栈信息都输出到了一个 /data/anr/traces.txt 的文件中, ROOT 手机导出该文件后,分析此日志可以得出一些结论,但 traces 文件信息比较抽象,难理解。总的来说,日志难收集,结果难分析。</p>    <h2><strong>如何避免ANR?</strong></h2>    <p>1 、运行在主线程里的任何方法都尽可能少做事情。特别是, Activity 应该在它的关键生命周期方法(如 onCreate() 和 onResume() )里尽可能少的去做创建操作。(可以采用重新开启子线程的方式,然后使用 Handler+Message 的方式做一些操作,比如更新主线程中的 ui 等)</p>    <p>2 、应用程序应该避免在 BroadcastReceiver 里做耗时的操作或计算。但不再是在子线程里做这些任务(因为 BroadcastReceiver 的生命周期短),替代的是,如果响应 Intent 广播需要执行一个耗时的动作的话,应用程序应该启动一个 Service 。(此处需要注意的是可以在广播接受者中启动 Service ,但是却不可以在 Service 中启动 broadcasereciver, 关于原因后续会有介绍,此处不是本文重点)</p>    <p>3 、避免在 IntentReceiver 里启动一个 Activity ,因为它会创建一个新的画面,并从当前用户正在运行的程序上抢夺焦点。如果你的应用程序在响应 Intent 广播时需要向用户展示什么,你应该使用 Notification Manager 来实现。</p>    <p>TraceView 是 android SDK 中自带的一个性能测试工具,可以在 Tools 目录下找到。 TraceView 能很精确的查看到每一个类,每一个类方法的执行时间。 APP 卡顿我们只需要保留当时的 traceview 文件,通过查看该文件,就可以定位绝大部分问题。</p>    <p>通过 Debug.startMethodTracing ( StringFileName )和 Debug.stopMethodTracing() 来记录一段时间内方法执行情况。</p>    <p>在主线程中不停的插入一个轻量级别的操作,如果该变量在指定的时间内,没有改变,则说明此刻APP卡顿。卡顿工具工具就是根据 <a href="/misc/goto?guid=4959613859338548615" rel="nofollow,noindex"> https://github.com/SalomonBrys/ANR-WatchDog </a> 项目改进而成的。把ANR这种警告变成错误让APP闪退,持久化当时信息。</p>    <p><strong>原理图:</strong></p>    <p><strong><img src="https://simg.open-open.com/show/c9ab28ac5307d8312736088e1c394ecc.png"> </strong></p>    <p><strong>实例:百度国际化浏览器ANR检测和问题定位</strong></p>    <p>问题背景:</p>    <p>百度国际化浏览器初次安装App,点击icon后,明显卡顿或者ANR,QA手工测试无法定位,RD优化代码多次依旧找不到问题的节点。</p>    <p>测试方法:</p>    <p>百度国际化浏览器加入卡顿工具jar,代码中初始化。</p>    <p>测试结果分析:</p>    <p>在打开traceview 文件后,通过 realTime/Call 从大到小排序,找到对应的与代码相关消耗时间最大的方法。</p>    <p style="text-align: center;"><strong><img src="https://simg.open-open.com/show/c625bce70e2a1150a4e0478cea8ec2db.png"> </strong></p>    <p>我们能够看到很明显的看出FrameWindow.initDataBase()方法占用CPU过长达到3S左右了,距离5S很接近,通过查看代码,结合业务逻辑,得知此处为数据库初始化,并且主要是标签数据库初始化。</p>    <p>从整个APP来看,启动页面初始化标签数据库并没有错,但是此刻本身逻辑就非常多,标签数据库初始化后并没有马上使用到,而是到二级页面才有查询动作,总的来说,就是增加资源紧张。</p>    <p>建议:标签数据库什么时候使用,什么时候初始化。建议放到二级页面初始化,减少页面App页面启动的负荷,减少冷启动时间,避免卡顿和ANR。并且标签数据库初始化放在线程中。</p>    <h2><strong>流畅度测试</strong></h2>    <p>  流畅度测试简单的来说就是Android页面绘制。Android系统每秒60hz,也就是大约每16ms刷新一次界面。但是在我们使用APP过程中,经常会看到页面有卡顿,或者说丢帧的现象。也就是说可能此刻两个页面绘制的时间差超过0.1S(人眼视觉残留0.1S)。</p>    <p>在确定衡量指标之前,我们先来研究一下Android的UI更新机制。</p>    <p><strong>原理分析</strong></p>    <p>在确定衡量指标之前,我们先来研究一下Android的UI更新机制。</p>    <p><strong>1、Android如何绘制UI?</strong></p>    <p>关于Android是如何更新UI,相信已经有很多文章介绍其中的步骤以及过程,大体上可以用下图来展示:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/8ad96e993872b04fd6948e2d904bdb3b.png"></p>    <p>从图中可以看到无论那条路走下去始终都由SurfaceFlinger来控制最后的更新。</p>    <p>在Android版本更新过程中,发现在JellyBean中Google加入了一个Project Butter,用来解决严重影响Android口碑的问题之一“UI流畅性差”的问题。而Project Butter中主要引入了三个核心元素:VSYNC(垂直同步)、Triple Buffer和Choreographer。</p>    <p><strong>2、从VSYNC开始</strong></p>    <p>VSync是VerticalSynchronization(垂直同步)的缩写,是一种在PC上很早就广泛使用的技术,可以简单的把它认为是一种定时中断。而在Android 4.1(JB)中已经开始引入VSync机制。</p>    <p><img src="https://simg.open-open.com/show/470586210615421fedb5d0580e695ea1.png"></p>    <p>上图所示是VSync机制下的绘制过程。从上图可以看出,CPU和GPU的处理时间都少于一个VSync的间隔,即16.6ms。如果每个间隔都有绘制的情况下,当前的FPS即为60帧。</p>    <p>当CPU和GPU处理时间都很慢,或因为其他的原因,如在主线程中干活太多,那么就会出现如下图这样的状况。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/4583180d94ac0035ed5572496f419081.png">  </p>    <p>从上图可以看到,CPU和GPU的处理时间因为各种原因都大于一个VSync的间隔(16.6ms),所以在第二个VSync还在处理1区域的绘制时,不可能实现理论上的FPS60,同时也出现了丢帧(SF: Skipped Frame)情况。</p>    <p>为了便于理解,上图用的是双Buffer机制的情况,实际上Android4.1引入了Triple Buffer,所以当双Buffer不够用时,Triple Buffer丢帧的情况如下图所示。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/3e777feea3f538f0d15c2461492260fb.png" alt="摆脱Android的“好朋友”卡顿ANR、不流畅、死机!" width="550" height="247"></p>    <p>VSync机制就像是播放动画片(60帧/s)。每次都会播放画面,有的时候有人偷懒了,机器坏了,就会出现播放速度降低的状况。我们把这个播放速度叫做流畅度。</p>    <p><strong>3、从FPS&丢帧到流畅度(SM:SMoothness)</strong></p>    <p>实际上在很多Android的App中,很少有需要不断地去绘制的场景,很多时候页面都是静态的。也就是会出现这样的状况,虽然1s中VSync的60个Loop不是每个都在做绘制的工作,FPS会比较低,但并不代表这个时候程序不流畅(如我将App放着不动,实测FPS为1)。所以FPS较低并不能代表当前App在UI上界面不流畅,而1s内VSync这个Loop运行了多少次更加能说明当前App的流畅程度。所以,下面这2个指标比FPS更能代表当前的App是否处于流畅的状态。同样这2个指标更加能够量化App卡顿的程度:</p>    <p>a)丢帧(SF:Skipped Frame):如上图2所示情况应该在16.6ms完成工作却因各种原因没做完,占了后n个16.6ms的时间,相当于丢了n帧。</p>    <p>b) 流畅度(SM:SMoothness):和丢帧相对,在VSync机制中1s内Loop运行的次数。</p>    <p>和丢帧相对1s内有60个Loop因为某几次工作时间超过了16.6ms(丢帧),这样Loop就无法运行60次(理论最大值)。</p>    <p>当流畅度越小的时候说明当前程序越卡顿。</p>    <p><strong>4、记数:如何得到流畅度(SM:SMoothness)</strong></p>    <p>接着上面的结论,如果在这样的机制下每次Loop运行之前进行通知,记个数就好了。</p>    <p>很幸运我们在新的Android的那一套机制中找到了一个画图的打杂工Choreographer这个对象。根据Google的官方API文档描述中,它是用来协调animations、input以及drawing时序的,并且每个Loop共用一个Choreographer对象。</p>    <p>下图为Choreographer的定义和结构。</p>    <p><img src="https://simg.open-open.com/show/0eae318bbcc6a96ff5bcc9417a4f27be.png"></p>    <p><strong>结论</strong></p>    <p>通过如上原理分析可以得出结论:</p>    <p>1. Android 4.1引入了VSync机制后,可以通过其Loop来了解当前App最高绘制能力。</p>    <p>固定每隔16.6ms执行一次(这个值是一个静态变量,会根据系统版本不同而采用不同的值,目前测试版本是16.6ms这样最高的刷新的帧率就控制在60FPS以内);</p>    <p>如果没有以上事件的时候同样也会运行这样一个Loop;</p>    <p>这个Loop在1s之内运行了多少次,即可以表示当前App绘制的最高的能力,也就是Android App卡顿的程度;</p>    <p>另外,在一次Loop时如果执行时间超过了16.6ms,那么用多于16.6ms的时间除以16.6ms,即是当前App的丢帧(SF:Skipped Frame)。</p>    <p>2. 可以在Choreographer的回调FrameCallback中,按秒计数表示当前App的流畅程度,即流畅度SM(SMoothness)。</p>    <p>采用这样方式就可以在App内部观测当前App的流畅度了。并且在丢帧的地方打印traceView,就可以知道丢帧的大概原因,大概位置,定位代码问题。</p>    <p> </p>    <p> </p>    <p> </p>    <p>来自:http://qa.baidu.com/academy/detail/article/122</p>    <p> </p>