【安卓稳定性之ANR】第一篇:安卓ANR问题综述

本文针对anr问题分析的一般套路与技巧进行了总结与归纳。
以下所有内容均为本人的个人理解以及经验积累,偏向于实战若有任何问题,请给出建议及帮忙进行纠错。
通过本文的阅读,你将有以下收获:
1:了解什么是anr
2:了解anr的简单分析套路
3:了解攻克较难无响应问题的部分手段

一、安卓无响应问题概述

ANR,应用程序无响应即Application not Responding,以下简称“无响应”或“anr”。

在安卓系统中,当应用因为Java Exception与Native Exception发生异常时都会启动对应闪退机制让应用程序死亡。无论是通过DefaultUncatchedExceptionHandler处理的JE,或是通过信号杀死进程的NE,最终都会使应用程序结束,让用户有一个“更好的用户体验”。想象一个场景,如果应用的某线程发生异常并仅在线程级别终止,在用户看来应用可能还没有发生异常,但是有功能却无法使用,这时候只能等用户发现之后自行杀死进程,这是极为不友好的。
与闪退相对应的,安卓中也有无响应机制。无响应与闪退不同,应用程序闪退时一定是发生了某种问题。但是无响应时,可能应用的整个进程没有发生任何问题,只是“慢了”。虽然应用可能没有问题发生,但是依然会弹出“应用程序无响应对话框”,安卓之所以要这样设计,是为了满足“与用户实时的交互”。
经常会听到用户说我手机卡了一下,刷微博手机有些卡顿。其实这些卡顿,在很多情况下是由于软件程序运行时效率慢了,不能与用户进行实时性的交互。严格一点的卡顿就是掉帧,以手机每秒刷新60帧为例,若手机每秒的刷新率低于60帧就是掉帧,则每隔16.67ms手机就需要绘制一帧,

这个绘制是执行在应用主线程,如果应用主线程由于某些消息执行耗时导致绘制被延迟,就发生了掉帧。如果应用主线程的某些消息执行了更长的时间(1秒、2秒或3秒),则会导致用户肉眼可见的卡顿。如果消息执行的时间还能更久,或者有可能是永远卡在这里呢?那用户后续的点击事件在
此消息执行完毕之前就再也不能被应用消费了。造成的现象就是,用户发现应用彻底卡死了,并无法用任何操作控制应用。这时候,无响应机制的作用就体现出来了,当可以发现应用卡住了,则会弹出一个对话框,让用户可以选择关闭应用或者继续等待,有些时候等一会应用也会恢复正常,原因是应用主线程的消息没有被永久卡住。

为了能有更加详细的认识,我介绍几个例子说明以上情况:

  1. 应用主线程卡住,等一会就会恢复正常的情况:应用开发者可能会在应用的主线程进行IO,IO耗时过久可能会导致无响应,如MIUIROM-78975;应用主线程正在timewaiting,即使没有被唤醒也会在计时结束后自行进入Runable状态抢夺CPU调度权;应用就是在生命周期时相关操作慢了,机器性能也不好,刚好卡在了input事件的5秒阈值导致无响应等等。
  2. 若用户不点击关闭,应用将永远卡死:应用发生死锁MIUI-1929582

以上,在我看来“无响应机制”是安卓系统为应用程序的“慢”、“卡顿”所做的一个兜底策略,如果“卡顿”太久,就会触发“无响应”机制:“前台应用弹出‘无响应对话框’,后台应用由于用户无感知直接杀死”。“无响应”机制就像是悬在应用开发者头上的一根利剑,督促着应用开发者优化应用加载、启动、运行时逻辑。常用来对抗“无响应机制”的方案就是异步线程,多进程启动加载等,具体关于此方面的开发经验我尚有不足,故不多展开介绍。

简单说明一下anr的分类以及“无响应机制”。根据anr reason可以将无响应分为四类:

  1. Service Timeout:服务没有及时start,bind,unbind,前台服务20s,后台服务200秒。其中startService是在AMS中的realStartServiceXXX()方法中“埋下的炸弹”(发送给AMS中mainHandler的延迟消息)。
  2. Broadcast Timeout:广播没有及时被处理完成,这里说明一下,只有有序广播会导致无响应,前台广播10s,后台广播60秒。广播是在BroadcastQueue中的processNextBroadcastXXX()方法中“埋下的炸弹”。
  3. contentprovider Timeout:contentprovider仅在发布时会发生无响应,发布时间10s,具体代码位置忘记了。有jira问题链接:MIUI-1989875
  4. input Timout:应用消费input超时,超时时间为5秒,也可以自定义时间为8秒,具体代码位置忘记了,可以自行查看源代码。

