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


背景介绍

       Android性能测试一直存在测试维度少,测试数据难收集,已收集数据难量化的特点,这些特点是因为Android手机版本碎片化、硬件多样化、App功能复杂造成的。

        移动端性能测试的特性,总的来说,可以分为卡顿ANR测试、流畅度测试、电量测试、流量测试。一个APP为什么需要性能测试,总的来说就是存在一些不严谨的代码,在低端机型造成卡顿,对手机上有限电量的浪费,昂贵流量的浪费,造成用户流失。测试人员需要通过性能测试来地发现和定位上述问题。

        在稳定性测试章节中介绍的smartmonkey不仅是个稳定性测试工具,还集成了多个性能测试工具,下面重点介绍下卡顿ANR测试和流量度测试两个工具的原理和应用。


卡顿ANR

        卡顿ANR与Android就是天生的朋友,从Android第一天诞生直到现在的8核CPU,Android还是未能摆脱页面不流畅,卡,死机的诟病,所以个人认为卡顿ANR测试是性能测试最主要的一块。

        卡顿简单的来说,就是手机没有及时响应、页面延迟,出现丢帧的现象,或者点击无响应。绝大多数的卡顿,稍等片刻系统就会恢复正常,但假如超过5S,就可能会引发手机ANR,造成更高级别的警告。如图所示:


什么会引发ANR

        在Android里,应用程序的响应性是由ActivityManager和WindowManager系统服务监视的。当它监测到以下情况中的一个时,Android就会针对特定的应用程序显示ANR:

ANR一般有三种类型:

1.KeyDispatchTimeout(5 seconds) --主要类型按键或触摸事件在特定时间内无响应

2.BroadcastTimeout(10 seconds) --BroadcastReceiver在特定时间内无法处理完成

3.ServiceTimeout(20 seconds) --小概率类型 Service在特定的时间内无法处理完成

这三种原因都会造成ANR,但是第一种情况基本上占了所有ANR的百分之九十以上,第三种的情况微乎其微。这三种ANR不是孤立的,有可能会相互影响。例如一个应用程序进程中同时有一个正在显示的Activity和一个正在处理消息的BroadcastReceiver,它们都运行在这个进程的主线程中。如果BRonReceive函数没有返回,此时用户点击屏幕,而onReceive超过5秒仍然没有返回,主线程无法处理用户输入事件,就会引起第1ANR。如果继续超过10秒没有返回,又会引起第2ANR。发生ANR的主要原因是潜在的耗时操作,例如网络或数据库操作,或者高耗时的计算如改变位图尺寸,应该在子线程里(或者以数据库操作为例,通过异步请求的方式)来完成。然而,不是说你的主线程阻塞在那里等待子线程的完成——也不是调用 Thread.wait()或是Thread.sleep()。替代的方法是,主线程应该为子线程提供一个Handler,以便完成时能够提交给主线程。以这种方式设计你的应用程序,将能保证你的主线程保持对输入的响应性并能避免由于5秒输入事件的超时引发的ANR对话框。

三种ANR发生时都会在log中输出错误信息,你会发现各个应用进程和系统进程的函数堆栈信息都输出到了一个/data/anr/traces.txt的文件中,ROOT手机导出该文件后,分析此日志可以得出一些结论,但traces文件信息比较抽象,难理解。总的来说,日志难收集,结果难分析。


如何避免ANR

1、运行在主线程里的任何方法都尽可能少做事情。特别是,Activity应该在它的关键生命周期方法(如onCreate()onResume())里尽可能少的去做创建操作。(可以采用重新开启子线程的方式,然后使用Handler+Message的方式做一些操作,比如更新主线程中的ui等)

2、应用程序应该避免在BroadcastReceiver里做耗时的操作或计算。但不再是在子线程里做这些任务(因为 BroadcastReceiver的生命周期短),替代的是,如果响应Intent广播需要执行一个耗时的动作的话,应用程序应该启动一个 Service。(此处需要注意的是可以在广播接受者中启动Service,但是却不可以在Service中启动broadcasereciver,关于原因后续会有介绍,此处不是本文重点)

