Android之點選Home鍵後再次開啟導致APP重啟問題

lvxiangan發表於2018-09-28

問題描述:

1、開發者打包釋出一個release版本app,頁面結構如下:閃屏頁SplashActivity---> 登入頁LoginActivity---> 主頁MainActivity

2、使用者下載app到手機,通過檔案管理器找到並安裝這個apk,安裝後提示:“安裝完成,你可以開啟xxx應用了”,

3、使用者開啟app,輸入賬號密碼跳轉到了主頁MainActivity。

4、使用者按下Home鍵,然後在程式列表點選app,
先後顯示:閃屏頁SplashActivity---> 登入頁LoginActivity,APP重啟了!
期待頁面:顯示原先的主頁MainActivity。


奇怪的是:真機在debug開發除錯時不會出現這個問題。

 

 

錯誤原因:
debug版本是通過adb安裝啟動或平常的桌面Icon圖示啟動,
release版本是安裝這類第三方平臺啟動。

兩者的啟動intent不相同!(相同是指:啟動類,action、category等等全部一樣,不可多項也不可缺少)

 

在解決問題前,先了解一下相關知識:

1、Home主介面其實也是一個Activity。當從APP介面按下Home鍵盤,實際是啟動APP跳轉到Home主介面,這樣我們的程式就被置於後臺,被這個Home主介面Activity覆蓋。

2、Activity的Task管理

  Android系統的App啟動與切換管理依賴於相關Activity的Task的管理。一個Task之中可能含有若干個Activity,為了簡便起見,我們這裡記錄
【Task A】的Activity分別為 【A1】 、【A2】等,
【Task B】的Activity分別為 【B1】 、【B2】。

那麼我們來分析下App之間是怎麼切換的。假設應用都是單Task應用(相對於大部分的普通App來說,都是採用單一Task來管理的)

  桌面程式App:【TaskA】 ---- 存在Activity有【A1】 ----  其棧的結構為 A1
        應用程式B:【TaskB】 ---- 存在Activity有【B1】【B2】 ---- 其棧的結構為 B1_B2
        應用程式C: 【TaskC】 ---- 存在Activity有【C1】【C2】 ---- 其棧的結構為 C1_C2

a、那麼我們進入桌面時:Task之間的結構是 A1 ---- 也就是隻有一個【TaskA】棧(桌面Task),並且位於最前端(這裡表現為最後新增的末端)

b、然後我們點選應用程式B的圖示,啟動B :Task之間的結構是 A1B1B2  ---- 新增了一個【TaskB】,而且【TaskB】也是位於最前端,現在顯示的是【TaskB】的B2的Activity的介面

c、接著點選home鍵: Android對於home做了特殊預設處理,就是會把桌面Task挪到所以Task最前端,Task結構應該變成  B1_B2_A1 ---- 【TaskA】挪到佇列最前端,現在顯示的是【TaskA】的A1的Activity的介面,也就是桌面

d、我們再在桌面點選應用程式C的圖示,啟動C : Task之間的結構變成 B1B2A1C1C2 ---- 新增了一個【TaskC】,而且【TaskC】也是位於最前端,現在顯示的是【TaskC】的C2的Activity的介面

從上面的例子,我們可以知道:

  我們編寫任何一個Activity的時候,都可以在AndroidManifest裡面顯式指定一個taskAffinity的屬性,也就是說該Activity歸屬於對應taskAffinity的棧;如果沒有指定任何taskAffinity,那麼該Activity將會直接歸屬於包名所在的Task之下。而我們啟動一個Activity時(這裡只討論standard啟動模式),那麼回去先搜尋對應的Task是否存在,如果不存在,新建一個Task並將Activity入棧,如果已經存在對應的Task,那麼直接在對應Task入棧即可

那麼問題來了:如果我們在上面第d步點選的圖片並不是程式C的圖示,而是重新點選了程式B的圖示,此時【TaskB】是已經存在的了,那麼為了不會講B的入口activity(B1)直接在【TaskB】入棧,而是將【TaskB】挪到前臺並不做任何Activity啟動的操作呢?

3、桌面的啟動管理:

  回頭研究下AndroidManifest這個檔案,我們輕而易舉發現,但凡是App入口Activity,那麼一定會包含 

<intent-filter>
    <action android:name="android.intent.action.MAIN" />
    <category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

這幾行程式碼。這裡到底有什麼玄機呢?其實這個就是跟桌面約定好的啟動攔截過濾器。因為桌面有一個很明顯的需求就是,如果我們再次點選已經在後臺的App圖示時,是應該將該後臺任務挪到前臺而不是再次啟動該App程式。

