本文出處:
炎之鎧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