3、避免在IntentReceiver里启动一个Activity,因为它会创建一个新的画面,并从当前用户正在运行的程序上抢夺焦点。如果你的应用程序在响应Intent广播时需要向用户展示什么,你应该使用Notification Manager来实现。

TraceViewandroid SDK中自带的一个性能测试工具,可以在Tools目录下找到。TraceView能很精确的查看到每一个类,每一个类方法的执行时间。APP卡顿我们只需要保留当时的traceview文件,通过查看该文件,就可以定位绝大部分问题。

通过Debug.startMethodTracingStringFileName)和Debug.stopMethodTracing()来记录一段时间内方法执行情况。

      在主线程中不停的插入一个轻量级别的操作,如果该变量在指定的时间内,没有改变,则说明此刻APP卡顿。卡顿工具工具就是根据https://github.com/SalomonBrys/ANR-WatchDog项目改进而成的。把ANR这种警告变成错误让APP闪退,持久化当时信息。

原理图:


实例:百度国际化浏览器ANR检测和问题定位

    问题背景:

百度国际化浏览器初次安装App,点击icon后,明显卡顿或者ANR,QA手工测试无法定位,RD优化代码多次依旧找不到问题的节点。

    测试方法:

百度国际化浏览器加入卡顿工具jar,代码中初始化。

    测试结果分析:

在打开traceview 文件后,通过 realTime/Call 从大到小排序,找到对应的与代码相关消耗时间最大的方法。

        我们能够看到很明显的看出FrameWindow.initDataBase()方法占用CPU过长达到3S左右了,距离5S很接近,通过查看代码,结合业务逻辑,得知此处为数据库初始化,并且主要是标签数据库初始化。

        从整个APP来看,启动页面初始化标签数据库并没有错,但是此刻本身逻辑就非常多,标签数据库初始化后并没有马上使用到,而是到二级页面才有查询动作,总的来说,就是增加资源紧张。

        建议:标签数据库什么时候使用,什么时候初始化。建议放到二级页面初始化,减少页面App页面启动的负荷,减少冷启动时间,避免卡顿和ANR。并且标签数据库初始化放在线程中。


流畅度测试

         流畅度测试简单的来说就是Android页面绘制。Android系统每秒60hz,也就是大约每16ms刷新一次界面。但是在我们使用APP过程中,经常会看到页面有卡顿,或者说丢帧的现象。也就是说可能此刻两个页面绘制的时间差超过0.1S(人眼视觉残留0.1S)。

        在确定衡量指标之前,我们先来研究一下Android的UI更新机制。


原理分析

在确定衡量指标之前,我们先来研究一下Android的UI更新机制。

1、Android如何绘制UI?

关于Android是如何更新UI,相信已经有很多文章介绍其中的步骤以及过程,大体上可以用下图来展示:

        从图中可以看到无论那条路走下去始终都由SurfaceFlinger来控制最后的更新。

        在Android版本更新过程中,发现在JellyBean中Google加入了一个Project Butter,用来解决严重影响Android口碑的问题之一“UI流畅性差”的问题。而Project Butter中主要引入了三个核心元素:VSYNC(垂直同步)、Triple Buffer和Choreographer。


