本篇是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);
}
}
...
}
主要做了三件事:
生成错误信息;
通过binder通知到AMS的handleApplicationCrash方法;
结束当前进程,完成应用的彻底退出。
接下来,我们看一下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的场景有很多种,比如输入事件无响应,广播事件无响应,Service中超时等等,具体类型和超时时间如下。
ANR的的触发逻辑和crash稍有不同,ARN是基于system_server进程触发的,而不是APP进程,我们这里仍然以输入事件为例进行讲解。
这里以输入事件类型举例,分发流程中会在InputDispatcher中进行处理,其中会有一个mAnrTimeouts集合来记录。事件如果分发出去,就会添加到这个集合当中,如果收到APP的响应,则会从这个集合中移除。当这个集合中时间最靠前的一个事件超过5S时,就会进入到输入事件的ANR流程。
具体的ANR流程图如下,这里仍然不详细讲解了,我们只要知道,最后会走到ProcessErrorStateRecord.java的appNotResponding的方法即可。
也是在这个方法中,完成日志采集和无响应弹框的提示的。其它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显示)。
上面讲到,无论crash还是anr,最终都会通知到dropbox。那么我们如果想监听这些异常,只要在dropbox中找一个切入点,等到发生异常的时候能够收到通知,就可以实现我们想要的效果了。所以接下来,我们就看一下dropbox收到异常信息后执行的流程,来找出这个可以hook的点。
我们仍然正向的梳理传递流程。
所以,最终处理异常信息的是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路径下的对应异常文件即可。
其实这里异常文件的命名也是有规律的,命名组成:type@time。
所以,收到广播之后,可以根据收到的参数拼接文件名,然后读取对应的异常文件即可知道到底是哪个应用,发生的哪种类型的异常了。
问1:应用崩溃后,有哪些方案可以实现跳转一个固定的页面,而不是崩溃退出?
这个问题印象中第一次被问到,还是字节跳动的面试题。这个问题的答案就先不回答了,方案绝不止一种,答案就留给读者来回答吧。