[ANR监控] ANR捕获,这些要点你必须知道

大家都知道,当发生ANR后,App会弹窗提示”应用失去响应,是否重启“,然后系统会dump一份trace文件,存在data/anr目录下。

普通应用如何监控ANR的发生呢?

这个时候,系统肯定是知道ANR发生了,所以像Console和Firebase这些工具都能很好的拿到ANR发生的时间和trace文件的内容。

但是,作为面向普通应用的监控sdk,很多系统应用有的权限都没有,我们怎么才能判断ANR的发生呢?另外高版本的Android系统,限制了普通应用读取trace文件的权限,我们又如何拿到ANR发生时dump出来的trace文件呢?

ANR捕获的基本原理

发生ANR的时候,系统的system_server进程会发送SIGQUIT信号给一些进程,包括发生ANR的进程、占用系统资源较多的进程,让这些进程进行dump操作。

我们可以通过在应用内拦截这个信号,来判断当前进程是否需要dump。但是收到信号并不表示当前进程一定发生了ANR,也可能是其他进程发生了ANR,刚好我们的进程运行在后台,且资源消耗比较多,system_server也发送信号让我们进程去做dump。所以光凭这个逻辑,无法判断是否是当前进程发生的ANR,还需要加一些其他的判断条件。接下来就让我们从Matrix的源码角度,看看业界通用的比较成熟的通用方案吧。

监听SIGQUIT信号

  1. 首先Matrix在AnrDumper方法里,将sigquit设置成SIG_UNBLOCK

因为Android默认把SIGQUIT信号设置成BLOCKED,所以只会响应sigwait,而不会进入我们设置的handler中。所以需要通过pthread_sigmaskSIGQUIT信号设置成UNBLOCK,才能进入我们的handler方法。

AnrDumper::AnrDumper(const char* anrTraceFile, const char* printTraceFile) {
    // must unblock SIGQUIT, otherwise the signal handler can not capture SIGQUIT
    mAnrTraceFile = anrTraceFile;
    mPrintTraceFile = printTraceFile;
    sigset_t sigSet;
    sigemptyset(&sigSet);
    sigaddset(&sigSet, SIGQUIT);
    pthread_sigmask(SIG_UNBLOCK, &sigSet , &old_sigSet);
}
  1. 然后通过sigaction,将方法地址设置成我们的signalHandler
bool SignalHandler::installHandlersLocked() {
    // 构造一个sigaction,将方法地址设置成我们的signalHandler
    struct sigaction sa{};
    sa.sa_sigaction = signalHandler;
    sa.sa_flags = SA_ONSTACK | SA_SIGINFO | SA_RESTART;
    // 设置一个新的sigaction
    if (sigaction(TARGET_SIG, &sa, nullptr) == -1) {
        return false;
    }
    sHandlerInstalled = true;
    return true;
}
  1. 当系统向进程发送SIGQUIT信号时,会进入我们的signalHandler方法,可以在这里可以对信号进行进一步的处理。Matrix是直接交给AnrDumperhandleSignal方法处理。
void SignalHandler::signalHandler(int sig, siginfo_t* info, void* uc) {
    std::unique_lock<std::mutex> lock(sHandlerStackMutex);
    for (auto it = sHandlerStack->rbegin(); it != sHandlerStack->rend(); ++it) {
        (*it)->handleSignal(sig, info, uc);
    }
    lock.unlock();
}
  1. 在处理完之后,将SIGQUIT重新发成系统的SignalCatcher处理。
static void sendSigToSignalCatcher() {
    int tid = getSignalCatcherThreadId();
    syscall(SYS_tgkill, getpid(), tid, SIGQUIT);
}

判断是否真的发生ANR

收到SIGQUIT只能说明进程要处理dump,并不能说明当前应用程序发生了ANR,如果直接上报ANR,会造成多报。所以还需要加更多的判断条件,来判断当前进程是否真正发生ANR。

  1. 当前程序是否有NOT_RESPONDING标记

如果应用发生了ANR,AMS会给进程加一个NOT_RESPONDING的标记,同时生成shortMsglongMsg。如果有这个标记,可以马上判断当前进程发生了一次ANR。

但是用NOT_RESPONDING标记来判断ANR,也有几个缺陷:

  • 生成比较慢,需要轮询去查,如果用户提前杀掉app,可能导致上报失败
  • 无法监控后台ANR,因为后台ANR,除非了【开发者选项】中打开【显示所有“应用程序无响应】,否则App不会进入NOT_RESPONDING状态。

Matrix的做法是开启一个子线程,去循环查是否有NOT_RESPONDING标记,500ms查一次,一共查20s。

