【轉載】Android Bug分析系列:第三方平臺安裝app啓動後,home鍵回到桌面後點擊app啓動時會再次啓動入口類bug的緣由剖析

前言html

  前些天,測試MM發現了一個比較奇怪的bug。前端

  具體表現是:android

  一、將app包經過電腦QQ傳送到手機QQ上面,點擊安裝,安裝後選擇打開app (此間的應用邏輯應該是要觸發 【閃屏頁Activity】, 而後跳轉 【主頁Activity】)api

  二、而後MM在 【主頁Activity】 時按下了 【Home鍵】,回到桌面app

  三、再點擊app的icon圖標,原諒耿直的咱們都是以爲應該直接回到【主頁Activity】,可是結果倒是又一次觸發 【閃屏頁Activity】,亮瞎了24K鈦合金狗眼的咱們以爲這玩法不對吧?測試

  四、而後,收拾收拾心情開始定位之路吧~this

 

現象分析spa

  先說說項目結構吧,咱們這邊的項目需求邏輯是 先進入 【閃屏頁Activity】(普通的Activity,啓動模式爲standard),而後根據一堆初始化操做和判斷,通常是接着進入【主頁Activity】(Activity的啓動模式爲singleTask);點擊home鍵不作任何攔截處理,按照系統默認邏輯返回Lanuch桌面。code

  也就是說,app的總體交互邏輯並無特殊之處,並不是業務邏輯致使的bug。那麼回顧下不一樣的地方,也就是啓動App的入口的區別了,一者是日常的桌面Icon圖標啓動,一者是QQ安裝這類第三方平臺啓動。咱們都知道,桌面啓動的話也是經過startActivity這個api經過特定的Intent向ActivityManagerServer發起啓動任務;因此咱們能夠推導出QQ安裝啓動這類方式也是經過Intent啓動對應的App。htm

  再往下分析的話,可能須要一些前置知識須要瞭解才能更好的理解。

 

前置知識

一、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啓動的操做呢?

 

二、桌面的啓動管理:

  回頭研究下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的產生緣由了。

 

原理剖析

  今後咱們能夠知道QQ安裝器其實也就是使用Intent來啓動其剛剛安裝的那個App,可是問題所在的是:他們的啓動Intent並無跟桌面的啓動Intent徹底一致!

咱們將桌面的Task記爲【TaskL】,QQ安裝器的Task記爲【TaskQ】,咱們應用的Task記爲【TaskA】,那麼分析以下:

 

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

打開QQ: L1Q1Q2 ---- Q2是安裝完畢後詢問是否啓動對應程序的Activity

點擊打開: L1Q1Q2A1A2 ---- A1是入口閃屏頁,A2是主頁Activity

返回桌面: Q1Q2A1A2L1 ---- 回到桌面頁,也就是L1前置

點擊A的圖標: Q1Q2L1A1A2A1 ---- 找到【TaskA】,挪到前臺,因爲比對Intent並非徹底一致,因此該請求是新啓動Activity,那麼把A1添加到對應的【TaskA】中

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

 

PS:這裏我稍微變種一下,由於通常咱們閃屏頁都是在啓動主頁後finish的,而主頁通常是singleTask模式

 

打開QQ: L1Q1Q2 ---- Q2是安裝完畢後詢問是否啓動對應程序的Activity

點擊打開: L1Q1Q2A2 ---- A1是入口閃屏頁,A2是主頁Activity,啓動後A1業務邏輯應該finish掉,因此從【TaskA】中挪去

返回桌面: Q1Q2A2L1 ---- 回到桌面頁,也就是L1前置

點擊A的圖標: Q1Q2L1A2A1 -> Q1Q2L1A2A1 ---- 找到【TaskA】,挪到前臺,因爲比對Intent並非徹底一致,因此該請求是新啓動Activity,那麼把A1添加到對應的【TaskA】中,而後A1所再一次觸發啓動主頁,可是主頁是singleTask模式,因此又回到了上次對應的A2主頁,因此現象爲再一次出現閃屏頁,而後回到原先的主頁界面。

 

解決思路

  一、讓騰訊那些第三方平臺修正其啓動Intent的設置,使其與原聲桌面啓動Intent保持徹底一致。(PS:基本不可能)

  二、自身業務代碼規避,咱們能夠知道,若是是多餘的閃屏頁入口Activity的話,其基本不可能位於Task的根部,而若是正常啓動的話,閃屏頁入口Activity一定在多對應的Task的根部位置,那麼咱們能夠從這個地方對於這個bug進行規避,方法就是在閃屏頁入口Activity的onCreate代碼加入以下一段代碼:

複製代碼
// 避免從桌面啓動程序後,會從新實例化入口類的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;
        }
    }
}
複製代碼

 問題解決!

 http://www.cnblogs.com/net168/p/5722752.html

相關文章
相關標籤/搜索