而從柯元旦所著的《android核心剖析》一書中有記錄如下規則:

  每次啟動Intent導致新建立Task的時候,該Task會記錄導致其建立的Intent;而如果後續需要有一個新的與建立Intent完全一致(完全一致定位為:啟動類,action、category等等全部一樣,不可多項也不可缺少),那麼該Intent並不會觸發Activity的新建啟動,而只會將已經存在的對應Task移到前臺;這也就是為什麼桌面會在再次點選圖示時將後臺任務挪到前臺而不是重新啟動App的實現。

  那麼為啥要指定入口Activity特定的action和category呢,那就是為了讓桌面啟動app所用的Intent具有特殊性,也就是新增了特別的攔截器,避免其他應用內或者應用間的Intent對於這個啟動方式的干擾。

說了這麼多,我們可以著手分析上續bug的產生原因了。

原理剖析

  檔案管理器雖然使用Intent來啟動剛剛安裝的那個App,但:它的啟動Intent並沒有跟桌面的啟動Intent完全一致!

我們將桌面的Task記為【TaskDesktop】,檔案管理器的Task記為【TaskFile】,我們應用的Task記為【TaskApp】,分析如下:

進入桌面: D1 ---- D1是單純的桌面

開啟檔案管理器: D1_F1_F2 ---- F2是安裝完畢後詢問是否啟動對應程式的Activity

點選開啟: D1_F1_F2_A1_A2 ---- A1是入口閃屏頁,A2是登入Activity

返回桌面: F1_F2_A1_A2_D1 ---- 回到桌面頁,也就是D1前置

點選A的圖示: F1_F2_D1_A1_A2_A1 ---- 找到【TaskA】,挪到前臺,由於比對Intent並不是完全一致,所以該請求是新啟動Activity,那麼把A1新增到對應的【TaskA】中

所以bug出現了,出現了再一次的閃屏頁【A1】,問題定位成功!

PS:這裡我稍微變種一下,因為一般我們閃屏頁都是在啟動登入頁後finish的,而登入頁一般是singleTask模式

開啟檔案管理器: D1_F1_F2 ---- F2是安裝完畢後詢問是否啟動對應程式的Activity

點選開啟: D1_F1_F2_A2 ---- A1是入口閃屏頁,A2是登入Activity,啟動後A1業務邏輯應該finish掉,所以從【TaskA】中挪去

返回桌面: F1_F2_A2_D1 ---- 回到桌面頁,也就是D1前置

點選A的圖示: F1_F2_D1_A2_A1 -> 找到【TaskA】,挪到前臺,由於比對啟動的Intent不完全一致,所以新建立一個A1 Activity,那麼把A1新增到對應的【TaskA】中,然後A1所再一次觸發啟動登入頁 A2,但是登入頁是singleTask模式,所以又回到了上次對應的A2登入頁,所以現象為再一次出現閃屏頁,然後回到原先的登入頁介面

解決思路

  正常啟動的閃屏頁Activity必定在【TaskA】的最底部(實際已finish掉被登入頁取代),而第二次閃屏Activity不可能位於Task的最底部,所以在閃屏頁Activity的onCreate程式碼:

    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        // 避免從桌面啟動程式後,會重新例項化入口類的activity
        if (!this.isTaskRoot()) {
            Intent intent = getIntent();
            if (intent != null) {
                String action = intent.getAction();
                if (intent.hasCategory(Intent.CATEGORY_LAUNCHER) && Intent.ACTION_MAIN.equals(action)) {
                    finish();
                    return;
                }
            }
        }
    }

也可以這樣修改:

    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        //首次啟動 Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT 為 0,再次點選圖示啟動時就不為零了
        if ((getIntent().getFlags() & Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) != 0) {
            finish();
            return;
        }
    }

這兩種方法在 setContentView() 方法之前和之後都可以。

 

讀到這裡,細心的讀者一定會問:你上面說的情形只適用閃屏頁和登入頁,如果登入進去主頁MainActivity按Home鍵,如何處理呢?

1、閃屏頁的OnCreate方法根據登入狀態判斷跳轉

        if ((getIntent().getFlags() & Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) != 0) {
            if (UserInfo.getInstance().isLogined) { // 若已登入,跳轉到主頁
                readyGoThenKill(MainActivity.class);
            } else { // 若未登入,引導使用者登入
                readyGoThenKill(UserLoginActivity.class);
            }
            return;
        }

2、設定MainActivity的launchMode="singleTask",在AndroidManifest.xml修改

<activity
    android:name="com.emp.frame.MainActivity"
    android:launchMode="singleTask" />

 

PS:如果APP頁面複雜、路徑很深,要所有頁面都實現:按Home鍵---再次點選app圖示---恢復原來頁面狀態不重啟,暫時沒有太好的方法,只能像上面一樣逐一指定launchMode=“singleTask”及跳轉判斷,歡迎有其他方法的朋友留言分享,謝謝!

 

 

參考:碼農叔叔(enjoy風鈴)出處:http://www.cnblogs.com/net168/ 

 

 

 

 

相關文章