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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章