new Thread(new Runnable() {
                @Override
                public void run() {
                    checkErrorStateCycle(isSigQuit);
                }
            }, CHECK_ANR_STATE_THREAD_NAME).start();

循环检查进程的Error State

    private static void checkErrorStateCycle(boolean isSigQuit) {
        int checkErrorStateCount = 0;
        while (checkErrorStateCount < CHECK_ERROR_STATE_COUNT) {
            try {
                checkErrorStateCount++;
                boolean myAnr = checkErrorState();
                //  如果有NOT_RESPONDING标记,马上上报
                if (myAnr) {
                    report(true, isSigQuit);
                    break;
                }
                Thread.sleep(CHECK_ERROR_STATE_INTERVAL);
            } catch (Throwable t) {
                MatrixLog.e(TAG, "checkErrorStateCycle error, e : " + t.getMessage());
                break;
            }
        }
    }

单词检查的逻辑,通过getProcessInErrorState获取。

    private static boolean checkErrorState() {
        try {
            Application application =
                    sApplication == null ? Matrix.with().getApplication() : sApplication;
            ActivityManager am = (ActivityManager) application
                    .getSystemService(Context.ACTIVITY_SERVICE);
            List<ActivityManager.ProcessErrorStateInfo> procs = am.getProcessesInErrorState();
            for (ActivityManager.ProcessErrorStateInfo proc : procs) {
                if (proc.uid != android.os.Process.myUid()
                        && proc.condition == ActivityManager.ProcessErrorStateInfo.NOT_RESPONDING) {
                    MatrixLog.i(TAG, "maybe received other apps ANR signal");
                    return false;
                }
                // 当前进程,且状态是NOT_RESPONDING,才返回true
                if (proc.pid != android.os.Process.myPid()) continue;
                if (proc.condition != ActivityManager.ProcessErrorStateInfo.NOT_RESPONDING) {
                    continue;
                }
                return true;
            }
            return false;
        } catch (Throwable t) {
            MatrixLog.e(TAG, "[checkErrorState] error : %s", t.getMessage());
        }
        return false;
    }
  1. 主线程是否被block

通过反射,可以获取主线程Looper中的消息头mMessages。因为MessageQueue中的消息是根据Message.when的时间升序排列的,所以反射获取的消息头就是第一个待处理的消息。

判断主线程是否被block的方法:判断当前时间和when的差值。

  • 前台:如果差值大于2s,算block
  • 后台:差值大于10s,算block
    private static final long FOREGROUND_MSG_THRESHOLD = -2000;
    private static final long BACKGROUND_MSG_THRESHOLD = -10000;

    @RequiresApi(api = Build.VERSION_CODES.M)
    private static boolean isMainThreadBlocked() {
        try {
            MessageQueue mainQueue = Looper.getMainLooper().getQueue();
            Field field = mainQueue.getClass().getDeclaredField("mMessages");
            field.setAccessible(true);
            final Message mMessage = (Message) field.get(mainQueue);
            if (mMessage != null) {
                anrMessageString = mMessage.toString();
                long when = mMessage.getWhen();
                if (when == 0) {
                    return false;
                }
                long time = when - SystemClock.uptimeMillis();
                anrMessageWhen = time;
                long timeThreshold = BACKGROUND_MSG_THRESHOLD;
                if (currentForeground) {
                    timeThreshold = FOREGROUND_MSG_THRESHOLD;
                }
                return time < timeThreshold;
            } else {
                MatrixLog.i(TAG, "mMessage is null");
            }
        } catch (Exception e) {
            return false;
        }
        return false;
    }

主动模拟SIGQUIT信号

另外,我们自己进程也可以手动触发一个SIGQUIT信号,来获取trace文件里额外的信息。比如在发生卡顿的时候,也可以使用SIGQUIT来让我们进程dump一个当前的堆栈。

不过这个方法比较重,因为dump堆栈的时候,需要暂停所有的线程,会影响程序的运行效率。所以一般不会在线上轻易使用,可以在线下监控的时候用。

static void nativePrintTrace() {
    fromMyPrintTrace = true;
    kill(getpid(), SIGQUIT);
}

总结

综上所述,判断当前进程是否发生ANR的业界通用做法如下:

  • 通过拦截SIGQUIT来判断系统是否向进程发送信号
  • 收到SIGQUIT后,判断主线程是否block,如果主线程block,上报ANR
  • 循环获取App ErrorState,如果状态变成NOT_RESPONDING,上报ANR

下一篇文章会讲,如何获取系统dump的trace文件,敬请期待。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容