谷歌的文档ANR 已经解释的比较清楚,这篇文章一半是把我知道的信息整理出来,另外一半是总结我处理ANR的一些经验。:)
出现场景:
出现以下任何情况时,系统都会针对您的应用触发 ANR:
- 当您的 Activity 位于前台时,您的应用在 5 秒钟内未响应输入事件或 BroadcastReceiver(如按键或屏幕轻触事件)。
- 虽然前台没有 Activity,但您的 BroadcastReceiver 用了相当长的时间仍未执行完毕。
优化的标准:
当应用出现以下情况时,Android Vitals 会认为 ANR 次数超出了正常范围:
至少有 0.47% 的每日工作时段出现了至少一次 ANR。
至少有 0.24% 的每日工作时段出现了至少两次 ANR。
我遇到的ANR的场景:
场景一:出现问题手机:一加8T android 11
- 打开应用,执行清理进程
public static void killAllProcess() {
ActivityManager am = (ActivityManager) CGApp.INSTANCE.getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningAppProcessInfo> runningApps = am.getRunningAppProcesses();
if (runningApps != null && !runningApps.isEmpty()) {
for (ActivityManager.RunningAppProcessInfo processInfo : runningApps) {
if (processInfo.pid != Process.myPid()) {
Process.killProcess(processInfo.pid);
}
}
killCurProcess();
}
}
public static void killCurProcess() {
Process.killProcess(Process.myPid());
}
- 再次打开应用,等10s,10s内滑动一下屏幕,会出现ANR
分析:
Process.killProcess 其实就是发送一个SIGNAL_KILL的信号给进程。
Process、android_util_Process、kill
/**
* Kill the process with the given PID.
* Note that, though this API allows us to request to
* kill any process based on its PID, the kernel will
* still impose standard restrictions on which PIDs you
* are actually able to kill. Typically this means only
* the process running the caller's packages/application
* and any additional processes created by that app; packages
* sharing a common UID will also be able to kill each
* other's processes.
*/
public static final void killProcess(int pid) {
sendSignal(pid, SIGNAL_KILL);
}
void android_os_Process_sendSignal(JNIEnv* env, jobject clazz, jint pid, jint sig)
{
if (pid > 0) {
LOGI("Sending signal. PID: %d SIG: %d", pid, sig);
kill(pid, sig);
}
}
- 看系统代码,就是调用系统api去kill而已,没什么特别。步骤2之后,也确实看到进程号变了。
- 我在我的pixel(android11)手机上,如此调用是不会触发anr的。
- 猜测(我确实没有证据)是一加系统改了什么,如果被杀的进程有Activity未关闭,会延迟10秒左右,再恢复一次原来进程的数据,这时候的恢复会导致ANR。
场景二:
- 某游戏接入我做的SDK,SDK打开支付页面时出现ANR,堆栈发生在Fragment.startActivityForResult
场景三:
线上一吨没办法复现的ANR堆栈。一吨nativePollOnce的堆栈。
分析:
- 线上我们抓去anr的方式有点问题
- 出现nativePollOnce应该视为没有ANR,或者说和应用的代码没直接关系。
开发过程中的自查方法:
事后处理:出现ANR怎么办?
出现问题的手机连接电脑,在电脑上执行如下指令:
adb bugreport 20210523.zip
收集上来报告之后,找到对应时间的ANR报告,mainThread中可以看到阻塞的代码点。
预防:严格模式
使用 StrictMode 有助于您在开发应用时发现主线程上的意外 I/O 操作。您可以在应用级别或 Activity 级别使用 StrictMode。
预防:启用后台 ANR 对话框
只有在设备的开发者选项中启用了显示所有 ANR 时,Android 才会针对花费过长时间处理广播消息的应用显示 ANR 对话框。因此,系统并不会始终向用户显示后台 ANR 对话框,但应用仍可能会遇到性能问题。
预防:简单的监控主线程执行的任务
// MainLooperWatcher().mainThreadWatcher()
// watch all runnable which post in main thread
class MainLooperWatcher : Printer {
private val TAG = "MainLooperWatcher"
private var mLastMillis: Long = 0
private var mLastSeconds: Long = 0
private var mTimesPerSeconds: Long = 0
fun mainThreadWatcher() {
Looper.getMainLooper().setMessageLogging(this)
}
override fun println(msg: String?) {
// ignore system task
if (msg == null
|| TextUtils.isEmpty(msg)
|| msg.contains("Choreographer")
|| msg.contains("ActivityThread\$H")
) return
if (BuildConfig.IS_DEV) {
Lg.v(TAG, "looper msg: $msg")
}
if (msg.startsWith(">>>>>")) {
mLastMillis = SystemClock.elapsedRealtime()
mTimesPerSeconds++
if ( mTimesPerSeconds > 100) {
report(msg, 0, mTimesPerSeconds)
}
if (mLastMillis - mLastSeconds > 1000) {
mLastSeconds = mLastMillis
mTimesPerSeconds = 0
}
} else {// <<<<< Finished
val now = SystemClock.elapsedRealtime()
val usedMillis = now - mLastMillis
if (usedMillis < 16L) return
if (usedMillis > 48) {
Lg.w(TAG, "$usedMillis ms used for $msg, $mLastSeconds")
} else {
Lg.i(TAG, "$usedMillis ms used for $msg, $mLastSeconds")
}
if (usedMillis > 500L) {
report(msg, usedMillis, mTimesPerSeconds)
mTimesPerSeconds = 0
}
}
}
private fun report(msg: String, usedMillis: Long, callsInSecond: Long) {
val sb = StringBuilder()
Looper.getMainLooper().dump(StringBuilderPrinter(sb), "")
Lg.e(TAG, "$usedMillis ms used for $msg, $mLastSeconds", sb)
val exp = "[Block Runnable] ${usedMillis}ms used for ${msg}, $callsInSecond calls in one seconds"
Lg.e(TAG, exp)
if (Debug.isDebuggerConnected() || Debug.waitingForDebugger()) return
if (BuildConfig.DEBUG && usedMillis > 4000) {
throw RuntimeException(exp)
} else {
Toast.makeText(CGApp.getApplicationContext(), exp, Toast.LENGTH_LONG).show()
}
}
}
测试过程中,上面的1、2不可能每个测试同学都帮忙开启,所以有效的提醒是很有用的。
我扩展了Looper.getMainLooper().setMessageLogging
的方法,把在主线执行超过一定时间的任务打印出来。提前发现问题。
捕获上报#1
开一个异步线程序,每隔5s向主线程post一个任务,5s时间到,在异步线程中检测任务是否被执行,如果没有被执行,就上报奔溃信息(Thread.getAllStackTraces())。
捕获上报#2
系统的 system_server 进程在检测到 App 出现 ANR 后,会向出现 ANR 的进程发送 SIGQUIT (signal 3) 信号。正常情况下,系统的 libart.so 会收到该信号,并调用 Java 虚拟机的 dump 方法生成 traces。参考文章
玩~