引言
系统中触发 anr 一共有三处,前两者跟 ams 有关,touch 事件跟 ims 有关。至于 contentProvider,虽然源码中有杀进程的逻辑,但尝试后发现不会 anr,也不会杀应用进程。后面有时间再细分析吧。
- service 生命周期
- BroadcastReceive#onReceive。后简称 br,要注意
只有静态注册的 br 和有序广播会出现 anr。动态注册的 br 在 sendBroadcast() 时不会因为自己的耗时操作导致 anr - touch 事件
虽然三者处于不同服务,但 anr 处理逻辑基本一致:在调用应用进程之前会通过 handler send 一个定时消息,到点就 anr;应用进程处理完成后会调用 ams 或 ims,然后移除相应的消息,就不会 anr。这个消息的执行时间就是 anr 的时间间隔,如 touch 是 5s、broadcastReceive 前台 10s、后台 60s,Service 20s。
所以 anr 的最核心原因是:应用进程未能及时回调 ams/ims。原来一直以为 anr 的原因是 主线程执行耗时操作,这个说法不太严谨,因为 br 可以指定 onReceive() 运行在子线程,但即使在子线程中如果 onReceive() 执行太久,也会 anr。
原因
上面说过 anr 的最核心原因是:应用进程未能及时回调 ams/ims。细分下其实有两种情况,anr 并不全是应用自己的锅
- 没机会回调。比如 cpu 太忙,没时间执行应用进程。比如某些进程占有太多 cpu,特别是
kswap进程,它占用太多 cpu 时往往意味着内存不足。 - 没时间回调。比如应用进程执行耗时操作、锁出错、应用进程等待第三方应用返回等
总的来说,anr 的原因有很多种,有些是自己的代码,有一些是系统环境、第三方应用。因此,分析时不仅需要看应用代码,还需要拿到当时的系统环境、不同进程的状态等
文件
- trace 文件,在 /data/anr 目录下,可拿到 anr 时各进程状态、内存情况。较新版本会以时间戳为名生成日志文件,但主要内容与旧版本的 traces 差不多。本文中所有 traces 文件都是通过模拟器拿到的,也可通过 adb bugreport 拿到。
- dropbox 文件,新版本中 /data/anr 目录下的 anr 文件已经没有太多内容,可以使用 dropbox 命令导出 dropbox 文件。
- 日志文件,可拿到 anr 前后 cpu 负载情况。android 系统在运行时会输出大量日志,这些日志在分析 anr 时很有用。在日志文件中通过搜索
anr in结合时间可定位到需求的日志。下面是一段 Log 日志
11-24 19:58:39.403 I/ActivityManager(19576): Dumping to /data/anr/anr_2021-11-24-19-58-39-399 // 指向当前 anr 日志
// 中间隔好很多 行
11-24 19:58:40.687 E/ActivityManager(19576): ANR in XXX
11-24 19:58:40.687 E/ActivityManager(19576): PID: 8654
11-24 19:58:40.687 E/ActivityManager(19576): Reason: XXX // anr 原因
11-24 19:58:40.687 E/ActivityManager(19576): Load: 1.24 / 0.48 / 0.28 // cpu 负载
11-24 19:58:40.687 E/ActivityManager(19576): CPU usage from 154384ms to 0ms ago (2021-11-24 19:56:04.565 to 2021-11-24 19:58:38.950): // 各进程 CPU 使用情况
...
11-24 19:58:40.687 E/ActivityManager(19576): 3.7% TOTAL: 1.5% user + 2.1% kernel + 0% iowait + 0% softirq// CPU 使用情况汇总
11-24 19:58:40.687 E/ActivityManager(19576): CPU usage from 27ms to 432ms later (2021-11-24 19:58:38.977 to 2021-11-24 19:58:39.381):// 另一段时间的 CPU 使用情况
11-24 19:58:40.687 E/ActivityManager(19576): 100% 8654/com.sogle.androiddemo: 100% user + 0% kernel // 8654 进程占了 100% CPU
11-24 19:58:40.687 E/ActivityManager(19576): 100% 8681/Thread-2: 100% user + 0% kernel // 8654 进程下的 8681 线程用了 100%
...
11-24 19:58:40.687 E/ActivityManager(19576): 39% TOTAL: 32% user + 7.6% kernel
11-24 19:58:40.691 D/ActivityManager(19576): Completed ANR of XXX in 1741ms, latency 1ms
上面一共有两段 CPU 使用情况,第二段时间更近,理论上具有更高的价值。
bugreport
现在通过 /data/anr 目录下拿的日志,一般情况下是没有多大价值的。可通过 bugreport 导出日志信息。最重要的是 bugreport-BUILD_ID-DATA.txt 文件,里面有 anr 时 dumpsys 的一些信息。
信息的具体分析参考官方文档
- 通过搜索
am_anr和ANR in(注意大小写)定位 anr 的具体位置。其中 ANR in 定位的地方包含有 anr 发生时 CPU 的更多信息 - 搜索
WATCHDOG KILLING SYSTEM PROCESS。这是 WatchDog 在杀死进程前的日志输出,而 WatchDog 用来监控 system_server, 所以搜索之后可以得到关于 anr 的一些信息 am_proc_died和am_proc_start:如果出现频繁,可能是内存不足,系统不断杀死进程又启动另外的进程am_low_memory:向应用程序报告内存不足。
onTrimMemory
内存不足时的系统回调,参数值越大表示内存情况越紧急
- 根据注释说法,
不应该使用相等比较参数,应该使用 >=。if(level == TRIM_MEMORY_UI_HIDDEN){ // 这种写法不好 } if(level >= TRIM_MEMORY_UI_HIDDEN){ // 注释建议这种写法 } - level 一般常用的值有:
TRIM_MEMORY_COMPLETE:内存很低,如果还没有足够多的内存,下一步就该杀掉当前进程,最严厉的级别TRIM_MEMORY_MODERATE:内存很低,但应用进程除于缓存列表的中间位置,如果还得不到足够的内存,就有可能被杀死TRIM_MEMORY_BACKGROUND:内存不够,开始清理缓存进程。一般 Level 值就比它大就应该清除一些缓存TRIM_MEMORY_UI_HIDDEN:表示应用进入后台- 剩余还有一些带 RUNNING 的 level,它表示当前进程是前台进程,但由于内存紧张,也希望能释放一些内存。
onTrimMemory() 的使用可以参考 Glide 实现:
public void trimMemory(int level) {
if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
// Entering list of cached background apps
// Evict our entire bitmap cache
// 退入后台,并且已经开始杀后台进程了,所以释放所有缓存保命
clearMemory();
} else if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN
|| level == android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) {
// The app's UI is no longer visible, or app is in the foreground but system is running
// critically low on memory
// Evict oldest half of our bitmap cache
// 应用切到后台或者在前台但内存很低了,所以将缓存减少一半
trimToSize(getMaxSize() / 2);
}
}
示例
总结
先看线程堆栈。一般死锁都可以直接看出来- 其实看
cpu, 内存。要注意,如果内存不足也可能导致 cpu 过高,主要是看是不是 kswapd0 进程占用了太多资源- 如果内存足够,且主线程未执行太多操作,考虑是否子线程执行了太多操作。可罗列所有子线程,哪一个处于执行状态,哪一个嫌疑最大。参考 链接第 4.4 节
等待第三方应用
主要是应用通过 binder 驱动调用第三方,包括但不限于各种系统服务
参考:ANR日志分析全面解析
这一部分的关键字是 BinderProxy#transactNative。在 binder 驱动中,这是应用由 Java 层进入 native 层的最后一个方法,所以方法栈最后到该方法时,基本上可以确定由第三方应用引起的 anr
在链接中还给出另一个关键字 blockUntilThreadAvailable,这是 WatchDog 中用于检测 binder 线程的 monitor,所以如果出现死锁之类的通过该关键字就可以看出。
链接中有一个例子,另补充一个。关于日志中各参数的分析可参考:traces 日志线程参数解析大全
// 出 anr 进程的主线程
"main" prio=5 tid=1 Native // native 表示当前线程正在执行 native 代码
| group="main" sCount=1 dsCount=0 flags=1 obj=0x710af508 self=0xe0b00010
| sysTid=19759 nice=0 cgrp=foreground sched=0/0 handle=0xef084478
| state=S schedstat=( 1174820541 1447510370 1946 ) utm=51 stm=65 core=0 HZ=100
| stack=0xff565000-0xff567000 stackSize=8192KB
| held mutexes=
native: #00 pc 00000b97 [vdso] (__kernel_vsyscall+7)
native: #01 pc 000cd46c /apex/com.android.runtime/lib/bionic/libc.so (__ioctl+28)
native: #02 pc 00080e6a /apex/com.android.runtime/lib/bionic/libc.so (ioctl+58)
// 到 IpcThreadData,下一步就是通过 ioctl 跟 binder 驱动通信。具体参考 binder 驱动相关代码
native: #03 pc 00050edb /system/lib/libbinder.so (android::IPCThreadState::talkWithDriver(bool)+331)
native: #04 pc 000522af /system/lib/libbinder.so (android::IPCThreadState::waitForResponse(android::Parcel*, int*)+47)
native: #05 pc 00051fb1 /system/lib/libbinder.so (android::IPCThreadState::transact(int, unsigned int, android::Parcel const&, android::Parcel*, unsigned int)+177)
native: #06 pc 00048de9 /system/lib/libbinder.so (android::BpBinder::transact(unsigned int, android::Parcel const&, android::Parcel*, unsigned int)+153)
native: #07 pc 0012302f /system/lib/libandroid_runtime.so (android_os_BinderProxy_transact(_JNIEnv*, _jobject*, int, _jobject*, _jobject*, int)+143)
at android.os.BinderProxy.transactNative(Native method) // 要执行 native 代码
at android.os.BinderProxy.transact(BinderProxy.java:540)
at android.net.wifi.IWifiManager$Stub$Proxy.getCurrentNetwork(IWifiManager.java:3129)
at android.net.wifi.WifiManager.getCurrentNetwork(WifiManager.java:4989)
通过上面堆栈可以看出主线程要执行的是 IWifiManager#getCurrentNetwork(),再全局搜 WifiServiceImpl.getCurrentNetwork。为什么是 WifiServiceImpl?因为从倒数第二行可以看出,这是一个 aidl,系统中 IWifiManager 对应的实现就是 WifiServiceImpl。得到日志如下
// Binder:19576_6 是线程名,其中 19576 是线程所处的进程的进程 id
// Binder 前缀表明它是一个 binder 线程,即用于处理 binder 驱动转交的请求
"Binder:19576_6" prio=5 tid=108 Waiting
| group="main" sCount=1 dsCount=0 flags=1 obj=0x14000270 self=0xb4b5ce10
| sysTid=19897 nice=0 cgrp=foreground sched=0/0 handle=0xb28a91e0
| state=S schedstat=( 40637786 168237145 212 ) utm=0 stm=3 core=1 HZ=100
| stack=0xb27ae000-0xb27b0000 stackSize=1008KB
| held mutexes=
at java.lang.Object.wait(Native method)
- waiting on <0x01f71270> (a java.lang.Object) // 从源码看出,同步锁内部又通过 wait 等待
at java.lang.Object.wait(Object.java:442)
at java.lang.Object.wait(Object.java:568)
at com.android.wifi.x.com.android.internal.util.AsyncChannel$SyncMessenger.sendMessageSynchronously(AsyncChannel.java:848)
- locked <0x01f71270> (a java.lang.Object) // 从源码中看出,这里先 sync 加同步锁
at com.android.wifi.x.com.android.internal.util.AsyncChannel$SyncMessenger.access$100(AsyncChannel.java:762)
at com.android.wifi.x.com.android.internal.util.AsyncChannel.sendMessageSynchronously(AsyncChannel.java:675)
at com.android.server.wifi.util.WifiAsyncChannel.sendMessageSynchronously(WifiAsyncChannel.java:92)
at com.android.wifi.x.com.android.internal.util.AsyncChannel.sendMessageSynchronously(AsyncChannel.java:688)
at com.android.server.wifi.ClientModeImpl.syncGetCurrentNetwork(ClientModeImpl.java:1805)
at com.android.server.wifi.WifiServiceImpl.getCurrentNetwork(WifiServiceImpl.java:3500)
到这里就可以明白应用主线程被阻塞在 WifiManager 上,粗略看一下 WifiManager 中相关逻辑可以发现 WifiManager 在等待另一个跨进程的结果。至于另一个进程到底是谁,不细分析。
WifiManager 属于 systemServer 进程,拿进程 id(可通过线程名分离出来) 去查就可以看出这的确是 SystemServer 进程。