1. 程式人生 > >android-安裝應用直接“開啟”,home鍵返回桌面再次點選應用圖示,重新啟動MAIN_action的Activity

android-安裝應用直接“開啟”,home鍵返回桌面再次點選應用圖示,重新啟動MAIN_action的Activity

今天測試MM偶爾發現了這麼個神奇的現象:給測試MM發了個應用安裝包,MM安裝後開啟應用,進入主介面後,點選home鍵返回了桌面,然後從桌面再點選應用圖示再次開啟應用,這時候神奇的事情發生了~~~竟然打開了應用的登入介面(MAIN_action的Activity)!這不科學啊,這不是應該之前停留的介面麼!~

我自測了一遍,按照測試MM的操作就出現問題,但我自己用adb命令安裝後啟動app就沒有問題!這真吭啊~~ 趕緊分析分析找找解決辦法吧~

現象分析:
  大致說一下專案結構,MAIN-Activity是登入介面(普通的Activity,啟動模式為standard),然後根據一堆初始化操作和判斷,一般是接著進入【主頁Activity】(Activity的啟動模式為singleTask);點選home鍵不做任何攔截處理,按照系統預設邏輯返回Lanuch桌面。
  也就是說,app的整體互動邏輯並沒有特殊之處,並非業務邏輯導致的bug。那麼回顧下不同的地方,也就是啟動App的入口的區別了,一者是平常的桌面Icon圖示啟動,一者是安裝程式啟動入口。我們都知道,桌面啟動的話也是通過startActivity這個api通過特定的Intent向ActivityManagerServer發起啟動任務;所以我們可以推匯出安裝程式中的【開啟】,也是通過Intent啟動對應的App。
  再往下分析的話,可能需要一些前置知識需要了解才能更好的理解。

前置知識

1、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】 —- 其棧的結構為 B1B2
  應用程式C: 【TaskC】 —- 存在Activity有【C1】【C2】 —- 其棧的結構為 C1C2
  
a、那麼我們進入桌面時:Task之間的結構是 A1 —- 也就是隻有一個【TaskA】棧(桌面Task),並且位於最前端(這裡表現為最後新增的末端)

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

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

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

從上面的例子,我們可以大致瞭解到Android是怎麼管理不同app之間切換的邏輯:
  我們編寫任何一個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啟動的操作呢?

2、桌面的啟動管理
回頭研究下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的產生原因了。

原理剖析

  從以上的前置知識分析,我們大致得到這樣的結論:安裝程式直接開啟APP,它的啟動Intent和我們從桌面啟動APP時用到的Intent不一致!
  
我們將桌面的Task記為【TaskL】,安裝程式的Task記為【TaskQ】,我們應用的Task記為【TaskA】,那麼分析如下:
進入桌面: L1 —- L1是單純的桌面
安裝程式,進入app安裝: L1Q1Q2 —- Q2是安裝完畢後詢問是否啟動對應安裝程式的Activity
點選開啟: L1Q1Q2A1A2 —- A1是入口登入介面,A2是主頁Activity
返回桌面: Q1Q2A1A2L1 —- 回到桌面頁,也就是L1前置
點選A的圖示: Q1Q2L1A1A2A1 —- 找到【TaskA】,挪到前臺;但此時該TaskA判斷啟動它的Intent和之前啟動它的Intent不一致,所以重新啟動了一個Activity到TaskA中;

bug出現了!!! 再次啟動了登入介面,問題定位成功!

PS:如果主介面的launchMode為singleTask模式,那麼在【點選開啟】這一步時,task中存在的Activity為:L1Q1Q2A1A2,因為A1會被finish掉!

解決辦法

  1. 讓那些安裝程式修正其啟動Intent的設定,使其與原聲桌面啟動Intent保持完全一致。(PS:基本不可能)
  2. 自身業務程式碼規避:如果是多餘的入口Activity,其基本不可能位於Task的根部,而正常啟動的入口Activity必定在對應的Task的根部。所以我們可以從這個地方對於這個bug進行規避,方法就是在入口Activity的onCreate程式碼加入如下一段程式碼:
// 避免從桌面啟動程式後,會重新例項化入口類的activity
if (!this.isTaskRoot()) { // 當前類不是該Task的根部,那麼之前啟動
   Intent intent = getIntent();
   if (intent != null) {
      String action = intent.getAction();
      if (intent.hasCategory(Intent.CATEGORY_LAUNCHER) && Intent.ACTION_MAIN.equals(action)) { // 當前類是從桌面啟動的
         finish(); // finish掉該類,直接開啟該Task中現存的Activity
         return;
      }
   }
}

特別感謝: