什么是Crash?
App Crash全称Application crash, 对于Crash可分为Java Crash和Native Crash。
对于Crash所有的Android App开发者都会遇到,那么为什么会出现Crash呢?系统又是如何处理Crash的呢? 例如,在开发中大家经常使用try…catch语句来进行异常捕获,但还是会有一些异常是在运行中动态产生的,这些没有被有效捕获的异常就是导致应用Crash的原因。
小结:Crash是由于代码异常而导致App非正常退出现象,也就是我们常说的崩溃
系统的Crash 处理过程
1.1 Java Crash 产生过程
上图是Android App启动时序图,我们想要探知的Crash秘密就存在于ZygoteInit.zygoteInit()函数中。下面让我们一起来看一下它的内部实现。
public static final Runnable zygoteInit(int targetSdkVersion, long[] disabledCompatChanges,
String[] argv, ClassLoader classLoader) {
//重定向的System.out和System.err到Android日志
RuntimeInit.redirectLogStreams();
// **重点关注** 用于初始化一些App运行期间需要用到的配置
RuntimeInit.commonInit();
// 调用Native方法进行初始化
ZygoteInit.nativeZygoteInit();
...
}
可以看到zygoteInit
是一个组装函数,下面让我们一探隐藏在内部的RuntimeInit.commonInit()
函数。
protected static final void commonInit() {
// LoggingHandler 用于组装异常信息并打印
LoggingHandler loggingHandler = new LoggingHandler();
// API 30 开始加入RuntimeHooks。RuntimeHooks.setUncaughtExceptionPreHandler()内部调用了Thread.setUncaughtExceptionPreHandler()
RuntimeHooks.setUncaughtExceptionPreHandler(loggingHandler);
// KillApplicationHandler 用于弹出Dialog并杀死进程
Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler));
...
}
所以当Android App发生未被捕获的异常发生时会终止线程,此时系统便会调用UncaughtExceptionHandler
,告诉它被出错的线程以及对应的异常,然后便会调用uncaughtException
函数.如果UncaughtExceptionHandler
没有被设置,则会调用对应线程组的DefaultUncaughtExceptionHandler
。
Thread.setUncaughtExceptionPreHandler
覆盖所有线程,会在DefaultUncaughtExceptionHandler
之前调用,只能在Android Framework内部调用该方法.
Thread.setDefaultUncaughtExceptionHandler
在任意线程中设置即可作为所有线程的默认异常处理,可以在应用层调用,每次调用传入的Thread.UncaughtExceptionHandler
都会替换上一次的.
new Thread().setUncaughtExceptionHandler()
只可以处理当前线程的异常,如果有Thread设置了UncaughtExceptionHandler
,则在当前线程不会再使用全局的DefaultUncaughtExceptionHandler
.
LoggingHandler.Java
private static class LoggingHandler implements Thread.UncaughtExceptionHandler {
// 用于标识是否已触发处理逻辑
public volatile boolean mTriggered = false;
@Override
public void uncaughtException(Thread t, Throwable e) {
mTriggered = true;
// 如果 KillApplicationHandler.uncaughtException()已经被触发了则不再继续执行
if (mCrashing) return;
// 判断是否为系统进程,mApplicationObject == null,一定不是普通的app进程. 但是除了system进程, 也有可能是shell进程, 即通过app_process + 命令参数 的方式创建的进程
if (mApplicationObject == null && (Process.SYSTEM_UID == Process.myUid())) {
Clog_e(TAG, "*** FATAL EXCEPTION IN SYSTEM PROCESS: " + t.getName(), e);
} else {
StringBuilder message = new StringBuilder();
message.append("FATAL EXCEPTION: ").append(t.getName()).append("\n");
final String processName = ActivityThread.currentProcessName();
if (processName != null) {
message.append("Process: ").append(processName).append(", ");
}
message.append("PID: ").append(Process.myPid());
Clog_e(TAG, message.toString(), e);
}
}
}
- 当system进程Crash的信息:
以
*** FATAL EXCEPTION IN SYSTEM PROCESS [线程名]
为开头;
在第二行开始输出发生Crash时的调用栈信息;
- 其他进程Crash时的信息:
以
FATAL EXCEPTION: [线程名]
为开头
在第二行输出Process: [进程名], PID: [进程id]
;
第三行开始输出发生Crash时的调用栈信息;
KillApplicationHandler.Java
private static class KillApplicationHandler implements Thread.UncaughtExceptionHandler {
// 内部持有LoggingHandler用于输出异常信息。
private final LoggingHandler mLoggingHandler;
public KillApplicationHandler(LoggingHandler loggingHandler) {
this.mLoggingHandler = Objects.requireNonNull(loggingHandler);
}
@Override
public void uncaughtException(Thread t, Throwable e) {
try {
// 触发异常信息记录
ensureLogging(t, e);
//避免在处理崩溃信息时发生异常导致无限重入
if (mCrashing) return;
mCrashing = true;
//进行意外停止分析
if (ActivityThread.currentActivityThread() != null) {
ActivityThread.currentActivityThread().stopProfiling();
}
// 在此处弹出弹窗并杀死所在进程 **核心 见小节1.2**
ActivityManager.getService().handleApplicationCrash(
mApplicationObject, new ApplicationErrorReport.ParcelableCrashInfo(e));
} catch (Throwable t2) {
...
} finally {
// 通过finally语句块保证能执行并彻底杀掉Crash进程。当Crash进程被杀后,并没有完全结束,还有Binder死亡通知的流程还没有处理完成。
Process.killProcess(Process.myPid());
System.exit(10);
}
}
private void ensureLogging(Thread t, Throwable e) {
// 调用mLoggingHandler组装并打印异常信息
}
}
上面的代码注释很详细就不再赘述了。现在我们可以总结一下Android Crash的特点:
1. 一般情况下程序出错时会弹出提示框;
2. 程序所在进程被杀死,JVM虚拟机退出;
3. 系统提供了捕获Crash的接口;
4. 由Java 层代码引发的Java Crash 较容易捕获分析;
5. 有C++ 层代码引发的Native Crash 一般的工具不能将其捕获;
1.2 发生Crash 时系统都做了什么?
在上一节中我们详细的梳理了Android Crash的产生过程,其中在KillApplicationHandler .uncaughtException()
函数中我们看到了ActivityManager.getService().handleApplicationCrash()
这样一个函数,其实这才是Android系统在发生Crash时所做事情的核心入口,接下来我们就将对其展开讲解。
我们先来看一下处理Crash的流程图:
ActivityManager.getService()返回的是ActivityManagerProxy实例(简称AMP),AMP经过binder调用最终交给ActivityManagerService(简称AMS)中相应的方法去处理,故接下来调用的是AMS.handleApplicationCrash()。
1.2.1 AMS.handleApplicationCrash()
public void handleApplicationCrash(IBinder app,
ApplicationErrorReport.ParcelableCrashInfo crashInfo) {
// 获取进程对象,见1.2.2小节
ProcessRecord r = findAppProcess(app, "Crash");
final String processName = app == null ? "system_server"
: (r == null ? "unknown" : r.processName);
// 见1.2.3小节
handleApplicationCrashInner("crash", r, processName, crashInfo);
}
关于进程名(processName):
- 当远程IBinder对象为空时,则进程名为system_server;
- 当远程IBinder对象不为空,且ProcessRecord为空时,则进程名为unknown;
- 当远程IBinder对象不为空,且ProcessRecord不为空时,则进程名为ProcessRecord对象中相应进程名。
1.2.1 AMS.findAppProcess()
private ProcessRecord findAppProcess(IBinder app, String reason) {
if (app == null) {
return null;
}
synchronized (this) {
return 找到app对应的进程信息,如果没找到会返回Null;
}
}
1.2.3 AMS.handleApplicationCrashInner()
void handleApplicationCrashInner(String eventType, ProcessRecord r, String processName,
ApplicationErrorReport.CrashInfo crashInfo) {
//将Crash信息写入到 Event log
EventLog.writeEvent(EventLogTags.AM_CRASH,...);
//将错误信息添加到 DropBox
addErrorToDropBox(eventType, r, processName, null, null, null, null, null, crashInfo);
// 见1.2.4
mAppErrors.crashApplication(r, crashInfo);
}
addErrorToDropBox
是将Crash的信息输出到目录/data/system/dropbox
。例如system_server的dropbox文件名为system_server_crash@时间戳.txt
1.2.4 AppErrors.crashApplication()
void crashApplication(ProcessRecord r, ApplicationErrorReport.CrashInfo crashInfo) {
//清除远程调用者uid和pid信息,并保存到origId
final int callingPid = Binder.getCallingPid();
final int callingUid = Binder.getCallingUid();
final long origId = Binder.clearCallingIdentity();
try {
if(存在ActivityController,比如monkey){
// 调用monkey的appCrashed
retutn;
}
// 见1.2.5
if (!makeAppCrashingLocked()) {
Binder.restoreCallingIdentity(origId);
return;
}
...
//发送消息SHOW_ERROR_MSG,弹出提示crash的对话框,等待用户选择【见小节10】
mUiHandler.sendMessage(msg);
//进入阻塞等待,直到用户选择crash对话框
int res = result.get();
} finally {
//恢复远程调用者uid和pid
Binder.restoreCallingIdentity(origId);
}
}
此方法主要做的两件事:
- 调用
makeAppCrashingLocked
,继续处理Crash流程;- 发送消息,弹出提示Crash的对话框,等待用户选择;
1.2.5 AppErrors.makeAppCrashingLocked()
private boolean makeAppCrashingLocked(ProcessRecord app,
String shortMsg, String longMsg, String stackTrace, AppErrorDialog.Data data) {
app.setCrashing(true);
//封装crash信息到crashingReport对象
app.crashingReport = generateProcessError(app,
ActivityManager.ProcessErrorStateInfo.CRASHED, null, shortMsg, longMsg, stackTrace);
// 见1.2.6
app.startAppProblemLocked();
// 停止屏幕冻结 见1.2.7
app.getWindowProcessController().stopFreezingActivities();
// 见1.2.8
return handleAppCrashLocked(app, "force-crash" /*reason*/, shortMsg, longMsg, stackTrace,
data);
}
1.2.6 ProcessRecord.startAppProblemLocked()
void startAppProblemLocked() {
ComponentName errorReportReceiver = null;
for (int userId : mService.mUserController.getCurrentProfileIds()) {
if (this.userId == userId) {
// 获取获取当前用户下的Crash应用的 ErrorReceiver(需要在系统的设置中的错误报告功能开启时才会有值)
errorReportReceiver = ApplicationErrorReport.getErrorReportReceiver(
mService.mContext, info.packageName, info.flags);
}
}
//调用AMS忽略当前app的广播接收
mService.skipCurrentReceiverLocked(this);
}
此方法主要做的两件事:
- 如果开启了错误报告功能,获取广播接收器的组件名称;
- 忽略发生Crash应用的所有广播接收;
1.2.7 ProcessRecord.stopFreezingActivities()
public void stopFreezingActivities(boolean force) {
...
//其中activities类型为ArrayList<ActivityRecord>,停止进程里所有的Activity
int i = activities.size();
while (i > 0) {
i--;
activities.get(i).stopFreezingScreenLocked(true);
}
}
public void stopFreezingScreenLocked(boolean force) {
// appToken是WindowManager的token。 见1.7.9
WMS.stopAppFreezingScreen(appToken, force);
}
1.2.8 AppErrors.handleAppCrashLocked()
boolean handleAppCrashLocked(ProcessRecord app, String reason, String shortMsg, String longMsg, String stackTrace) {
if(同一进程在1分钟内连续两次crash){
if(不是persistent进程){
// ActivityStackSupervisor 简称ASS
ASS.handleAppCrashLocked, 直接结束该应用所有activity
AMS.removeProcessLocked,杀死该进程以及同一个进程组下的所有进程
}
ASS.resumeTopActivitiesLocked,恢复栈顶第一个非finishing状态的activity
}else{
ASS.finishTopRunningActivityLocked,执行结束栈顶正在运行activity
}
}
1.2.9 WMS.stopFreezingScreenLocked()
public void stopFreezingScreen() {
if (权限检查) {
throw new SecurityException("Requires FREEZE_SCREEN permission");
}
synchronized(mWindowMap) {
1. 处理屏幕旋转相关逻辑;
2. 移除冻屏的超时消息;
3. 屏幕旋转动画的相关操作;
4. 使能输入事件分发功能;
5. display冻结时,执行gc操作;
6. 更新当前的屏幕方向;
7. 发送configuraion改变的消息。
}
}
1.2.10 AMS.UiHandler
final class UiHandler extends Handler {
public void handleMessage(Message msg) {
switch (msg.what) {
case SHOW_ERROR_MSG:
1. 创建提示crash对话框,等待用户选择,5分钟操作等待。
2. 阻塞等待用户选择,当用户不做任何选择5分钟超时后,默认选择“确定”,当手机休眠时也默认选择“确定”
break;
...
}
}
1.3 总结
本文主要结合源码,详细介绍了App Crash后系统的处理流程:
1.首先发生Crash所在进程,在创建之初便准备好了DefaultUncaughtHandler,用来来处理Uncaught Exception,并输出当前Crash基本信息;
2. 调用当前进程中的AMP.handleApplicationCrash;经过binder ipc机制,传递到system_server进程;
3. 接下来,进入system_server进程,调用binder服务端执行AMS.handleApplicationCrash;
4. 从mProcessNames查找到目标进程的ProcessRecord对象;并将进程crash信息输出到目录/data/system/dropbox;
5. 执行makeAppCrashingLocked如果开启了错误报告功能,创建当前用户下的Crash应用的ErrorReceiver,并忽略当前应用的广播;
停止当前进程中所有Activity中的WMS的冻结屏幕消息,并执行相关一些屏幕相关操作;
6. 执行handleAppCrashLocked方法,
当1分钟内同一进程连续Crash两次时,且非persistent进程,则直接结束该应用所有Activity,并杀死该进程以及同一个进程组下的所有进程。然后再恢复栈顶第一个非finishing状态的Activity;
当1分钟内同一进程连续crash两次时,且persistent进程,,则只执行恢复栈顶第一个非finishing状态的Activity;
当1分钟内同一进程未发生连续Crash两次时,则执行结束栈顶正在运行Activity的流程。7. 通过UiHandler发送消息SHOW_ERROR_MSG,弹出Crash对话框;
8. system_server进程执行完成。回到Crash进程开始执行杀掉当前进程的操作;
9. 当Crash进程被杀,通过binder死亡通知,告知system_server进程来执行appDiedLocked();
10. 最后,执行清理应用相关的activity/service/ContentProvider/receiver组件信息。
这基本就是整个应用Crash后系统的执行过程。
小知识:
当60s内连续Crash两次的非persistent进程时,被认定为bad进程:那么如果第3次从后台启动该进程(Intent.getFlags来判断),则会拒绝创建进程;
当Crash次数达到两次的非persistent进程发生Crash时,则再次杀该进程,随后即便允许自启的Service也会在被杀后拒绝再次启动。