美文网首页安卓开发笔记仓库Android开发
如何优雅的退出应用和处理崩溃异常自动重启

如何优雅的退出应用和处理崩溃异常自动重启

作者: nanchen2251 | 来源:发表于2017-07-21 18:11 被阅读3042次

    写在前面

    这是最近一些朋友问我的问题,我把它整理成了一个库,供大家享用,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 中的 <Application> 标签中添加 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 类,因为我们崩溃的时候需要结束程序后再重启,所以我们需要退出应用,这里我们采用上面的第三种方式。

    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。

    /**
         * 当 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 信息。

      /**
         * 自定义错误处理,收集错误信息,发送错误报告等操作均在此完成
         *
         * @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:
    allprojects {
            repositories {
                ...
                maven { url 'https://jitpack.io' }
            }
        }
    
    Step 2. Add the dependency
    dependencies {
                compile 'com.github.nanchen2251:AppManager:1.0.1'
        }
    

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

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

    3、你也可以禁止重启

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

    效果图

    [图片上传失败...(image-ad15ca-1509694874814)]

    做不完的开源,写不完的矫情。欢迎扫描下方二维码或者公众号搜索「nanchen」关注我的微信公众号,目前多运营 Android ,尽自己所能为你提升。如果你喜欢,为我点赞分享吧~


    nanchen

    相关文章

      网友评论

      • 37d5fc52de9e:等待5秒左右,提示要重启,然后anr了.不动了...怎么回事..
      • 黑白咖:为什么在uncaughtException方法里,重启了Activity后,又removeAllActivity
      • 黑白咖:签到
      • 七界无尘Zxl:和我的处理一模一样,不过多了重启,赞一个
        nanchen2251:@七界无尘Zxl 哈哈,其实很多都是这样处理的。
      • 书生依旧:new Thread() {
        @Override
        public void run() {
        Looper.prepare();
        Toast.makeText(mContext, getTips(ex), Toast.LENGTH_LONG).show();
        Looper.loop();
        }
        }.start();

        这个代码,子线程更新 UI 了啊
        nanchen2251:@书生依旧 建议你先尝试一下再来哈。
        书生依旧: @南尘2251 Toast 啊
        nanchen2251: @书生依旧 哪里是更新ui了?
      • 请叫我章鱼哥:屌屌屌👍👍👍👍
        辅助上分:Application存这么多activity 考虑过内存泄漏么。。
        nanchen2251: @王涛涛涛涛 那就来我公众号投稿。哈哈哈

      本文标题:如何优雅的退出应用和处理崩溃异常自动重启

      本文链接:https://www.haomeiwen.com/subject/uquekxtx.html