Fork me on GitHub

如果优雅地退出应用和处理崩溃异常并重启

写在前面

这是最近一些朋友问我的问题,我把它整理成了一个库,供大家享用,GitHub 地址:https://github.com/nanchen2251/AppManager

从四个应用场景说起

  • 退出应用
    相信各位朋友或多或少都会有遇到过需要在某个特定的地方退出应用的需求,这个场景一定非常普遍。

  • 崩溃后重启
    程序总是无法做到尽善尽美,有时候你也不知道因为什么原因导致了 APP 的崩溃,这无疑是非常糟糕的用户体验。这时候我们可以采用重启机制来增强用户舒适体验感。

  • 莫名其妙重启
    然而心细的小伙伴肯定会发现,在部分手机上会出现莫名其妙的崩溃后重启(后面会讲原因),而且最要命的是,假设你有三个 Activity,他们分别是 Act1, Act2, Act3,它们的启动顺序是 Act1 -> Act2 -> Act3,而如果在 Act3 发生了崩溃,这时候极有可能应用重启后进入的是 Act2,而 Act2 中需要某个来源于 Act1 (或者在 Act1 中通过接口获取) 的参数,当没有这个参数的时候会引发崩溃(或者数据不全)。这时候你可能最直观的想法就是禁止应用重启,但或许这并不是最佳的方式。

  • 崩溃时弹出一个对话框
    在部分手机上,当崩溃的时候,会弹出一个提示对话框。在这种情况下,用户只有点击 “强行关闭” 来结束程序。当该对话框出现,对用户来说是相当不友好的。或许我们可以通过某种方式拦截掉系统的处理,让应用出错时不再显示它。

退出应用的几种方式

Andorid 退出应用的方式很多,常见的也就下面四种。

  • System.exit(0) 使用系统的方法,强制退出
    System.exit(0) 表示的是终止程序,终止当前正在运行的 Java 虚拟机,在 Java 中我们也使用这种方式来关闭整个应用,在前期很多开发人员都是使用这种方式,我自己在开发项目过程中也用过这种方式来退出,但是有时候会在部分机型中,当退出应用后弹出应用程序崩溃的对话框,有时退出后还会再次启动,少部分的用户体验不太好。但现在也依旧还会有少部分的开发人员会使用这种方式,因为使用方式很简单,只需要在需要退出的地方加上这句代码就行。

  • 抛出异常,强制退出
    这种方式现在基本上已经看不到了,用户体验比第一种方式更差,就是让抛出异常、是系统崩溃、从而达到退出应用的效果

  • 使用 Application 退出
    目前比较常用方法之一,我们都知道 ApplicationAndroid 的系统组件,当应用程序启动时,会自动帮我们创建一个 Application,而且一个应用程序只能存在一个 Application ,它的生命周期也是最长的,如果需要使用自己创建的 Application 时,这个时候我们只需要在 Androidmanifest.xml 中的 标签中添加 name 属性:把创建的 Application 完整的包名 + 类名放进了就行了。

  • 使用广播退出
    使用广播来实现退出应用程序,其实实现的思路相对于第三种更简单,我们编写一个 BaseActivity,让其他的 Activity 都继承于它,当我需要退出时,我们就销毁 BaseActivity,那么其他继承与它的 Activity 都会销毁。

四种方式的代码也就不多提,需要的自己去Android:销毁所有的Activity退出应用程序几种方式

莫名其妙重启?

上面的场景中说到了,再部分手机上会出现崩溃后自动重启的情况,这让我们很不好控制。经本人测试,在 Android 的 API 21 ( Android 5.0 ) 以下,Crash 会直接退出应用,但是在 API 21 ( Android 5.0 ) 以上,系统会遵循以下原则进行重启:

  • 包含 Service,如果应用 Crash 的时候,运行着 Service,那么系统会重新启动 Service。
  • 不包含 Service,只有一个 Activity,那么系统不会重新启动该 Activity。
  • 不包含 Service,但当前堆栈中存在两个 Activity:Act1 -> Act2,如果 Act2 发生了 Crash ,那么系统会重启 Act1。
  • 不包含 Service,但是当前堆栈中存在三个 Activity:Act1 -> Act2 -> Act3,如果 Act3 崩溃,那么系统会重启 Act2,并且 Act1 依然存在,即可以从重启的 Act2 回到 Act1。

在这样的情况下,我们或许会有两种需求:

  • 崩溃后不允许重启
  • 崩溃后需要重启

怎么办

