Bootstrap

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

本文出处
炎之铠csdn博客:http://blog.csdn.net/totond
炎之铠邮箱:[email protected]
本文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

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;