本文出处:
炎之铠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