2、从VSYNC开始

        VSync是VerticalSynchronization(垂直同步)的缩写,是一种在PC上很早就广泛使用的技术,可以简单的把它认为是一种定时中断。而在Android 4.1(JB)中已经开始引入VSync机制。

        上图所示是VSync机制下的绘制过程。从上图可以看出,CPU和GPU的处理时间都少于一个VSync的间隔,即16.6ms。如果每个间隔都有绘制的情况下,当前的FPS即为60帧。

        当CPU和GPU处理时间都很慢,或因为其他的原因,如在主线程中干活太多,那么就会出现如下图这样的状况。

 

        从上图可以看到,CPU和GPU的处理时间因为各种原因都大于一个VSync的间隔(16.6ms),所以在第二个VSync还在处理1区域的绘制时,不可能实现理论上的FPS60,同时也出现了丢帧(SF: Skipped Frame)情况。

        为了便于理解,上图用的是双Buffer机制的情况,实际上Android4.1引入了Triple Buffer,所以当双Buffer不够用时,Triple Buffer丢帧的情况如下图所示。

        VSync机制就像是播放动画片(60帧/s)。每次都会播放画面,有的时候有人偷懒了,机器坏了,就会出现播放速度降低的状况。我们把这个播放速度叫做流畅度。


3、从FPS&丢帧到流畅度(SM:SMoothness)

        实际上在很多Android的App中,很少有需要不断地去绘制的场景,很多时候页面都是静态的。也就是会出现这样的状况,虽然1s中VSync的60个Loop不是每个都在做绘制的工作,FPS会比较低,但并不代表这个时候程序不流畅(如我将App放着不动,实测FPS为1)。所以FPS较低并不能代表当前App在UI上界面不流畅,而1s内VSync这个Loop运行了多少次更加能说明当前App的流畅程度。所以,下面这2个指标比FPS更能代表当前的App是否处于流畅的状态。同样这2个指标更加能够量化App卡顿的程度:

    a)丢帧(SF:Skipped Frame):如上图2所示情况应该在16.6ms完成工作却因各种原因没做完,占了后n个16.6ms的时间,相当于丢了n帧。

    b) 流畅度(SM:SMoothness):和丢帧相对,在VSync机制中1s内Loop运行的次数。

        和丢帧相对1s内有60个Loop因为某几次工作时间超过了16.6ms(丢帧),这样Loop就无法运行60次(理论最大值)。

当流畅度越小的时候说明当前程序越卡顿。


4、记数:如何得到流畅度(SM:SMoothness)

        接着上面的结论,如果在这样的机制下每次Loop运行之前进行通知,记个数就好了。

        很幸运我们在新的Android的那一套机制中找到了一个画图的打杂工Choreographer这个对象。根据Google的官方API文档描述中,它是用来协调animations、input以及drawing时序的,并且每个Loop共用一个Choreographer对象。

下图为Choreographer的定义和结构。


结论

通过如上原理分析可以得出结论:

1. Android 4.1引入了VSync机制后,可以通过其Loop来了解当前App最高绘制能力。

    固定每隔16.6ms执行一次(这个值是一个静态变量,会根据系统版本不同而采用不同的值,目前测试版本是16.6ms这样最高的刷新的帧率就控制在60FPS以内);

    如果没有以上事件的时候同样也会运行这样一个Loop;

    这个Loop在1s之内运行了多少次,即可以表示当前App绘制的最高的能力,也就是Android App卡顿的程度;

    另外,在一次Loop时如果执行时间超过了16.6ms,那么用多于16.6ms的时间除以16.6ms,即是当前App的丢帧(SF:Skipped Frame)。


2. 可以在Choreographer的回调FrameCallback中,按秒计数表示当前App的流畅程度,即流畅度SM(SMoothness)。

        采用这样方式就可以在App内部观测当前App的流畅度了。并且在丢帧的地方打印traceView,就可以知道丢帧的大概原因,大概位置,定位代码问题。


如果你看的意犹未尽,如果你想随时随地充实自己,请扫描以下二维码,关注我们的公众账号,可以获取更多技术类干货,还有精彩活动与你分享~


大咖招募
欢迎App研发/测试方面的大牛来投稿,开设专栏。我们提供丰厚的稿酬,预约个人专访,帮助建立个人技术品牌!
立即投稿

我要评论

字数不能超过140字,谢谢!
提交

评论({{allComments.length}})

{{comment.create_time.substr(0,16)}}

显示所有评论
复制成功!