翻看 API 我们发现,Java 中存在一个 UncaughtExceotionHandler 的接口,而在 Android 中我们沿用了它,我们可以采用这个接口实现我们想要的功能。
(为了方便,我把它做成了库,传送门:https://github.com/nanchen2251/AppManager

讲一些核心

CrashApplication

首先是我们的 CrashApplication 类,因为我们崩溃的时候需要结束程序后再重启,所以我们需要退出应用,这里我们采用上面的第三种方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class CrashApplication extends Application {
private List<Activity> mActivityList;
@Override
public void onCreate() {
super.onCreate();
mActivityList = new ArrayList<>();
}
/**
* 添加单个Activity
*/
public void addActivity(Activity activity) {
// 为了避免重复添加,需要判断当前集合是否满足不存在该Activity
if (!mActivityList.contains(activity)) {
mActivityList.add(activity); // 把当前Activity添加到集合中
}
}
/**
* 销毁单个Activity
*/
public void removeActivity(Activity activity) {
// 判断当前集合是否存在该Activity
if (mActivityList.contains(activity)) {
mActivityList.remove(activity); // 从集合中移除
if (activity != null){
activity.finish(); // 销毁当前Activity
}
}
}
/**
* 销毁所有的Activity
*/
public void removeAllActivity() {
// 通过循环,把集合中的所有Activity销毁
for (Activity activity : mActivityList) {
if (activity != null){
activity.finish();
}
}
//杀死该应用进程
android.os.Process.killProcess(android.os.Process.myPid());
}
}

UncaughtExceptionHandlerImpl

我们当然少不了新建一个 UncaughtExceptionHandlerImpl 类去实现我们的 UncaughtExceptionHandler 接口,它必须实现我们的 uncaughtException(thread, throwable) 方法,我们接下来可以在这中间作文章。需要特别注意的是:重启必须清除堆栈内的 Activity。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 当 UncaughtException 发生时会转入该函数来处理
*/
@SuppressWarnings("WrongConstant")
@Override
public void uncaughtException(Thread thread, Throwable ex) {
if (!handleException(ex) && mDefaultHandler != null) {
// 如果用户没有处理则让系统默认的异常处理器来处理
mDefaultHandler.uncaughtException(thread, ex);
} else {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Log.e(TAG, "error : ", e);
}
if (mIsRestartApp) { // 如果需要重启
Intent intent = new Intent(mContext.getApplicationContext(), mRestartActivity);
AlarmManager mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
//重启应用,得使用PendingIntent
PendingIntent restartIntent = PendingIntent.getActivity(
mContext.getApplicationContext(), 0, intent,
Intent.FLAG_ACTIVITY_NEW_TASK);
mAlarmManager.set(AlarmManager.RTC, System.currentTimeMillis() + mRestartTime,
restartIntent); // 重启应用
}
// 结束应用
((CrashApplication) mContext.getApplicationContext()).removeAllActivity();
}
}

我们的 handleException(throwable) 方法用于弹出 Toast 和收集 Crash 信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* 自定义错误处理,收集错误信息,发送错误报告等操作均在此完成
*
* @param ex
* @return true:如果处理了该异常信息;否则返回 false
*/
private boolean handleException(final Throwable ex) {
if (ex == null) {
return false;
}
// 使用 Toast 来显示异常信息
new Thread() {
@Override
public void run() {
Looper.prepare();
Toast.makeText(mContext, getTips(ex), Toast.LENGTH_LONG).show();
Looper.loop();
}
}.start();
// 如果用户不赋予外部存储卡的写权限导致的崩溃,会造成循环崩溃
// if (mIsDebug) {
// // 收集设备参数信息
// collectDeviceInfo(mContext);
// // 保存日志文件
// saveCrashInfo2File(ex);
// }
return true;
}

封装好的使用

1、添加依赖

Step 1. Add it in your root build.gradle at the end of repositories:
1
2
3
4
5
6
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
Step 2. Add the dependency
1
2
3
dependencies {
compile 'com.github.nanchen2251:AppManager:1.0.1'
}

2、在需要使用的地方使用

1
2
// 设置崩溃后自动重启 APP
UncaughtExceptionHandlerImpl.getInstance().init(this, BuildConfig.DEBUG, true, 0, MainActivity.class);

3、你也可以禁止重启

1
// 禁止重启UncaughtExceptionHandlerImpl.getInstance().init(this,BuildConfig.DEBUG);

欢迎关注我的技术公众号(公众号搜索nanchen),每天一篇 Android 资源分享。

效果图

image