1. 程式人生 > >【轉載】Android Bug分析系列:第三方平臺安裝app啟動後,home鍵回到桌面後點擊app啟動時會再次啟動入口類bug的原因剖析

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

特殊 返回 androidm android系統 圖片 管理 相關 OS 簡便

前言

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

  具體表現是:

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

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

  3、再點擊app的icon圖標,原諒耿直的我們都是覺得應該直接回到【主頁Activity】,但是結果卻是又一次觸發 【閃屏頁Activity】,亮瞎了24K鈦合金狗眼的我們覺得這玩法不對吧?

  4、然後,收拾收拾心情開始定位之路吧~

現象分析

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

  也就是說,app的整體交互邏輯並沒有特殊之處,並非業務邏輯導致的bug。那麽回顧下不同的地方,也就是啟動App的入口的區別了,一者是平常的桌面Icon圖標啟動,一者是QQ安裝這類第三方平臺啟動。我們都知道,桌面啟動的話也是通過startActivity這個api通過特定的Intent向ActivityManagerServer發起啟動任務;所以我們可以推導出QQ安裝啟動這類方式也是通過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的產生原因了。

原理剖析

  從此我們可以知道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主頁,所以現象為再一次出現閃屏頁,然後回到原先的主頁界面。

解決思路

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

  2、自身業務代碼規避,我們可以知道,如果是多余的閃屏頁入口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

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