我们都知道,安卓中如果有未处理的异常,会导致崩溃并且退出应用。而如果你有一些java开发经验的话,你也许会知道,java中如果有未处理的异常,只会中断当前的线程,应用进程本身并不会退出。这是为何?安卓不也是基于java去开发的吗?
我们就带着这个疑问,去学习了解安卓中的异常处理机制,从而解答这个问题。
备注:本文的异常仅指java层的,native层的另外篇章讲解。
我们首先做一个实验,创建两个线程1和2,线程1和2中都是每隔1S输出一次内容。但是让线程2在第3次输出时崩溃,会怎样呢?代码如下:
new Thread(() -> {
int i = 0;
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1,次数:" + i++);
}
}).start();
new Thread(() -> {
int i = 0;
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程2,次数:" + i++);
if (i == 2) {
String str = null;
System.out.print(str.length());
}
}
}).start();
实验结果如下,证明线程2停掉了,线程1仍继续执行。
线程1,次数:0
线程2,次数:0
线程2,次数:1
线程1,次数:1
Exception in thread "Thread-1" java.lang.NullPointerException
at com.xt.Other.lambda$main$1(Other.java:46)
at java.lang.Thread.run(Thread.java:748)
线程1,次数:2
这时候,你也许会尝试一下主线程崩溃会怎样,这个需求满足,代码如下:
new Thread(() -> {
int i = 0;
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程1,次数:" + i++);
}
}).start();
String str = null;
System.out.print(str.length());
我们发现,主线程崩溃了,仍然不会影响子线程的执行,结果如下:
Exception in thread "main" java.lang.NullPointerException
at com.xt.Other.main(Other.java:36)
线程1,次数:0
线程1,次数:1
所以,我们可以得到一个初步的结论,java的崩溃,只会终止所在线程的执行,并不会导致应用进程的退出。
同样的实验我们在安卓上试一下,同样的代码发现无论是主线程,还是子线程异常,都会提示应用的异常退出。
我们这里举一个子线程崩溃的例子,方便我们后续演示,代码如下,点击之后就会触发子线程崩溃。
if (getString(R.string.test).equalsIgnoreCase(title)) {
new Thread(new Runnable() {
@Override
public void run() {
String str = null;
System.out.println(str.length());
}
}).start();
}
java当中(当然包括安卓),其实线程中所有的未处理异常,最终都会由虚拟机转交到Thread.dispatchUncaughtException方法中,该方法如下:
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);
}
首先我们看一下initialUeh对象,这是Thread中的uncaughtExceptionPreHandler,安卓中,一般会把往其中设置RuntimeInit.LoggingHandler对象,用来收集一些崩溃日志信息。如下图:
因为这里不涉及到主流程,所以具体如何去采集崩溃日志的我们就不展开了,因为无论这里的initialUeh是否为空,都会执行到最后的这行代码:
getUncaughtExceptionHandler().uncaughtException(this, e);
那么getUncaughtExceptionHandler返回的是什么呢?如下:
public UncaughtExceptionHandler getUncaughtExceptionHandler() {
return uncaughtExceptionHandler != null ?
uncaughtExceptionHandler : group;
}
uncaughtExceptionHandler是当前对象中的成员变量UncaughtExceptionHandler,
group是当前对象中的ThreadGroup
private ThreadGroup group;
private volatile UncaughtExceptionHandler uncaughtExceptionHandler;
一般来说,我们是不会主动给uncaughtExceptionHandler设置对象的,所以会走到ThreadGroup.uncaughtException的逻辑。
ThreadGroup.uncaughtException方法如下,它首先一层一层上抛逻辑,直到传递到最上层parent=null,这时候它又会去获取Thread中的defaultUncaughtExceptionHandler对象,然后交由其进行异常处理。
public void uncaughtException(Thread t, Throwable e) {
if (parent != null) {
parent.uncaughtException(t, e);
} else {
Thread.UncaughtExceptionHandler ueh =
Thread.getDefaultUncaughtExceptionHandler();
if (ueh != null) {
ueh.uncaughtException(t, e);
} else if (!(e instanceof ThreadDeath)) {
System.err.print("Exception in thread \""
+ t.getName() + "\" ");
e.printStackTrace(System.err);
}
}
}
我们看一下Thread中的defaultUncaughtExceptionHandler,如下,它是一个静态的成员变量,所以所有的线程(包括主线程)用的都是最同一个对象。
// null unless explicitly set
private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;
我们通过断点调试,发现这个对象是RuntimeInit.KillApplicationHandler,如下图所示。
所以,我们就要看一下KillApplicationHandler中到底做了什么。
另外,KillApplicationHandler是何时设置进去的?这个我们2.5小节来讲。
我们看一下其中的uncaughtException方法,如下:
private static class KillApplicationHandler implements Thread.UncaughtExceptionHandler {
private final LoggingHandler mLoggingHandler;
...
@Override
public void uncaughtException(Thread t, Throwable e) {
try {
ensureLogging(t, e);
...
ActivityManager.getService().handleApplicationCrash(
mApplicationObject, new ApplicationErrorReport.ParcelableCrashInfo(e));
} catch (Throwable t2) {
...
} finally {
// Try everything to make sure this process goes away.
Process.killProcess(Process.myPid());
System.exit(10);
}
}
...
}
核心就三块,
1.ensureLogging:如果2.2中未处理异常,则再次进行处理,这里的处理逻辑只是收集相关信息。
2.handleApplicationCrash:把崩溃信息转发到AMS,尤其完成日志的采集和记录。
最终日志会记录到data/system/dropbox文件夹下,这一块我们2.4小节来讲。
3.杀掉当前进程,并且退出当前正在执行的线程。
Process.killProcess(Process.myPid());
System.exit(10);
所以,安卓之所以发生异常进程会退出,原因就在于此。
上面说到,会通过handleApplicationCrash的方式传递到AMS,由AMS完成崩溃的记录和持久化,我们来看一下这个流程。
首先,AMS中handleApplicationCrash方法完成接收,传递给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);
}
handleApplicationCrashInner中主要就是日志的崩溃记录,最后通过addErrorToDropBox方法进行日志记录,最终传递到DroxBoxManagerSerivice中最终完成崩溃信息的记录,因为不涉及到主流程,所以我们就不展开了,只要知道最终是保存到data/system/dropbox文件夹下即可。
void handleApplicationCrashInner(String eventType, ProcessRecord r, String processName,
ApplicationErrorReport.CrashInfo crashInfo) {
float loadingProgress = 1;
IncrementalMetrics incrementalMetrics = null;
// Obtain Incremental information if available
if (r != null && r.info != null && r.info.packageName != null) {
..。各种信息采集,记录到crashInfo中
);
final int relaunchReason = r == null ? RELAUNCH_REASON_NONE
: r.getWindowProcessController().computeRelaunchReason();
final String relaunchReasonString = relaunchReasonToString(relaunchReason);
if (crashInfo.crashTag == null) {
crashInfo.crashTag = relaunchReasonString;
} else {
crashInfo.crashTag = crashInfo.crashTag + " " + relaunchReasonString;
}
//进行日志记录,把crashInfo中的翻译成string进行记录
addErrorToDropBox(
eventType, r, processName, null, null, null, null, null, null, crashInfo,
new Float(loadingProgress), incrementalMetrics, null);
mAppErrors.crashApplication(r, crashInfo);
}
这个其实要涉及到APP的启动流程了,启动流程的问题具体可以看这一篇:android源码学习- APP启动流程(android12源码)
我们这里直接用下图讲解,一样就不细细讲了。
最终在commonInit()方法中,完成的loggingHandler和defaultUncaughtExceptionHandler的设置。
LoggingHandler loggingHandler = new LoggingHandler();
RuntimeHooks.setUncaughtExceptionPreHandler(loggingHandler);
Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler));
既然上面讲到,安卓进程的崩溃,是APP自己处理的。并且2.2中讲到,优先处理Thread中的uncaughtExceptionHandler对象,只有这个对象为空时,才会走到系统默认的defaultUncaughtExceptionHandler中。所以,我们是否自己可以设置uncaughtExceptionHandler来避免进程被杀死呢?
首先,我们在子线程中做一个实验,还是用2.1的例子,但是我们主动设置一个uncaughtExceptionHandler,代码如下:
if (getString(R.string.test).equalsIgnoreCase(title)) {
//A线程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
String str = null;
System.out.println(str.length());
}
});
thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) {
}
});
thread.start();
return;
}
实验结果正如我们猜测那样,进程没有退出,其它功能仍然能继续使用。
然后,我们在主线程中试一下,代码如下:
if (getString(R.string.test).equalsIgnoreCase(title)) {
//A线程
Thread.currentThread().setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) {
}
});
String str = null;
System.out.println(str.length());
return;
}
我们发现,虽然没有崩溃,但是后续点击任何按钮都没有反应了,而且过了一会提示了一个ANR。这是为什么呢?
其实上面所说的问题,原因是在于安卓主线程处理任务,采用的是Handler机制。即主线程永远不退出,依次执行queue中的任务。而每个任务通过runnable的方式注册到queue中去执行,注册线程有可能是主线程,也有可能是子线程。
为什么安卓这么设计呢?很简单啊,如果主线程执行完任务退出了,那么后续谁来响应我们的各种操作呢?
具体handler的原理本文就不扩展了,有兴趣的可以看一下这篇文章,讲的详细:
android源码学习-Handler机制及其六个核心点
我们看一下Handler中是如何执行runnable任务的。代码在Looper的loopOnce方法中:
private static boolean loopOnce(final Looper me,
final long ident, final int thresholdOverride) {
...
try {
msg.target.dispatchMessage(msg);
if (observer != null) {
observer.messageDispatched(token, msg);
}
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
} catch (Exception exception) {
if (observer != null) {
observer.dispatchingThrewException(token, msg, exception);
}
throw exception;
} finally {
ThreadLocalWorkSource.restore(origWorkSource);
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
...
}
如果diapatchMessage中出现异常,那么就会走到catch中,但是catch中又再次抛出了异常,所以由loopOnce方法的上层去拦截。
public static void loop() {
...
for (;;) {
if (!loopOnce(me, ident, thresholdOverride)) {
return;
}
}
}
loop中也没有相关的异常处理操作,所以loop方法就会执行完成,就代表主线程执行完了。主线程都执行完成了,那么谁还会响应我们的操作呢?自然就是任何点击都无反应了。
既然主线程不能退出,那么有什么办法可以保证主线程正常分发任务事件,又能trycatch住主线程异常呢?办法自然是有的,我们可以往主线程注册一个永不结束的任务,然后再这个任务中,再去做具体主线程任务的分发就可以了。代码如下:
if (id == R.id.button1) {
Handler().post {
while (true) {
try {
Log.i("lxltest", "loop启动")
Looper.loop()
} catch (e: Exception) {
e.printStackTrace()
}
}
}
} else if (id == R.id.button2) {
throw NullPointerException("null point")
}
这样我们实验下来,主线程的未处理异常就不会导致进程退出了,这也是一个开源框架的核心原理:https://github.com/android-notes/Cockroach //避免主线程异常导致退出的一个框架。
当然,这样做也会存在各种各样的问题,比如做数据处理的时候发生异常未处理,再去进行界面渲染,就有可能显示一个异常的界面。这个就由读者自行选择吧。
最出名的异常监控工具应该就是bugly了,它的做法是通过注册defaultUncaughtExceptionHandler,在自定义的ExceptionHandler中,去完成异常日志的统计和持久化,在完成后杀掉当前进程。所以我们可以模仿着bugly实现一个小的异常日志监控工具,当然,由于只能注册一个defaultUncaughtExceptionHandler,所以我们要完成了自己的异常统计和上报后,要在交还给bugly。最终实现代码如下
public class BuglyCrashHandler implements Thread.UncaughtExceptionHandler {
Thread.UncaughtExceptionHandler exceptionHandler;//bugly的出异常处理handler
List activities = new ArrayList<>();
static BuglyCrashHandler instance;
public BuglyCrashHandler(Application application, Thread.UncaughtExceptionHandler handler) {
exceptionHandler = handler;
registerActivityListener(application);
instance = this;
}
@Override
public void uncaughtException(Thread t, Throwable e) {
recordCrash();
if (exceptionHandler != null) {
exceptionHandler.uncaughtException(t, e);
}
}
public void recordCrash(Exception e) {
//完成异常的日志记录
}
public static BuglyCrashHandler getInstance() {
return instance;
}
}
调用处代码就更简单了,如下。请注意,务必在bugly的初始化代码之后调用。
Thread.setDefaultUncaughtExceptionHandler(new BuglyCrashHandler(this,Thread.getDefaultUncaughtExceptionHandler()));