此外,关于无响应机制的详解,可以看我实习期产出的wiki【安卓稳定性之ANR】第三篇:ANR小结,或者网上其他相关博客如http://gityuan.com/2016/07/02/android-anr/,更好的是可以直接看安卓源代码,自己总结印象会更深刻。

简单无响应问题分析套路

工欲善其事必先利其器,这里的“器”不是指看日志的软件,而是我们解问题可以依赖的源码日志。当然,我也推荐一个我喜欢用的看日志软件:UltraEdit。

这里有一份很简单无响应问题的日志bugreport-XXXX-xxxxx-2021-04-21-09-58-34.zip(CSDN好像不能上传文件,哈哈那就算了。)

无响应问题中我们主要关注两个日志,1:/bugreport-XXXX-xxxxx-2021-04-21-09-58-34.txt,以下简称“日志”;2:/FS/data/anr/anr_2021-04-21-09-53-11-099,以下简称“trace文件”或“trace”。拿到日志,先要确认发生无响应的应用(斗鱼),以及无响应发生的时间。在日志中搜索“am_anr”,发现以下日志;

04-21 09:53:10.072  1000  1685 28676 I am_anr  : [0,26182,air.tv.douyu.android,955792964,Input dispatching timed out (air.tv.douyu.android/com.douyu.module.home.pages.main.MainActivity,
dc674dc air.tv.douyu.android/com.douyu.module.home.pages.main.MainActivity (server) is not responding. Waited 5004ms for FocusEvent(hasFocus=false))]

从该日志中,我们可以确认以及需要的信息为:

  1. 无响应的应用包名:air.tv.douyu.android(斗鱼)
  2. 无响应发生的大致时间:09:53:10.072(一般来说,可以认为am_anr打印的时间就是真正发生无响应的时间,实际上发生无响应的时间比这个还要早一点,一般是相应消息timeout的那一瞬间,但是可以忽略不计。需要知道的是,打印出am_anr到前台应用弹出“无响应窗口”之间的时间可能会有几秒的延迟。)
  3. 无响应的reason:Input dispatching timed out (air.tv.douyu.android/com.douyu.module.home.pages.main. Waited 5004ms for FocusEvent(hasFocus=false)) input超时5秒。
  4. 应用进程号:26182

知道以上信息之后,再看trace文件,主要关注主线程。

----- pid 26182 at 2021-04-21 09:53:11 -----
Cmd line: air.tv.douyu.android
ABI: 'arm'
Build type: optimized
Zygote loaded classes=15966 post zygote classes=4642
//classloader信息
 
//类加载耗时
Classes initialized: 3578 in 1.384s
Intern table: 41047 strong; 665 weak
JNI: CheckJNI is off; globals=838 (plus 138 weak)
//应用so库
Libraries: /data/app/~~ZZaMW4uMrb1D6k-GJZdTKw==/air.tv.douyu.android-X5byqtu7mIckm_IlvyLGwQ==/lib/arm/libc++_shared.so /data/app/~~ZZaMW4uMrb1D6k-GJZdTKw==/air.tv.douyu.android-X5byqtu7mIckm_IlvyLGwQ==/lib/arm/libfbjni.so
//堆内存
Heap: 24% free, 9193KB/11MB; 164761 objects
Dumping cumulative Gc timings
//GC信息
Total time spent in GC: 652.207ms
Mean GC size throughput: 57MB/s per cpu-time: 115MB/s
Mean GC object throughput: 817957 objects/s
Total number of allocations 698238
Total bytes allocated 46MB
Total bytes freed 37MB
Free memory 2957KB
Free memory until GC 2957KB
Free memory until OOME 503MB
Total memory 11MB
Max memory 512MB
Zygote space size 3432KB
Total mutator paused time: 4.868ms
Total time waiting for GC to complete: 21.511us
Total GC count: 5
Total GC time: 652.207ms
Total blocking GC count: 0
Total blocking GC time: 0
Histogram of GC count per 10000 ms: 0:3,1:1,3:1
Histogram of blocking GC count per 10000 ms: 0:5
Native bytes total: 61657456 registered: 97168
 
