APM系列之二-原理篇-监听整个系统的anr和crash

前言

本篇是APM系列文章的第二篇,主要介绍如何通过一个第三方应用,去监控整个系统中所有应用的异常事件,比如Crash或者anr。

一.应用的崩溃流程

安卓应用是按照线程去执行的,当线程中发生异常,并且线程的相关调用堆栈并没有try catch保护的话,那么线程会结束执行,在destroy的时候,会进行异常处理。相关代码如下:

thread.cc

void Thread::Destroy() {
    ...
    //处理异常
    HandleUncaughtExceptions(soa);
}

void Thread::HandleUncaughtExceptions(ScopedObjectAccessAlreadyRunnable& soa) {
  if (!IsExceptionPending()) {
    return;
  }
  ScopedLocalRef peer(tlsPtr_.jni_env, soa.AddLocalReference(tlsPtr_.opeer));
  ScopedThreadStateChange tsc(this, ThreadState::kNative);

  // Get and clear the exception.
  ScopedLocalRef exception(tlsPtr_.jni_env, tlsPtr_.jni_env->ExceptionOccurred());
  tlsPtr_.jni_env->ExceptionClear();
   
  //这里通知java层中Thread中的dispatchUncaughtException方法。
  // Call the Thread instance's dispatchUncaughtException(Throwable)
  tlsPtr_.jni_env->CallVoidMethod(peer.get(),
      WellKnownClasses::java_lang_Thread_dispatchUncaughtException,
      exception.get());
  ...
}

在Thread的方法中,会有固定的异常处理机制,我们先看代码,在解释。

public final void dispatchUncaughtException(Throwable e) {
        Thread.UncaughtExceptionHandler initialUeh =
                Thread.getUncaughtExceptionPreHandler();
        if (initialUeh != null) {
            try {
                initialUeh.uncaughtException(this, e);
            } catch (RuntimeException | Error ignored) {
                // Throwables thrown by the initial handler are ignored
            }
        }
        getUncaughtExceptionHandler().uncaughtException(this, e);
}

Thread.UncaughtExceptionHandler类型的对象initialUeh用来记录崩溃信息,并不处理异常。其在安卓中的实现类是RuntimeInit.LoggingHandler。而getUncaughtExceptionHandler()返回的是RuntimeInit.KillApplicationHandler,这个就是我们APP中真正去处理崩溃的实现类,我们看一下这个实现类:

private static class KillApplicationHandler implements Thread.UncaughtExceptionHandler {
        private final LoggingHandler mLoggingHandler;
        ...
        @Override
        public void uncaughtException(Thread t, Throwable e) {
            try {
                //1.处理异常信息
                ensureLogging(t, e);
                ...
                //2.通知AMS
                ActivityManager.getService().handleApplicationCrash(
                        mApplicationObject, new ApplicationErrorReport.ParcelableCrashInfo(e));
            } catch (Throwable t2) {
               ...
            } finally {
                // Try everything to make sure this process goes away.
                //3.结束当前进程
                Process.killProcess(Process.myPid());
                System.exit(10);
            }
        }
        ...
    }

主要做了三件事:

  1. 生成错误信息;

  2. 通过binder通知到AMS的handleApplicationCrash方法;

  3. 结束当前进程,完成应用的彻底退出。

接下来,我们看一下system_server侧接受后的处理。

首先,AMS的handlerApplicationCrash方法中收到崩溃信息后,标记异常类型为crash,然后交给其handleApplicationCrashInner方法处理。

public void handleApplicationCrash(IBinder app, ApplicationErrorReport.ParcelableCrashInfo crashInfo) {
    ProcessRecord r = findAppProcess(app, "Crash");
    final String processName = app == null ? "system_server": (r == null ? "unknown" : r.processName);
    handleApplicationCrashInner("crash", r, processName, crashInfo);
}

二,记录异常到EventLog中。

EventLogTags.writeAmCrash(Binder.getCallingPid(),...)

三,发送异常信息到dropbox中,进行记录。

addErrorToDropBox(
        eventType, r, processName, null, null, null, null, null, null, crashInfo,
        new Float(loadingProgress), incrementalMetrics, null);

四,弹出相关的提示框,告知用户应用已崩溃。

mAppErrors.crashApplication(r, crashInfo);

其中,发送异常信息到dropbox的流程。AMS是一个system_server的线程,也是一个服务。而dropbox也是一样,也是一个服务和线程。DroxBoxManagerSerivice服务最终会完成最终崩溃信息的记录和持久化,最终存储到data/system/dropbox文件夹下。这一块我们就不详细赘述了,我们只要知道,所有APP的崩溃,都会通知到dropbox即可。

native崩溃的机制其实也是类似的,最终也是会通知到dropbox,这里就不扩展了。

二.应用的ANR流程

导致ANR的场景有很多种,比如输入事件无响应,广播事件无响应,Service中超时等等,具体类型和超时时间如下。

APM系列之二-原理篇-监听整个系统的anr和crash_第1张图片

ANR的的触发逻辑和crash稍有不同,ARN是基于system_server进程触发的,而不是APP进程,我们这里仍然以输入事件为例进行讲解。

这里以输入事件类型举例,分发流程中会在InputDispatcher中进行处理,其中会有一个mAnrTimeouts集合来记录。事件如果分发出去,就会添加到这个集合当中,如果收到APP的响应,则会从这个集合中移除。当这个集合中时间最靠前的一个事件超过5S时,就会进入到输入事件的ANR流程。

