@TryLoveCatch
2022-05-18T11:40:44.000000Z
字数 15313
阅读 1848
Android知识体系
ANR(Application Not Responding),应用程序无响应,会严重影响用户体验。ANR的直观体验是用户在操作APP的过程中,感觉界面卡顿,如果 Android应用的界面线程处于阻塞状态的时间过长,会触发“应用无响应”(ANR) 错误,如下图所示,ANR 对话框会为用户提供强行退出应用的选项
首先,我们再来回顾一下ANR的几种常见的类型,如下所示:
Service 与 Bradcast 只会打印 trace 信息,不会提示用户 ANR 弹窗,大部分可感知的 ANR 都是由于 InputEvent。
老师给我布置了个作业,要求我10分钟内完成,他说10分钟后再来检查。
但是,这里有个问题,假如我5分钟就写完了作业,是不是可以主动去告诉老师,而不是让他再多等5分钟呢?
当然可以!
这样就可以提前结束本次等待过程,大大节省时间从而提高效率。
上述过程就简单的模拟了ANR的实现原理,更术语的说法如下。
ANR的检测逻辑有两个参与者:观测者A和被观测者B。
当然,A和B这两者是不在同一个线程中的。
由于A和B是在不同线程中的,所以B即使死循环,也不会影响C的检测过程。
如果是同一个线程,B如果陷入死循环,那么C永远都执行不到了,还检测个毛。
所以,我们可以将ANR更精炼的总结为: 埋雷、拆雷和爆雷。
InputEvennt不同于其他,它的 ANR 触发需要下一次输入,也很容易理解,上一次事件在处理中,虽然可能超时,但是下一次事件还没到来,如果下一次输入事件到来,则检测上一次事件是否超时。
从log中找到ANR反生的信息:可以从log中搜索“ANR in”或“am_anr”,会找到ANR发生的log,该行会包含了ANR的时间、进程、是何种ANR等信息,如果是BroadcastReceiver的ANR可以怀疑BroadCastReceiver.onRecieve()的问题,如果的Service或Provider就怀疑是否其onCreate()的问题。
在该条log之后会有CPU usage的信息,表明了CPU在ANR前后的用量(log会表明截取ANR的时间),从各种CPU Usage信息中大概可以分析如下几点:
除了上述的情况(1)以外,分析CPU usage之后,确定问题需要我们进一步分析trace文件。trace文件记录了发生ANR前后该进程的各个线程的stack。对我们分析ANR问题最有价值的就是其中主线程的stack,一般主线程的trace可能有如下几种情况:
"main" prio=5 tid=1 Native
| group="main" sCount=1 dsCount=0 obj=0x757855c8 self=0xb4d76500
| sysTid=3276 nice=0 cgrp=default sched=0/0 handle=0xb6ff5b34
| state=S schedstat=( 50540218363 186568972172 209049 ) utm=3290 stm=1764 core=3 HZ=100
| stack=0xbe307000-0xbe309000 stackSize=8MB
| held mutexes=
kernel: (couldn't read /proc/self/task/3276/stack)
native: #00 pc 0004099c /system/lib/libc.so (__epoll_pwait+20)
native: #01 pc 00019f63 /system/lib/libc.so (epoll_pwait+26)
native: #02 pc 00019f71 /system/lib/libc.so (epoll_wait+6)
native: #03 pc 00012ce7 /system/lib/libutils.so (_ZN7android6Looper9pollInnerEi+102)
native: #04 pc 00012f63 /system/lib/libutils.so (_ZN7android6Looper8pollOnceEiPiS1_PPv+130)
native: #05 pc 00086abd /system/lib/libandroid_runtime.so (_ZN7android18NativeMessageQueue8pollOnceEP7_JNIEnvP8_jobjecti+22)
native: #06 pc 0000055d /data/dalvik-cache/arm/system@framework@boot.oat (Java_android_os_MessageQueue_nativePollOnce__JI+96)
at android.os.MessageQueue.nativePollOnce(Native method)
at android.os.MessageQueue.next(MessageQueue.java:323)
at android.os.Looper.loop(Looper.java:138)
at android.app.ActivityThread.main(ActivityThread.java:5528)
at java.lang.reflect.Method.invoke!(Native method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:740)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:630)
当然这种情况很有可能是由于该进程的其他线程消耗掉了CPU资源,这就需要分析其他线程的trace以及ANR前后该进程自己输出的log了。
adb bugreport /Users/xxx/
ANR reason主要有以下几类:
BlockCanary是国内开发者 markzhai 开发的一款非侵入式的轻量性能监控组件,在 Github 上有接近 4000 star,可以检测主线程上的各种卡顿问题,并提供各种堆栈信息。
BlockCanary — 轻松找出Android App界面卡顿元凶
利用了Android 原生 Looper.loop 中的一个 log 打印逻辑。
主要代码如下:
public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
final MessageQueue queue = me.mQueue;
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
// This must be in a local variable, in case a UI event sets the logger
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
try {
msg.target.dispatchMessage(msg);
} finally {
}
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
}
}
关键是这两句:
logging.println(">>>>> Dispatching to " + msg.target + " " + msg.callback + ": " + msg.what);
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
通过Looper提供的接口设置logging:
Looper.getMainLooper().setMessageLogging(mainLooperPrinter);
在mainLooperPrinter中判断start和end,来获取主线程dispatch该message的开始和结束时间,并判定该时间超过阈值(如2000毫秒)为主线程卡慢发生,并dump出各种信息,提供开发者分析性能瓶颈。
@Override
public void println(String x) {
if (!mStartedPrinting) {
mStartTimeMillis = System.currentTimeMillis();
mStartThreadTimeMillis = SystemClock.currentThreadTimeMillis();
mStartedPrinting = true;
} else {
final long endTime = System.currentTimeMillis();
mStartedPrinting = false;
if (isBlock(endTime)) {
notifyBlockEvent(endTime);
}
}
}
private boolean isBlock(long endTime) {
return endTime - mStartTimeMillis > mBlockThresholdMillis;
}
https://developer.aliyun.com/article/782573
在 print 方法中时,取消上一次异步线程延迟任务,重新触发一次延迟任务)。若 UI Looper 任务发生卡顿,则延迟任务被执行,且在卡顿期间的获取主线程堆栈信息,之后在下一次 print 方法被执行的时候,若确认发生卡顿,则可把主线程堆栈信息当做卡顿堆栈记录上报。
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
在线上环境,大部分场景下并不会发生卡顿,但卡顿检测 SDK 会一直执行。可以发现 app 每一帧时间(16.6ms),UI Looper 中的任务会执行多次,最终产生大量的无效字符串拼接操作和大量小对象碎片。
使用 Android 帧回调代替 Looper Task 方案,同时满足以上 2 个条件:
此外,还有一个优点:
为避免 BlockCanary 方案延迟任务触发和取消的频率过高的问题,仅在帧回调处记录时间戳,不再取消延迟任务,但在延迟任务执行时判断是否发生卡顿,同时记录主线程堆栈。
假设 500ms 以上为卡顿,整体方案流程图如下:
定义 5s 以上卡顿为 ANR,为检测 ANR,同样通过 500ms 卡顿检测,并做卡顿堆栈聚合,当连续发生卡顿大于 5s 且堆栈信息不变,则认为发生 ANR。发生 ANR 时,记录当前设备 CPU 负载等信息。
https://juejin.cn/post/6844904066259091469
ANR-WatchDog 是参考 Android WatchDog 机制(com.android.server.WatchDog.java)起个单独线程向主线程发送一个变量 +1 操作,自我休眠自定义 ANR 的阈值,休眠过后判断变量是否 +1 完成,如果未完成则告警。
SafeLooper是个比较新奇的思路,本身就是一个堵塞的消息,在自己内部进行消息的处理,通过反射接管主线程Looper的功能。
也可以参考这里。
当 ANR 发生的时候,我们是可以通过监听/data/anr 文件夹的写入情况来判断是否发生了 ANR,看起来这是一个不错的时机。需要注意的是,所有应用发生 ANR 的时候都会进行回调,因此需要做一些过滤与判断,如包名、进程号等。
最大的困难是兼容性问题,这个方案受限于Android系统的SELinux机制,5.0以后基本已经使低权限应用无法监听到Trace文件,但是可以在开发内测阶段通过root手机修改app对应的te文件提权进行监控。
https://droidyue.com/blog/2021/03/07/view-android-anr-stacktrace/
比如网络访问、访问数据库、文件读写、频繁的大量的计算赋值逻辑,这些都是常见的ANR原因,具体根据ANR的Trace文件调用堆栈信息可以直接看出来,这边不再赘述。
以上的ANR一般使用异步的方式解决,当然不是简单的new一个线程,最好根据业务场景以及频率来决定,Android常用的异步操作有AsyncTask,IntentService,线程池(官方四种或自定义),最好用一个线程池管理操作线程,不建议每次直接new一个线程。相关阅读见Android 子线程更新UI详解。
比如下面这份ANR日志信息,CPU数值异常偏高到186%,导致CPU过高的原因可能是频繁地IO读写,频繁调用耗时JNI接口。
ActivityManager: ANR in com.autonavi.amapauto (com.autonavi.amapauto/.MainMapActivity)
ActivityManager: PID: 7321
ActivityManager: Reason: Input dispatching timed out (Waiting to send non-key event because the touched window has not finished processing certain input events that were delivered to it over 500.0ms ago. Wait queue length: 5. Wait queue head age: 5765.8ms.)
ActivityManager: Load: 16.72 / 11.58 / 8.91
ActivityManager: CPU usage from 0ms to 13484ms later:
ActivityManager: 186% 7321/com.autonavi.amapauto: 105% user + 80% kernel / faults: 2474 minor 3 major
ActivityManager: 49% 620/system_server: 22% user + 26% kernel / faults: 2787 minor
ActivityManager: 30% 1172/com.tencent.wecarspeech: 23% user + 7.2% kernel / faults: 3955 minor 2 major
ActivityManager: 11% 1536/com.android.bluetooth: 7.1% user + 4.2% kernel / faults: 2405 minor 2 major
ActivityManager: 4.7% 215/logd: 4.2% user + 0.4% kernel / faults: 13 minor
ActivityManager: 4% 239/debuggerd: 0.6% user + 3.4% kernel / faults: 4584 minor 1 major
ActivityManager: 6.8% 1296/com.android.phone: 4.1% user + 2.6% kernel / faults: 1230 minor
ActivityManager: 6.4% 925/com.nforetek.bt: 2.6% user + 3.7% kernel / faults: 1097 minor 1 major
ActivityManager: 5.8% 7537/com.tencent.wecarnavi:wecarbase: 2.9% user + 2.8% kernel / faults: 1150 minor
ActivityManager: 5.7% 7509/com.tencent.wecarnavi: 3% user + 2.6% kernel / faults: 951 minor
ActivityManager: 4.6% 866/sdcard: 0% user + 4.5% kernel / faults: 1 minor
ActivityManager: 4.3% 242/mediaserver: 3.2% user + 1.1% kernel
ActivityManager: 3.7% 858/com.android.launcher: 2.7% user + 0.9% kernel / faults: 741 minor 1 major
ActivityManager: 3.3% 792/gaei.cluster.service: 1.9% user + 1.4% kernel / faults: 908 minor
ActivityManager: 2.8% 1125/com.tencent.wecarnews: 1.4% user + 1.4% kernel / faults: 477 minor
ActivityManager: 2.7% 881/com.android.systemui: 1.5% user + 1.1% kernel / faults: 963 minor 2 major
ActivityManager: 2.6% 8825/top: 0.8% user + 1.8% kernel
ActivityManager: 2.3% 798/com.gaei.bt: 0.9% user + 1.4% kernel / faults: 846 minor
ActivityManager: 2.1% 1101/com.tencent.wecarmusicp: 0.7% user + 1.4% kernel / faults: 42 minor
ActivityManager: 2.1% 7744/com.autonavi.amapauto:push: 1.1% user + 1% kernel / faults: 30916 minor
ActivityManager: 1.1% 1269/com.gaei.settings: 0.5% user + 0.5% kernel / faults: 955 minor
ActivityManager: 1.5% 110/mmcqd/3: 0% user + 1.5% kernel
ActivityManager: 0.8% 833/gaei.reverse: 0.4% user + 0.3% kernel / faults: 826 minor
ActivityManager: 0.7% 5949/logcat: 0.4% user + 0.3% kernel
ActivityManager: 1.2% 803/gaei.thirdparty.media.adapter: 0.7% user + 0.5% kernel / faults: 654 minor
ActivityManager: 0.6% 998/gaei.cluster: 0.3% user + 0.3% kernel / faults: 759 minor
ActivityManager: 0.6% 1279/com.gaei.gaeihvsmsettings: 0.3% user + 0.3% kernel / faults: 868 minor
ActivityManager: 1.1% 233/surfaceflinger: 0.5% user + 0.5% kernel
ActivityManager: 0.5% 838/gaei.lockscreen: 0.3% user + 0.2% kernel / faults: 750 minor
ActivityManager: 0.5% 964/gaei.ecallbcall: 0.2% user + 0.2% kernel / faults: 719 minor
ActivityManager: 0.5% 1256/com.thundersoft.update: 0.3% user + 0.1% kernel / faults: 745 minor
ActivityManager: 0.5% 1274/gaei.bluetooth: 0.2% user + 0.2% kernel / faults: 732 minor
ActivityManager: 0.5% 1285/com.gaei.vehichesetting: 0.3% user + 0.1% kernel / faults: 786 minor
ActivityManager: 0.5% 8916/kworker/0:2: 0% user + 0.5% kernel
ActivityManager: 0.5% 2439/cn.gaei.appstore: 0.4% user + 0% kernel / faults: 60 minor
ActivityManager: 0.3% 232/servicemanager: 0.1% user + 0.2% kernel
ActivityManager: 0.2% 252/rild: 0% user + 0.2% kernel
ActivityManager: 0.2% 2131/com.thundersoft.connectivity: 0.2% user + 0% kernel / faults: 133 minor
ActivityManager: 0.2% 2153/com.excelfore.hmiagent: 0% user + 0.2% kernel / faults: 1779 minor
ActivityManager: 0.1% 7383/com.autonavi.amapauto:locationservice: 0.1% user + 0% kernel / faults: 266 minor
ActivityManager: 0.2% 7/rcu_preempt: 0% user + 0.2% kernel
ActivityManager: 0% 7360/com.autonavi.amapauto:adiu: 0% user + 0% kernel / faults: 223 minor
ActivityManager: 0.1% 8892/kworker/u8:0: 0% user + 0.1% kernel
ActivityManager: 0% 1//init: 0% user + 0% kernel
ActivityManager: 0% 3/ksoftirqd/0: 0% user + 0% kernel
ActivityManager: 0% 107/debounce_task: 0% user + 0% kernel
ActivityManager: 0% 108/irq/43-mm-irq-t: 0% user + 0% kernel
ActivityManager: 0% 210/jbd2/mmcblk3p4-: 0% user + 0% kernel
ActivityManager: 0% 237/netd: 0% user + 0% kernel / faults: 15 minor
ActivityManager: 0% 244/smcd: 0% user + 0% kernel
ActivityManager: 0% 248/system_setting_service: 0% user + 0% kernel
ActivityManager: 0% 1087/com.trumpchi.assistant.app: 0% user + 0% kernel / faults: 2 minor
ActivityManager: 0% 2009/com.thunderst.update: 0% user + 0% kernel / faults: 17 minor
ActivityManager: 93% TOTAL: 51% user + 41% kernel + 0.1% iowait + 0.3% softirq
ActivityManager: CPU usage from 11778ms to 12377ms later:
ActivityManager: 46% 620/system_server: 23% user + 23% kernel
ActivityManager: rename tracefile/data/anr/traces.txt to /data/anr/traces_com.autonavi.amapauto_20190906_110548.txt
在CPU异常偏高的情况下,系统记录的ANR Trace文件里面不会有ANR应用的调用逻辑堆栈信息,比如只有下面的“main”信息。
"main" prio=5 tid=1 Native
| group="main" sCount=1 dsCount=0 obj=0x74030258 self=0xb4df6500
| sysTid=1336 nice=0 cgrp=default sched=0/0 handle=0xb6f90b34
| state=S schedstat=( 14858997090 10346952287 41860 ) utm=903 stm=582 core=1 HZ=100
| stack=0xbe7b2000-0xbe7b4000 stackSize=8MB
| held mutexes=
kernel: (couldn't read /proc/self/task/1336/stack)
native: #00 pc 00040d04 /system/lib/libc.so (__epoll_pwait+20)
native: #01 pc 0001a17f /system/lib/libc.so (epoll_pwait+26)
native: #02 pc 0001a18d /system/lib/libc.so (epoll_wait+6)
native: #03 pc 00012cfb /system/lib/libutils.so (android::Looper::pollInner(int)+102)
native: #04 pc 00012f77 /system/lib/libutils.so (android::Looper::pollOnce(int, int*, int*, void**)+130)
native: #05 pc 000807ad /system/lib/libandroid_runtime.so (android::NativeMessageQueue::pollOnce(_JNIEnv*, _jobject*, int)+22)
native: #06 pc 00008ebd /data/dalvik-cache/arm/system@framework@boot.oat (Java_android_os_MessageQueue_nativePollOnce__JI+96)
at android.os.MessageQueue.nativePollOnce(Native method)
at android.os.MessageQueue.next(MessageQueue.java:323)
at android.os.Looper.loop(Looper.java:135)
at android.app.ActivityThread.main(ActivityThread.java:5487)
at java.lang.reflect.Method.invoke!(Native method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
这个时候应该根据ANR的时间点分析应用的日志文件,往前10s看应用调用逻辑信息,分析下哪部分逻辑模块的日志输出过于频繁。但有一个例外情况,如果问题出在应用日志输出写文件过于频繁,则需要精简下日志,去除不必要的日志信息。过于频繁的日志输出占用IO带宽,竞争CPU资源,特别是在设备IO带宽偏低的情况下,很容易影响到。
如果是耗时JNI接口频繁调用导致的ANR,可根据场景规避减少JNI接口的调用,遇到过性能差的设备即使是在JNI层调用setInt赋值一个参数都会耗时50ms的情况。
一般是文件操作导致,比如下面的日志信息:
ANRManager: 100% TOTAL: 2% user + 2.1% kernel + 95% iowait + 0.1% softirq
iowait占比95%,分析ANR Trace文件,通过应用包名过滤调用堆栈信息,或者在ANR时间点往前看10s应用日志,看看当时做什么文件操作,一般也是用异步的方式来解决这个问题。
对于这种问题,一般会尝试将锁改为超时锁,比如lock的trylock,超时会自动释放锁,避免一直持有锁的情况发生。
主线程执行了Binder请求,对端迟迟未返回很容易出现这个问题,一般使用异步的方法解决。
系统对每个进程最多分配15个Binder线程,如果另一个进程发送太多重复Binder请求,那么就会导致接收端Binder线程被占满,从而处理不了其它的Binder请求。
判断Binder是否用完,可以在trace中搜索关键字”binder_f”,如果搜索到则表示已经用完,接着分析日志看是谁一直在消耗Binder或者是有死锁发生。
解决的方法就是降低极短时间内大量Binder请求,比如在发送BInder请求的函数中做时间差过滤,限定在500ms内最多执行一次。
Android ANR的设计原理
Qtest测试之道 Android ANR 监测方案解析
Android性能优化杂谈-如何监控和解决ANR问题?
今日头条 ANR 优化实践系列 - 设计原理及影响因素