//dump挂住线程的时间
suspend all histogram:  Sum: 4.715ms 99% C.I. 2us-3971.839us Avg: 261.944us Max: 4242us
 
//主要关注主线程
"main" prio=5 tid=1 Native  //线程名:main,线程优先级:5,tid:1,线程状态:处于Native
  | group="main" sCount=1 dsCount=0 flags=1 obj=0x72949d48 self=0xef3eee00  //属于main进程组
  | sysTid=26182 nice=0 cgrp=foreground sched=0/0 handle=0xef968470  //线程号:sysTid=26182,linux进程优先级:nice=0(linux系统无论是进程和线程都由task_struct结构体管理,越小优先级越高)
       //linux进程状态:state=D(可参考https://blog.csdn.net/sdkdlwk/article/details/65938204),2255555772的单位为10纳秒,
            //utm表示此堆栈在用户态占用cpu时间为17.3毫秒,stm表示此堆栈在内核态占用cpu时间为5.2毫秒。其中:2255555772纳秒*10 = 17.3毫秒+5.2毫秒。
  | state=D schedstat=( 2255555772 521739232 2407 ) utm=173 stm=52 core=6 HZ=100
  | stack=0xff4cf000-0xff4d1000 stackSize=8192KB
  | held mutexes=
  native: #00 pc 0009b304  /apex/com.android.runtime/lib/bionic/libc.so (fdatasync+12)
  native: #01 pc 00027af3  /system/lib/libsqlite.so (unixSync+18)
  native: #02 pc 0002c361  /system/lib/libsqlite.so (syncJournal+324)
  native: #03 pc 000186c5  /system/lib/libsqlite.so (sqlite3PagerCommitPhaseOne+420)
  native: #04 pc 00018d37  /system/lib/libsqlite.so (sqlite3BtreeCommitPhaseOne+90)
  native: #05 pc 00036a19  /system/lib/libsqlite.so (sqlite3VdbeHalt+2820)
  native: #06 pc 0004305f  /system/lib/libsqlite.so (sqlite3VdbeExec+39054)
  native: #07 pc 0001aa9d  /system/lib/libsqlite.so (sqlite3_step+516)
  native: #08 pc 0009c19d  /system/lib/libandroid_runtime.so (android::nativeExecute(_JNIEnv*, _jclass*, long long, long long)+8)
  at android.database.sqlite.SQLiteConnection.nativeExecute(Native method)
  at android.database.sqlite.SQLiteConnection.execute(SQLiteConnection.java:707)
  at android.database.sqlite.SQLiteConnection.setLocaleFromConfiguration(SQLiteConnection.java:460)
  at android.database.sqlite.SQLiteConnection.open(SQLiteConnection.java:261)
  at android.database.sqlite.SQLiteConnection.open(SQLiteConnection.java:205)
  at android.database.sqlite.SQLiteConnectionPool.openConnectionLocked(SQLiteConnectionPool.java:505)
  at android.database.sqlite.SQLiteConnectionPool.open(SQLiteConnectionPool.java:206)
  at android.database.sqlite.SQLiteConnectionPool.open(SQLiteConnectionPool.java:198)
  at android.database.sqlite.SQLiteDatabase.openInner(SQLiteDatabase.java:918)
  - locked <0x0c64a635> (a java.lang.Object)
  at android.database.sqlite.SQLiteDatabase.open(SQLiteDatabase.java:898)
  at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:762)
  at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:751)
  at android.database.sqlite.SQLiteOpenHelper.getDatabaseLocked(SQLiteOpenHelper.java:373)
  at android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:316)
  - locked <0x037d1aca> (a kcsdkint.fc$1)
  at kcsdkint.fc.c(unavailable:-1)
  at kcsdkint.fc.a(unavailable:-1)
  - locked <0x0caa243b> (a java.lang.Object)
  at kcsdkint.fd.e(unavailable:-1)
  at kcsdkint.fd.c(unavailable:-1)
  at kcsdkint.fd.<init>(unavailable:-1)
  at kcsdkint.fd.a(unavailable:-1)
  - locked <0x0c1d9758> (a java.lang.Class<kcsdkint.fd>)
  - locked <0x0c1d9758> (a java.lang.Class<kcsdkint.fd>)
  at kcsdkint.ep.<init>(unavailable:-1)
  at kcsdkint.ep.b(unavailable:-1)
  - locked <0x02c670b1> (a java.lang.Class<kcsdkint.ep>)
  - locked <0x02c670b1> (a java.lang.Class<kcsdkint.ep>)
  - locked <0x02c670b1> (a java.lang.Class<kcsdkint.ep>)
  at kcsdkint.if$17.a(unavailable:-1)
  at kcsdkint.dn.b(unavailable:-1)
  - locked <0x0eba1996> (a kcsdkint.if$17)
  at kcsdkint.dm.a(unavailable:-1)
  at kcsdkint.if.b(unavailable:-1)
  - locked <0x090cc917> (a kcsdkint.if)
  at tmsdk.common.KcBaseService.onBind(unavailable:-1)
  at android.app.ActivityThread.handleBindService(ActivityThread.java:4308)
  at android.app.ActivityThread.access$1800(ActivityThread.java:257)
  at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1997)
  at android.os.Handler.dispatchMessage(Handler.java:106)
  at android.os.Looper.loop(Looper.java:233)
  at android.app.ActivityThread.main(ActivityThread.java:8052)
  at java.lang.reflect.Method.invoke(Native method)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:656)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:967)