具体的ANR流程图如下,这里仍然不详细讲解了,我们只要知道,最后会走到ProcessErrorStateRecord.java的appNotResponding的方法即可。

APM系列之二-原理篇-监听整个系统的anr和crash_第2张图片

也是在这个方法中,完成日志采集和无响应弹框的提示的。其它ANR的类型最终也都会走到这个方法。

我们看一下ProcessErrorStateRecord中的appNotResponding方法:

class ProcessErrorStateRecord {
    ...
    void appNotResponding(String activityShortComponentName, ApplicationInfo aInfo,...) {
        ...
        //生成异常文件
        File tracesFile = ActivityManagerService.dumpStackTraces(firstPids,isSilentAnr ? null : processCpuTracker, isSilentAnr ? null : lastPids, nativePids, tracesFileException, offsets, annotation);
        ...
        //记录异常LOG
        FrameworkStatsLog.write(FrameworkStatsLog.ANR_OCCURRED, mApp.uid, mApp.processName,...)
        //通知到DropBox
        mService.addErrorToDropBox("anr", mApp, mApp.processName, activityShortComponentName, parentShortComponentName, parentPr, null, report.toString(), tracesFile, null, new Float(loadingProgress), incrementalMetrics, errorId);
        //展示异常弹框
        Message msg = Message.obtain();
        msg.what = ActivityManagerService.SHOW_NOT_RESPONDING_UI_MSG;
        msg.obj = new AppNotRespondingDialog.Data(mApp, aInfo, aboveSystem);
        mService.mUiHandler.sendMessageDelayed(msg, anrDialogDelayMs);
    }
}

主要做了四件事:

1.生成异常文件;

2.记录异常LOG,这里在log日志中会有体现;

3.把异常信息通知到Dropbox服务中;

4.展示异常弹框,这时候,应用无响应的弹框就显示出来了(会延时5S显示)。

三.监听应用crasn和anr

上面讲到,无论crash还是anr,最终都会通知到dropbox。那么我们如果想监听这些异常,只要在dropbox中找一个切入点,等到发生异常的时候能够收到通知,就可以实现我们想要的效果了。所以接下来,我们就看一下dropbox收到异常信息后执行的流程,来找出这个可以hook的点。

我们仍然正向的梳理传递流程。

APM系列之二-原理篇-监听整个系统的anr和crash_第3张图片

所以,最终处理异常信息的是DropBoxManagerService的add()方法,我们看一下这个方法:

public void add(DropBoxManager.Entry entry) {
    File temp = null;
    ...
    //生成异常文件
    temp = new File(mDropBoxDir, "drop" + Thread.currentThread().getId() + ".tmp");
    //发送异常广播
    final Intent dropboxIntent = new Intent(DropBoxManager.ACTION_DROPBOX_ENTRY_ADDED);
    dropboxIntent.putExtra(DropBoxManager.EXTRA_TAG, tag);
    dropboxIntent.putExtra(DropBoxManager.EXTRA_TIME, time);
    if (!mBooted) {
        dropboxIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
    }
    //发送广播
    mHandler.sendMessage(mHandler.obtainMessage(MSG_SEND_BROADCAST, dropboxIntent));
}

看到这里,突然豁然开朗,这里竟然有一个广播,那么,是不是只要监听这个广播,就可以实现监听Crash和ANR了呢?

我们详细看一下这一块的代码:

public void add(DropBoxManager.Entry entry) {
    final Intent dropboxIntent = new Intent(DropBoxManager.ACTION_DROPBOX_ENTRY_ADDED);
    dropboxIntent.putExtra(DropBoxManager.EXTRA_TAG, tag);
    dropboxIntent.putExtra(DropBoxManager.EXTRA_TIME, time);
    if (!mBooted) {
        dropboxIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
    }
    mHandler.sendMessage(mHandler.obtainMessage(MSG_SEND_BROADCAST, dropboxIntent));
}

if (msg.what == MSG_SEND_BROADCAST) {
    getContext().sendBroadcastAsUser((Intent)msg.obj, UserHandle.SYSTEM,
            android.Manifest.permission.READ_LOGS);
}

可以看到,首先生成了一个Action为ACTION_DROPBOX_ENTRY_ADDED的Intent,添加了两个参数分别是TAG和TIME。TAG可以区分是crash还是anr,而time就是异常的时间。

再看发送广播,广播要求接受者是system级别的,并且要求READ_LOGS的权限。

最后看一下dropbox的路径,mDropBoxDir="/data/system/dropbox"

所以,我们只要构造一个Action为ACTION_DROPBOX_ENTRY_ADDED类型的广播接收器,收到广播之后,再去读取dropbox路径下的对应异常文件即可。

APM系列之二-原理篇-监听整个系统的anr和crash_第4张图片

其实这里异常文件的命名也是有规律的,命名组成:type@time。

所以,收到广播之后,可以根据收到的参数拼接文件名,然后读取对应的异常文件即可知道到底是哪个应用,发生的哪种类型的异常了。

四.扩展性问题

问1:应用崩溃后,有哪些方案可以实现跳转一个固定的页面,而不是崩溃退出?

这个问题印象中第一次被问到,还是字节跳动的面试题。这个问题的答案就先不回答了,方案绝不止一种,答案就留给读者来回答吧。

你可能感兴趣的:(#,安卓-性能优化和稳定性,android,开发语言,dropbox,crash,anr)