ANR的排查方法

谷歌的文档ANR 已经解释的比较清楚,这篇文章一半是把我知道的信息整理出来,另外一半是总结我处理ANR的一些经验。:)

出现场景:

出现以下任何情况时,系统都会针对您的应用触发 ANR:

  1. 当您的 Activity 位于前台时,您的应用在 5 秒钟内未响应输入事件或 BroadcastReceiver(如按键或屏幕轻触事件)。
  2. 虽然前台没有 Activity,但您的 BroadcastReceiver 用了相当长的时间仍未执行完毕。

优化的标准:

当应用出现以下情况时,Android Vitals 会认为 ANR 次数超出了正常范围:
至少有 0.47% 的每日工作时段出现了至少一次 ANR。
至少有 0.24% 的每日工作时段出现了至少两次 ANR。

我遇到的ANR的场景:

场景一:出现问题手机:一加8T android 11

  1. 打开应用,执行清理进程
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());
}
  1. 再次打开应用,等10s,10s内滑动一下屏幕,会出现ANR

分析:

Process.killProcess 其实就是发送一个SIGNAL_KILL的信号给进程。
Processandroid_util_Processkill

/**
* 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);
    }
}
  1. 看系统代码,就是调用系统api去kill而已,没什么特别。步骤2之后,也确实看到进程号变了。
  2. 我在我的pixel(android11)手机上,如此调用是不会触发anr的。
  3. 猜测(我确实没有证据)是一加系统改了什么,如果被杀的进程有Activity未关闭,会延迟10秒左右,再恢复一次原来进程的数据,这时候的恢复会导致ANR。

场景二:

  1. 某游戏接入我做的SDK,SDK打开支付页面时出现ANR,堆栈发生在Fragment.startActivityForResult

场景三:

线上一吨没办法复现的ANR堆栈。一吨nativePollOnce的堆栈。

分析:

  1. 线上我们抓去anr的方式有点问题
  2. 出现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。参考文章

玩~

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值