从该日志中,我们可以确认以及需要的信息为:

  1. 发生无响应进程的包名,pid与trace文件创建时间(trace文件创建时间直接看trace文件名可以精确到毫秒anr_2021-04-21-1-09-53-099):pid 26182 at 2021-04-21 09:53:11、Cmd line: air.tv.douyu.android
  2. 应用的classloader、gc、heap、dumptrace耗时等信息
  3. 应用主线程状态与堆栈,此部分在注释里说明。

具体也可以查看袁辉辉的此文档:http://gityuan.com/2016/11/26/art-trace/。

在此堆栈中可以看出应用主线程卡在了打开数据库链接的逻辑中,如果可以证明应用发生无响应时确实是此堆栈的耗时卡住了主线程,那么trace有效。如果trace有效,那么调用SQLite的函数在应用代码,基本可以认为是应用问题。如果想要进一步确认,可以反编译应用代码,查找URI来确认打开哪个数据库链接。

那么下一步就是在日志中寻找此堆栈的耗时日志,如下:

04-21 09:53:16.188 10300 26182 26182 W Looper : PerfMonitor longMsg : seq=464 plan=09:52:59.079 late=126ms wall=16982ms running=23ms runnable=13ms io=3ms h=android.app.ActivityThread$H w=121 procState=-1
 
04-21 09:53:16.191 10300 26182 26182 I Choreographer: Skipped 1018 frames! The application may be doing too much work on its main thread.

以上日志可见,在09:53:16.188秒时,此消息执行完毕,卡住了主线程16.9秒,往前推16.9秒,时间大致为09:52:59。也就是说在09:52:59时,这个消息已经卡在这里了。再看trace的dump时间为09:53:11,简单的数学计算就可以说明确实是此消息导致的无响应。

那么,这个耗时的消息是否就是trace中dump下来的堆栈呢?

通过h=android.app.ActivityThread H w = 121 知 道 此 消 息 运 行 到 A c t i v i t y T h r e a d H w=121知道此消息运行到ActivityThread Hw=121ActivityThreadH中w=121的方法,查询源码可知,这是在执行BIND_SERVICE操作。再看堆栈,“at android.app.ActivityThread.handleBindService(ActivityThread.java:4308)”可以证明堆栈中的卡顿确实是导致无响应的原因(至少在这一行调用确实卡了16.9秒,再往上的堆栈可能会不准,仔细想想你懂得)。

这里再提一下,算是经验吧。此消息running在某个cpu上的时间为running=23ms,在堆栈中也可以印证,“state=D schedstat=( 2255555772 521739232 2407 ) utm=173 stm=52”这里显示此堆栈占用cpu时间为22.5ms,这个方式其实也可以从侧面显示trace中的整个堆栈确实就是此耗时消息的耗时堆栈,可能除了我以外很少有人会这么想问题。

到此,基本上可以确认此问题是应用自身问题导致了。

三、无响应问题攻克套路

此部分涉及到小米本身rom的一些性能打点以及特性。唯一能说的就是分析systrace了,该经验自己总结啦!

你可能感兴趣的:(安卓系统框架学习,android)