最近公司有一个需求,就是如何让App 不奔溃或者奔溃后可以自动重启?咋一听,可能你和我都会说,对可能Crash的地方try…catch 不就可以了?
然而细琢磨一下这个问题,其实并非如此简单。。。。接下来大家就跟我一起看看App Crash背后的缘由吧!
如何让自己的App不奔溃呢?其实问题主要涉及一下几个点:
1、App为什么会Crash?
2、未捕获到的异常导致的Crash怎么办?
3、有什么办法可以让APP不奔溃呢?
4、假如App奔溃后,能否自动重启呢?
带着这几个问题,和大家分享一下我查阅到的解决方案,以下内容或多或少参阅了其他博文,请大家不要见笑。。就当是自己学习总结了下哈。
首先捕获程序崩溃的异常就必须了解一下java中UncaughtExceptionHandler这个接口,android沿用了此接口,在android API中通过实现此接口,能够处理线程被一个无法捕捉的异常所终止的情况。
在java API中对该接口描述如下:
一、首先我们看下线程中抛出异常以后的处理逻辑。一旦代码抛出异常,并且我们没有捕捉的情况下,JVM 会调用 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
}
}
//这里会获取对应的 UncaughtExceptionHandler 对象,然后调用对应的 uncaughtException 方法
getUncaughtExceptionHandler().uncaughtException(this, e);
}
public UncaughtExceptionHandler getUncaughtExceptionHandler() {
//可以看到当 uncaughtExceptionHandler 没有赋值的时候,会返回 ThreadGroup 对象
return uncaughtExceptionHandler != null ?
uncaughtExceptionHandler : group;
}
通过源码得知,如果 App 中没有手动设置 uncaughtExceptionHandler 对象,那么会执行 ThreadGroup的uncaughtException 方法:
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.getDefaultUncaughtExceptionHandler() 获取默认的 UncaughtExceptionHandler ,然后调用 uncaughtException 方法,既然名字是默认的 uncaughtExceptionHandler 对象,那么必然有初始化的地方…这就需要从系统初始化开始说起,为了简化初始化流程,咱们直接从 RuntimeInit 的 main 方法开始说起
@UnsupportedAppUsage
public static final void main(String[] argv) {
enableDdms();
if (argv.length == 2 && argv[1].equals("application")) {
if (DEBUG) Slog.d(TAG, "RuntimeInit: Starting application");
redirectLogStreams();
} else {
if (DEBUG) Slog.d(TAG, "RuntimeInit: Starting tool");
}
commonInit();
/*
* Now that we're running in interpreted code, call back into native code
* to run the system.
*/
nativeFinishInit();
if (DEBUG) Slog.d(TAG, "Leaving RuntimeInit!");
}
@UnsupportedAppUsage
protected static final void commonInit() {
if (DEBUG) Slog.d(TAG, "Entered RuntimeInit!");
/*
* set handlers; these apply to all threads in the VM. Apps can replace
* the default handler, but not the pre handler.
*/
LoggingHandler loggingHandler = new LoggingHandler();
RuntimeHooks.setUncaughtExceptionPreHandler(loggingHandler);
Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler))
...代码省略...
initialized = true;
}
在这里,给Thread 设置一个 KillApplicationHandler 对象,而 KillApplicationHandler实现了 Thread.UncaughtExceptionHandler 这个接口,那么必然会重写 uncaughtException 方法。
App为什么会Crash?关键代码就在这里
@Override
public void uncaughtException(Thread t, Throwable e) {
try {
ensureLogging(t, e);
// Don't re-enter -- avoid infinite loops if crash-reporting crashes.
if (mCrashing) return;
mCrashing = true;
// Try to end profiling. If a profiler is running at this point, and we kill the
// process (below), the in-memory buffer will be lost. So try to stop, which will
// flush the buffer. (This makes method trace profiling useful to debug crashes.)
if (ActivityThread.currentActivityThread() != null) {
ActivityThread.currentActivityThread().stopProfiling();
}
// Bring up crash dialog, wait for it to be dismissed
ActivityManager.getService().handleApplicationCrash(
mApplicationObject, new ApplicationErrorReport.ParcelableCrashInfo(e));
} catch (Throwable t2) {
if (t2 instanceof DeadObjectException) {
// System process is dead; ignore
} else {
try {
Clog_e(TAG, "Error reporting crash", t2);
} catch (Throwable t3) {
// Even Clog_e() fails! Oh well.
}
}
} finally {
// Try everything to make sure this process goes away.
Process.killProcess(Process.myPid());
System.exit(10);
}
}
代码最后执行了 System.exit(10) ;而这个方法会直接干掉当前进程,也就是所谓的 App crash 了。所以我们一旦抛出异常,并且没有捕捉的话,程序就会被强制干掉。
这个问题其实上面有提到,实现UncaughtExceptionHandler接口,显示处理线程异常终止就行,具体方法如下:
/**
* Created by lxb on 2020/12/11
* <p>
* Thread.UncaughtExceptionHandler
* 接口说明:能够处理线程被一个无法捕获的异常所终止
*/
public class UnExcepHandler implements Thread.UncaughtExceptionHandler {
private static final String TAG = "UnExcepHandler";
private CatchExceptionApp application;
private final Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler;
public UnExcepHandler(CatchExceptionApp application) {
// 获取系统默认的UncaughtExceptionHandler 处理器
defaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
this.application = application;
}
/**
* @param thread Thread
* @param ex ex ==null,表示没有显示的处理异常;
* ex!=null,表示 thread为出异常的线程
*/
@Override
public void uncaughtException(@NonNull Thread thread, @NonNull Throwable ex) {
// 如果用户没有处理,则让系统默认的异常处理器处理
if (!handlerException(ex) && defaultUncaughtExceptionHandler != null) {
defaultUncaughtExceptionHandler.uncaughtException(thread, ex);
} else {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Intent intent = new Intent(application, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP
| Intent.FLAG_ACTIVITY_CLEAR_TASK
| Intent.FLAG_ACTIVITY_NEW_TASK);
@SuppressLint("WrongConstant") PendingIntent restartIntent =
PendingIntent.getActivity(application.getApplicationContext(), 0,
intent, intent.getFlags());
// 退出程序,1秒后自动重启程序
AlarmManager alarmManager = (AlarmManager) application.getSystemService(Context.ALARM_SERVICE);
alarmManager.set(AlarmManager.RTC,
System.currentTimeMillis() + 1000, restartIntent);
// application.finishAllActivity();
}
}
/**
* 自定义错误处理 / 收集错误信息 / 发送错误报告 都在此处理
*
* @param ex
* @return true: 处理了该异常 false: 不处理异常(系统处理)
*/
public boolean handlerException(Throwable ex) {
if (ex == null) {
return false;
}
// 显示无法捕获的异常
new Thread() {
@Override
public void run() {
super.run();
Looper.prepare();
Toast.makeText(application.getApplicationContext(),
"很抱歉,程序出现异常,即将退出。", Toast.LENGTH_LONG).show();
Looper.loop();
}
}.start();
return true;
}
通过在android Application 这个全局类中处理异常即可。
/**
* Created by lxb on 2020/12/11
* 全局捕获异常
*/
public class CatchExceptionApp extends Application {
private static final String TAG = "CatchExceptionApp";
public void init() {
// 设置UnExcepHandler 为程序的默认处理器
UnExcepHandler unExcepHandler = new UnExcepHandler(this);
Thread.setDefaultUncaughtExceptionHandler(unExcepHandler);
}
如何杀死异常进程,重启应用,就得使用PendingIntent,这个类是android中对Intent类的包装,具体代码如下:
/**
* @param thread Thread
* @param ex ex ==null,表示没有显示的处理异常;
* ex!=null,表示 thread为出异常的线程
*/
@Override
public void uncaughtException(@NonNull Thread thread, @NonNull Throwable ex) {
// 如果用户没有处理,则让系统默认的异常处理器处理
if (!handlerException(ex) && defaultUncaughtExceptionHandler != null) {
defaultUncaughtExceptionHandler.uncaughtException(thread, ex);
} else {
Intent intent = new Intent(application, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP
| Intent.FLAG_ACTIVITY_CLEAR_TASK
| Intent.FLAG_ACTIVITY_NEW_TASK);
@SuppressLint("WrongConstant") PendingIntent restartIntent =
PendingIntent.getActivity(application.getApplicationContext(), 0,
intent, intent.getFlags());
// 退出程序,1秒后自动重启程序
AlarmManager alarmManager = (AlarmManager) application.getSystemService(Context.ALARM_SERVICE);
alarmManager.set(AlarmManager.RTC,
System.currentTimeMillis() + 1000, restartIntent);
// application.finishAllActivity();
}
}
通过AlarmManager 启动它,并且关闭打开的Activity杀死异常进程就能够实现重新启动应用。
通过以上三个探究,很显然是有办法让App 不奔溃的。再重新看下这段源码:
public UncaughtExceptionHandler getUncaughtExceptionHandler() {
//可以看到当uncaughtExceptionHandler没有赋值的时候,会返回ThreadGroup对象
return uncaughtExceptionHandler != null ?
uncaughtExceptionHandler : group;
}
上面有提到只有在没有手动设置 UncaughtExceptionHandler 的时候,才会调用
defaultUncaughtExceptionHandler 对象,所以自然而然的就想到了实现这个类,然后在这里面做相应的处理。
接下来咱们可以写一个demo来验证这个推测到底可行不可行。。。
1、首先人为制造一个异常(主动抛出异常)看看效果
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btnCrashTwo: // 主线程抛出异常不奔溃处理
mainThreadCrash();
break;
}
}
private void mainThreadCrash() {
int i = 1 / 0;
Log.d("MainActivity", "i:" + i);
}
来看一下执行效果(运行效果图后期补上,抱歉!):
不出意料程序崩溃了!!!
那接下来咱们手动设置一个UncaughtExceptionHandler
/**
* Created by lxb on 2020/12/11
*/
public class CrashHandler implements Thread.UncaughtExceptionHandler {
private static final String TAG = "CrashHandler";
@Override
public void uncaughtException(@NonNull Thread thread, @NonNull Throwable e) {
Log.d(TAG, "[uncaughtException] : " + e.getMessage());
// 通过handler将toast抛到主线程弹出
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(CatchExceptionApp.mApp, "[uncaughtException] message:\n"
+ e.getMessage(), Toast.LENGTH_LONG).show();
}
});
}
}
public class CatchExceptionApp extends Application {
private static final String TAG = "CatchExceptionApp";
public static CatchExceptionApp mApp;
@Override
public void onCreate() {
super.onCreate();
mApp = this;
//主线程Crash处理
/**
* Thread.currentThread().setUncaughtExceptionHandler(new CrashHandler());
* tip:
* 这个是设置当前线程的方法,总不能给每个Thread 都设置一个,这肯定不可取
* Thread 中还有一个全局静态的 UncaughtExceptionHandler 可以解决这一问题
*/
Thread.currentThread().setUncaughtExceptionHandler(new CrashHandler());
}
}
再运行看下效果 :
看来我们的推测是正确的,确实 App 已经不会 crash 了,但是又出现了另外一个问题, App 卡死在了这个界面,怎么点击也没反应了。
那么这到底是怎么一回事呢?其实这也不难理解,我们的页面启动的入口是在 ActivityThread 的 main 方法:在这里面进行初始化主线程的 Loop ,然后执行 loop 循环,我们知道 Looper 是用来循环遍历消息队列的,一旦消息队列中存在消息,那么就会执行里面的操作。
整个 Android 系统就是基于事件驱动的,而事件主要就是基于 Looper 来获取的。所以如果这里一旦出现
crash,那么就直接会跳出整个 main 方法,自然 loop 循环也就跳出了,那么自然而然事件也就接收不到,更没法处理,所以整个 App
就会卡死在这里。
2、有没有其他办法可以保证 App 在抛出异常不 crash 的情况下,又能保证不会卡死呢?
既然 looper 是查询事件的核心类,那么我们是否可以不让跳出 loop 循环呢,乍一想好像没办法做到,我们没法给 loop 方法 try-catch 。但是我们可以给消息队列发送一个 loop 循环,然后给这个 loop 做一个 try-catch ,一旦外层的 loop 检测到这个事件,就会执行我们自己创建的 loop 循环,这样以后 App 内的所有事件都会在我们自己的 loop 循环中处理。一旦抛出异常,跳出 loop 循环以后,我们也可以在 loop 外层套一层 while 循环,让自己的 loop 再次工作。
再通过代码验证下咱们的推测吧:
package com.lxb.app_crash_auto_reboot;
import android.app.Activity;
import android.app.Application;
import android.os.Handler;
import android.os.Looper;
import android.os.Process;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.List;
/**
* Created by lxb on 2020/12/11
* 全局捕获异常
*/
public class CatchExceptionApp extends Application {
private static final String TAG = "CatchExceptionApp";
public static CatchExceptionApp mApp;
@Override
public void onCreate() {
super.onCreate();
mApp = this;
//主线程Crash处理
Handler handler = new Handler(getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
// 未捕获的异常导致APP Crash 后,通过while 让自己创建的loop 再次工作
while (true) {
//给自己创建的loop 添加 try catch ,一旦外层loop(系统奔溃)检测到这个事件,然后执行我们自己的loop循环
try {
Looper.loop();
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(CatchExceptionApp.this,
"main-Thread 抛出了异常",
Toast.LENGTH_SHORT).show();
}
}
}
});
}
}
执行一下效果看看:
至此,最后我们就解决了抛出异常导致 App crash 的问题了。
难道问题就这样终结了吗?当然没有那么快就结束,这里给主线程的Looper 发送 loop 循环都是主线程操作的,那么子线程如果抛出异常怎么办呢,这么处理应该也是会 crash 吧,那再做个实验吧:
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btnCrash: // 子线程抛出异常不奔溃处理
childThreadCrash();
break;
}
}
/**
* 人为造一个crash
*/
private void childThreadCrash() {
new Thread(new Runnable() {
@Override
public void run() {
// tvMsg.setText("人为造一个crash");
int i = 1 / 0;
Log.d("MainActivity", "i:" + i);
}
}).start();
}
执行一下效果看看:
不出意料,确实是直接crash的,那这个时候该怎么办呢?
刚才说的Thread.currentThread().setUncaughtExceptionHandler(new CrashHandler());似乎也不行,这是设置当前 Thread 的方法,总不能给每个 Thread 都设置一个吧,这肯定不可取。不过这时 Thead 的全局静态的 UncaughtExceptionHandler 对象就派上用场了
private static volatile UncaughtExceptionHandler defaultUncaughtExceptionHandler;
ThreadGroup 里面最终会调用到他的方法,一开始在 RunTimeInit 里面初始化的。既然这样,那我们直接覆盖这个对象就可以
/**
* Created by lxb on 2020/12/11
* 全局捕获异常
*/
public class CatchExceptionApp extends Application {
private static final String TAG = "CatchExceptionApp";
public static CatchExceptionApp mApp;
@Override
public void onCreate() {
super.onCreate();
mApp = this;
Handler handler = new Handler(getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
// 未捕获的异常导致APP Crash 后,通过while 让自己创建的loop 再次工作
while (true) {
//给自己创建的loop 添加 try catch ,一旦外层loop(系统奔溃)检测到这个事件,然后执行我们自己的loop循环
try {
Looper.loop();
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(CatchExceptionApp.this,
"main-Thread 抛出了异常",
Toast.LENGTH_SHORT).show();
}
}
}
});
// 子线程Crash,需要添加这个
Thread.setDefaultUncaughtExceptionHandler(new CrashHandler());
}
}
这样就解决了子线程抛出异常而crash的问题了。
今天主要就说了一件事:如何捕获程序中的异常不让APP崩溃,从而给用户带来最好的体验。
主要有以下做法:
1、通过在主线程里面发送一个消息,捕获主线程的异常,并在异常发生后继续调用Looper.loop方法,使得主线程继续处理消息。
2、对于子线程的异常,可以通过Thread.setDefaultUncaughtExceptionHandler来拦截,并且子线程的停止不会给用户带来感知。
3、对于在生命周期内发生的异常,可以通过替换ActivityThread.mH.mCallback的方法来捕获,并且通过token来结束Activity或者直接杀死进程。
第三点文中未介绍,感兴趣的小伙伴可以自行查阅总结。
可能有的朋友会问,为什么要让程序不崩溃呢?会有哪些情况需要我们进行这样操作呢?
其实还是有很多时候,有些异常我们无法预料或者给用户带来几乎是无感知的异常,比如:
等等这些情况,我们就可以通过这样的操作来让APP牺牲掉这部分的功能来维护系统的稳定性。我们现在的需求采用这种方法就很符合,其他小伙伴可以根据自己App的需求,不过建议最好的方式就是控制代码质量,尽量减少 crash 的发生。
https://www.jianshu.com/p/37f363308d5f
https://developer.aliyun.com/article/63992
https://www.jianshu.com/p/4fa8a9b814b1
https://mp.weixin.qq.com/s/ZzkgnhalwBv9yAwHj8jQVA