美文网首页原理解析Android开发Android开发
Android APP一键退出的方法总结分析

Android APP一键退出的方法总结分析

作者: 炎之铠 | 来源:发表于2017-06-09 15:33 被阅读229次

    本文出处
    炎之铠csdn博客:http://blog.csdn.net/totond
    炎之铠邮箱:yanzhikai_yjk@qq.com
    本文demo地址:https://github.com/totond/TestAppExit
    本文原创,转载请注明本出处!

    前言——到底APP需不要退出功能

    Google是推荐APP不需要退出功能的,因为只要把APP切到后台,系统的GC机制会自动根据内存情况来对后台进程进行回收,如果APP进程没被回收的话,用户还能快速切回APP。然后在用户的习惯和一些开发者对这种设计的不重视(例如我上一篇写的Application里面的onTrimMemory()方法就没什么人用),还加上以前的手机内存不是很大(GC有时来不及清理后台,导致手机有时会变得很卡),就导致了这种退出APP的需求比较多(如很多APP的连点两下后退键退出功能),到了现在这个问题还是有很多人争论,可以看看这个
      对于这个问题,我保持中立(要是我说不需要的话,这篇博客就不用写了_),当有需求来的时候就做吧。网上也很多这方面的文章,有不少实现,但是每种方法都有它的优缺点,下面就把这些方法都测试,然后分析总结一下吧。

    本文测试使用的API版本为23,Android6.0,minSDK为API16.

    准备工作

    要测试肯定要先写demo,在这里先准备demo,里面的Activity跳转关系是这样的:


      所有Activity的启动模式是默认模式standard,后面测试需要再改。
      写了一个Application子类BaseApplication,用registerActivityLifecycleCallbacks()来监听每个Activity的生命周期改变,和输出当前进程ID(这个后面有用):
    registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
                @Override
                public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
                    Log.d(TAG, "onActivityCreated: " + activity.getLocalClassName());
                    Log.d(TAG, "Pid: " +  + Process.myPid());
                }
    
                @Override
                public void onActivityStarted(Activity activity) {
                    Log.d(TAG, "onActivityStarted: " + activity.getLocalClassName());
                }
    
                @Override
                public void onActivityResumed(Activity activity) {
                }
    
                @Override
                public void onActivityPaused(Activity activity) {
                }
    
                @Override
                public void onActivityStopped(Activity activity) {
                    Log.d(TAG, "onActivityStopped: " + activity.getLocalClassName());
                }
    
                @Override
                public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
    
                }
    
                @Override
                public void onActivityDestroyed(Activity activity) {
                    Log.d(TAG, "onActivityDestroyed: " + activity.getLocalClassName());
                }
            });
    

    每个Activity都有两个Button,一个用来调用退出APP的方法(CloseButton):

        public void onClick(View v) {
            Log.d(BaseApplication.getTAG(), "按下Close———————————————————————————— ");
            exitAPP1();
    //        exitAPP2();
    //        exitAPP3();
    //        exitAPP4();
    //        exitAPP5();
        }
    

    一个用来调用下面那几种行不通的方法,用来测试研究一下(TestButton):

        public void onClick(View v) {
            Log.d(BaseApplication.getTAG(), "按下Test———————————————————————————— ")
            System.exit(0);
    //        Runtime.getRuntime().exit(0);
    
    //        Process.killProcess(Process.myPid());
    
    
    //        ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
    //        activityManager.killBackgroundProcesses(context.getPackageName());
    
    //        activityManager.restartPackage(context.getPackageName());
        }
    

    到了这里,准备工作就做好了,由于代码较多,这里就不全贴了,具体的实现代码大家可以到demo地址去下。

    网上流传的几种不行的方法

    网上一些比较旧的资料,有一些经过我测试后,发现行不通的方法(不知道是不是当时他们用的时候行得通而到现在行不通)。

    System.exit(0)方法

    这个方法和Runtime.getRuntime().exit(0)等价,按道理来说是结束当前的虚拟机,经过测试之后发现,执行后是关闭了当前的虚拟机,但是它还重新启动了一个新的虚拟机,把除了当前Activity之外的其他未关闭的Activity按照顺序重新启动过一遍。简单来说,就是finish了当前Activity,然后刷新了一遍APP。从下面的log可以看到,APP的进程ID都变了(这里只开两个Activity是为了输出简洁一点,其实已做过各种各样花式的Activity启动顺序和启动模式的组合测试了,结论还是一样):

    com.yanzhikai.testappexit D/TestAppExit: onActivityCreated: Activities.MainActivity
    com.yanzhikai.testappexit D/TestAppExit: Pid: 29579
    com.yanzhikai.testappexit D/TestAppExit: onActivityStarted: Activities.MainActivity
    com.yanzhikai.testappexit D/TestAppExit: onActivityCreated: Activities.A0Activity
    com.yanzhikai.testappexit D/TestAppExit: Pid: 29579
    com.yanzhikai.testappexit D/TestAppExit: onActivityStarted: Activities.A0Activity
    com.yanzhikai.testappexit D/TestAppExit: onActivityStopped: Activities.MainActivity
    com.yanzhikai.testappexit D/TestAppExit: 按下Test———————————————————————————— 
    com.yanzhikai.testappexit D/TestAppExit: onActivityCreated: Activities.MainActivity
    com.yanzhikai.testappexit D/TestAppExit: Pid: 29985
    com.yanzhikai.testappexit D/TestAppExit: onActivityStarted: Activities.MainActivity
    

    Process.killProcess(Process.myPid())方法

    这个方法按道理是杀死当前线程,测试结果却发现实际效果和上面的System.exit(0)方法一样:

    com.yanzhikai.testappexit D/TestAppExit: onActivityCreated: Activities.MainActivity
    com.yanzhikai.testappexit D/TestAppExit: Pid: 30082
    com.yanzhikai.testappexit D/TestAppExit: onActivityStarted: Activities.MainActivity
    com.yanzhikai.testappexit D/TestAppExit: onActivityCreated: Activities.A0Activity
    com.yanzhikai.testappexit D/TestAppExit: Pid: 30082
    com.yanzhikai.testappexit D/TestAppExit: onActivityStarted: Activities.A0Activity
    com.yanzhikai.testappexit D/TestAppExit: onActivityStopped: Activities.MainActivity
    com.yanzhikai.testappexit D/TestAppExit: 按下Test———————————————————————————— 
    com.yanzhikai.testappexit D/TestAppExit: onActivityCreated: Activities.MainActivity
    com.yanzhikai.testappexit D/TestAppExit: Pid: 30603
    com.yanzhikai.testappexit D/TestAppExit: onActivityStarted: Activities.MainActivity
    

    所以,当前Activity是当前任务栈最后一个Activity,而APP又没有其他任务栈的时候,调用这两个方法是可以正常结束Activity的,就是和在第一个Activity调用finish差不多,不过finish不会结束进程,这两个方法会。
    至于上面两个方法为什么会这样,和API的描述不符,有人说是系统厂商的修改,有人说是Google的安全机制,本人才疏学浅,找不到原因,还请知道真相的各位告知一下,非常感谢。

    ActivityManager.killBackgroundProcesses()方法

    网上资料的写这个方法,看名字都觉得它不行了,结果当然是没有效果,这个应该是用于结束后台进程的方法,但是不知道为什么被人拿来说成结束当前APP。

    ActivityManager.restartPackage()方法

    这个是很旧的方法了,现在都弃用了(据说以前是可以的),现在在我的API24版本,它只是一个包着killBackgroundProcesses()方法的马甲:

        @Deprecated
        public void restartPackage(String packageName) {
            killBackgroundProcesses(packageName);
        }
    

    一键退出APP的方法

    讲了这么久终于进入正题,所谓一键,就是一调用这个方法,就可以关闭这个APP的意思,下面的几个方法是一键关闭当前APP里面所有的Activity,并且结束APP进程(要是不想结束可以不使用System.exit(0))。

    第一种方法——简单粗暴法

    网上的有一些方法是模拟Activity栈来管理Activity,这个方法不用模拟,直接调用系统API获取当前的任务栈,把里面的Activity全部finish掉,再结束进程(如果不想结束进程,可以不调用System.exit(0))。

        @TargetApi(Build.VERSION_CODES.LOLLIPOP)
        private void exitAPP1() {
            ActivityManager activityManager = (ActivityManager) context.getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE);
            List<ActivityManager.AppTask> appTaskList = activityManager.getAppTasks();
            for (ActivityManager.AppTask appTask : appTaskList) {
                appTask.finishAndRemoveTask();
            }
    //        appTaskList.get(0).finishAndRemoveTask();
            System.exit(0);
    

    根据API源码的注释描述,这个ActivityManager.getAppTasks()方法的作用应该是:

        Get the list of tasks associated with the calling application.
    

    但是经过测试,很多情况(一个APP里有多个任务栈,一个APP了开了多个进程)获取的appTaskList里面只有一个AppTask,只是获取到了当前的任务栈,注释上不是说会给出当前运行的APP的所有Task么,有知道的真相的请告知一下。

    优缺点

    优点:

    • 简单粗暴,直接用Context获取ActivityManager来获取APP当前任务栈就行了,不需要其他操作。

    缺点:

    • 这个方法只能结束当前任务栈,对于APP有多个任务栈的情况(有Activity的启动模式是singleInstance),不会结束其他的后台任务栈,这就需要自己做逻辑判断了;
    • 需要API21或以上,也就是Android5.0以上,根据从AndroidStudio2.3.2获得的数据,目前Android5.0以上的手机和平板占比是40.5%(不知道准不准,有没有考虑中国国情)。
        

    第二种方法——保存管理法

    这种方法在网上最流行了,就是自己建立一个容器,来保存正在运行的Activity的实例,在onCreate方法写入,在onDestroy方法写出,当需要结束APP的时候把所有Activity实例拿出来finish掉。这里我采用LinkedList,增删速度快。
      在BaseApplication存放activityLinkedList,通过registerActivityLifecycleCallbacks()方法来控制所有Activity实例的增删。

        @Override
        public void onCreate() {
            super.onCreate();
    
            activityLinkedList = new LinkedList<>();
    
            registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
                @Override
                public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
                    Log.d(TAG, "onActivityCreated: " + activity.getLocalClassName());
                    Log.d(TAG, "Pid: " +  + Process.myPid());
                    activityLinkedList.add(activity);
                }
    
                @Override
                public void onActivityStarted(Activity activity) {
                    Log.d(TAG, "onActivityStarted: " + activity.getLocalClassName());
                }
    
                @Override
                public void onActivityResumed(Activity activity) {
                }
    
                @Override
                public void onActivityPaused(Activity activity) {
                }
    
                @Override
                public void onActivityStopped(Activity activity) {
                    Log.d(TAG, "onActivityStopped: " + activity.getLocalClassName());
                }
    
                @Override
                public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
    
                }
    
                @Override
                public void onActivityDestroyed(Activity activity) {
                    Log.d(TAG, "onActivityDestroyed: " + activity.getLocalClassName());
                    activityLinkedList.remove(activity);
                }
            });
        }
        
    
        public static void showList() {
            for (Activity activity : activityLinkedList) {
                Log.d(TAG, "showList: " + activity.getLocalClassName());
            }
        }
    
        public static void exitAppList() {
            for (Activity activity : activityLinkedList) {
                activity.finish();
            }
        }
    

    结束APP的时候,调用exitAppList()方法并结束进程就可以了,还可以用showList()来log一下当前运行的Activity名单:

        private void exitAPP2() {
            BaseApplication.showList();
            BaseApplication.exitAppList();
            System.exit(0);
        }
    

    优缺点

    优点:

    • 这种方法不需要考虑到Activity有多个任务栈的情况,无论启动模式是什么,只要Activity的创建和结束时经历正常的生命周期——即创建时经过onCreate方法,结束时经过onDestroy方法,都不能逃离ActivityList的“魔爪”。

    缺点:

    • 前面说了Activity是要正常的经历生命周期,这种方法才不会出现问题。
        下面我们来测试一下,首先是按顺序进入到B0Activity,然后按下Test,这里的Test是执行一次System.exit(0),然后就会发现Close方法并不能正常结束所有Activity,只结束了当前Activity:

        上面这个问题并不大,因为应该没有人会无端端地调用一下System.exit(0),但是下面这个问题比较大,我在B0Activity人为的加一个空指针,让进入的时候会抛出空指针异常,让APP Crash,然后发现APP也是重启了进程,并且Activity栈回退到A0Activity,这时候可以看到activityLinkedList里面只有一个A0Activity了,并不能实现一键退出的效果。

        也就是说,采用这种方法,遇到Activity不是经过正常生命周期创建和结束的情况,是达不到退出的效果的(要是Activity非正常结束而且APP进程没有结束的话,activityLinkedList持有这个Activity的实例可能会导致内存泄漏,不过目前我还没看到过这种情况,上面两种情况都是进程重启了)。

    第三种方法——釜底抽薪法

    这种方法让APP的入口Activity采用SingleTask启动模式,那样如果在后面的Activity启动这个处于任务栈底部的Activity的时候,就会调用它的onNewIntent()方法,在这个方法里根据Intent判断是否调用finish,是的话那么整个任务栈的Activity就都结束了:

    这里在MainActivity里重写onNewIntent()方法:

        //exitApp3()方法使用
        @Override
        protected void onNewIntent(Intent intent) {
            super.onNewIntent(intent);
            if (intent != null) {
                boolean isExitApp = intent.getBooleanExtra("exit", false);
                if (isExitApp) {
                    this.finish();
                }
            }
        }
    

    退出时调用:

        private void exitAPP3() {
            Intent intent = new Intent(context, MainActivity.class);
            intent.putExtra("exit", true);
            context.startActivity(intent);
            System.exit(0);
        }
    

    优缺点

    优点

    • 不怕第二种方法那样的Activity不走正常生命周期的情况,实现比较简洁。

    缺点

    • 要让MainActivity的启动模式限定为singleTask
    • 这种方法也是只能结束当前任务栈的Activity,如果有启动模式为SingleInstance的Activity的话就要自己写逻辑判断了。

    第四种方法——RxBus退出法

    使用RxBus当作事件总线,当Activity在onCreate()的时候注册订阅:

        //exitApp4()方法使用
        private Disposable disposable;
    
        //exitApp4()方法使用注册订阅
        private void initRxBusExit(){
            disposable = RxBus.getInstance().toObservable(String.class)
                    .subscribe(new Consumer<String>() {
                        @Override
                        public void accept(String s) throws Exception {
                            if (s.equals("exit")){
                                finish();
                            }
                        }
                    });
        }
    

    在Activity的onDestroy()取消订阅:

            //exitApp4()方法使用取消订阅
            if (!disposable.isDisposed()){
                disposable.dispose();;
            }
    

    当需要退出的时候,发送事件:

        private void exitAPP4() {
            RxBus.getInstance().post("exit");
            System.exit(0);
        }
    

    这里RxBus的实现是基于RxJava2.0.1,参考了这篇文章,防止文章太长这里就不贴了,具体的实现可以去我的demo里看。

    优缺点

    优点

    • 可以与RxJava和RxBus结合,对于使用RxBus和RxJava的项目可以很容易地使用。

    缺点

    • 需要RxJava和RxBus,对于不使用的RxJava来实现的APP不推荐使用,可以使用其他的方法。
    • 需要在每个Activity的onCreate()方法和onDestroy()方法来注册和取消订阅,这样比较麻烦,不过可以采用一个Activity统一的基类解决,但是这样也有了要继承统一基类的麻烦。
    • 和第二种一样,要是出现Crash然后重启进程的话还是会失效。

    第五种方法——广播监听法

    这种方法和上一种比较像,通过让每一个Activity在onCreate()和onDestroy()的时候注册和注销一个广播接收器:

    public class CloseReceiver extends BroadcastReceiver {
        private Activity activity;
    
        public CloseReceiver(Activity activity){
            this.activity = activity;
        }
    
        @Override
        public void onReceive(Context context, Intent intent) {
            activity.finish();
        }
    }
    
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            //exitApp5()方法使用
            closeReceiver = new CloseReceiver(this);
            registerReceiver(closeReceiver,new IntentFilter(BaseApplication.EXIT));
        }
    
        protected void onDestroy() {
            super.onDestroy();
            //exitApp5()方法使用
            unregisterReceiver(closeReceiver);
        }
    

    当需要退出的时候调用:

        private void exitAPP5() {
            context.sendBroadcast(new Intent(BaseApplication.EXIT));
        }
    

    优缺点

    优点

    • 和第二种方法一样,不需要考虑到Activity有多个任务栈的情况。

    缺点

    • 需要为每个打开的Activity注册广播接收器,这样比较麻烦,不过可以采用一个Activity统一的基类解决,但是这样也有了要继承统一基类的麻烦。
    • 和第二种一样,要是出现Crash然后重启进程的话还是会失效。
    • 这种方法不能在后面加System.exit(0)来结束进程,因为执行了发送广播这个方法之后,不会等到广播接收器收到广播,程序就开始执行下一句System.exit(0),然后就直接变成执行System.exit(0)的效果了。

    总结

    总的来说,第二种方法——保存管理法比较实用,不过当Android5.0普及之后,第一种方法应该会用的比较多,因为用到SingleInstance启动模式Activity的APP应该比较少。当APPActivity数量不多,而且对启动模式没有特别的需求的时候(也就是懒的时候),可以选择第三种方法。而第四种方法则是比较适合用到RxJava的时候使用。

    参考文章

    http://www.jianshu.com/p/8cd954b43eed
    http://johnnyshieh.me/posts/rxbus-rxjava2/
    http://www.imooc.com/article/3300

    相关文章

      网友评论

      • 简域工作室:我只想说,当你 System.exit 以后,服务是会重启的。
        所以,如果真的要退出,请先 stop 服务。
        炎之铠:@Brevent 如何停止服务,停止那些服务,怎样停止服务的逻辑,是通过Activity的onDestory或者onStop等方法里面实现的啊,本文只涉及退出Activity。看来我是说的不清楚,要加一些说明,谢谢提出意见。

      本文标题:Android APP一键退出的方法总结分析

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