Android 源碼系列之從源碼的角度深入理解Activity的launchModel特性

        轉載請註明出處:http://blog.csdn.net/llew2011/article/details/52509515

        隨着公司新業務的起步由於原有APP_A的包已經很大了,所以上邊要求另外開發一款APP_B,要求是APP_A和APP_B賬號通用且兩個APP可以相互打開。賬號通用也就是說在APP_A上登錄了那麼打開APP_B也就默認是登錄狀態,這個實現也不復雜就不介紹了;APP相互打開本來也不是難事,但是在測試的過程中發現了一個之前沒有遇到的問題,現象如下圖的demo所示:


        運行現象是在APP_A中打開了APP_B後,這時候在APP_B中進行任何操作都是沒問題的,在APP_B不退出的情況下若摁了HOME鍵切換到桌面後此時再點擊APP_A的icon圖標打開APP_A時,發現界面竟然是APP_B的界面,當時感覺很詭異,是什麼原因導致出現這種現象呢?當時就琢磨着可能是APP_B運行在了APP_A的任務棧中了,於是開始排查代碼,在APP_B中響應APP_A的代碼如下所示:

<activity
    android:name="com.llew.wb.A"
    android:label="@string/app_name" >
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />

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

        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />

        <data android:scheme="llew" />
    </intent-filter>
</activity>
        由於我們APP_A和APP_B約定了相互打開採用scheme的形式,所以響應代碼看起來是沒有問題的,接着查看在APP_A中打開APP_B的代碼,如下所示:
public void openAPP_B1() {
	Uri uri = Uri.parse("llew://");
	Intent intent = new Intent(Intent.ACTION_VIEW, uri);
	startActivity(intent);
}
        這段就是打開我們APP_B的代碼,看上去也沒有什麼問題,但是爲什麼會出現上述現象呢?然後我就嘗試在openAPP_B中採用另外的方式,代碼如下:
public void openAPP_B2() {
	Intent intent = getPackageManager().getLaunchIntentForPackage("packageName");
	if(null != intent) {
		startActivity(intent);
	}
}

        方式二以前使用過並看過這塊相關源碼,所以首先就想到了通過PackageManager來獲取Intent來啓動我們的APP_B,運行程序後發現第二種方式是沒問題的,那也就是說在第二種中採用PackageManager獲取到的Intent肯定是和採用第一種方式獲取到的Intent是有區別的,那他們的區別在哪呢?先不說結論我們接着往下看,運行程序通過debug模式分別查看這兩種方式獲取到的Intent的不同之處:

        方式一的intent截圖如下所示:

        方式二的intent截圖如下所示:

        通過對比這兩種方式的Intent對象可以發現方式二中的intent對象包含了flg屬性,而該flg屬性的值恰好是Intent.FLAG_ACTIVITY_NEW_TASK的值,這時候豁然開朗了,原來方式二中的Intent添加了FLAG_ACTIVITY_NEW_TASK標記,也就是說採用方式一開打APP_B時的頁面是運行在APP_A的任務棧中,而通過方式二打開APP_B的頁面運行在了新的任務棧中。爲了證明通過方式一打開的APP_B的頁面是運行在APP_A的任務棧中,我們可以使用adb shell dumpsys activity activities 命令來查看Activity任務棧的情況,截圖如下:

        然後我們在方式一中的Intent也添加FLAG_ACTIVITY_NEW_TASK標記在運行一下,使用adb shell dumpsys activity activities 命令查看一下,截圖如下:

        出現以上問題的原因就是APP_B運行在了APP_A的任務棧中,解決方法也就是在啓動APP_B的時候讓APP_B運行在新的任務棧中,接下來順帶進入源碼看一看通過PackageManager獲取到的Intent對象在哪賦值的flag標記吧,在Activity中調用getPackageManager()輾轉調用的是其間接父類ContextWrapper的getPackageManager()的方法,源碼如下所示:

@Override
public PackageManager getPackageManager() {
	// mBase爲Context類型,其實現類爲ContextImpl
    return mBase.getPackageManager();
}
        ContextWrapper的getPackageManager()方法中調用的是Context的getPackageManager()同名方法,而mBase的實現類爲ContextImpl,所以我們直接查看ContextImpl的getPackageManager()方法,源碼如下:
@Override
public PackageManager getPackageManager() {
	// 如果mPackageManager非空就直接返回
    if (mPackageManager != null) {
        return mPackageManager;
    }

    // 通過ActivityThread獲取IPackageManager對象pm
    IPackageManager pm = ActivityThread.getPackageManager();
    if (pm != null) {
    	// 新建ApplicationPackageManager對象並返回
        // Doesn't matter if we make more than one instance.
        return (mPackageManager = new ApplicationPackageManager(this, pm));
    }

    return null;
}
        通過源碼我們知道getPackageManger()方法獲取的是ApplicationPackageManager對象,獲取Intent對象就是調用該對象的getLaunchIntentForPackage()方法,源碼如下:
@Override
public Intent getLaunchIntentForPackage(String packageName) {
    // First see if the package has an INFO activity; the existence of
    // such an activity is implied to be the desired front-door for the
    // overall package (such as if it has multiple launcher entries).
    Intent intentToResolve = new Intent(Intent.ACTION_MAIN);
    intentToResolve.addCategory(Intent.CATEGORY_INFO);
    intentToResolve.setPackage(packageName);
    List<ResolveInfo> ris = queryIntentActivities(intentToResolve, 0);

    // Otherwise, try to find a main launcher activity.
    if (ris == null || ris.size() <= 0) {
        // reuse the intent instance
        intentToResolve.removeCategory(Intent.CATEGORY_INFO);
        intentToResolve.addCategory(Intent.CATEGORY_LAUNCHER);
        intentToResolve.setPackage(packageName);
        ris = queryIntentActivities(intentToResolve, 0);
    }
    if (ris == null || ris.size() <= 0) {
        return null;
    }
    // 運行到這裏是查找到了符合條件的Intent了,新建Intent
    Intent intent = new Intent(intentToResolve);
    // 在這裏給Intent添加了我們期待的FLAG_ACTIVITY_NEW_TASK標籤
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    intent.setClassName(ris.get(0).activityInfo.packageName, ris.get(0).activityInfo.name);
    // 返回新建的Intent對象
    return intent;
}

        通過源碼我們看到在ApplicationPackageManager的getLaunchIntentForPackage()方法中給符合條件的Intent添加了FLAG_ACTIVITY_NEW_TASK標籤,而該標籤的作用就是爲目標Activity開啓新的任務棧並把目標Activity放到棧底。

        開始講解Activity的launchMode之前我們先提一下任務和返回棧的概念,以下部分內容參考自官方文檔

  • 任務和返回棧
            應用通常包含多個Activity。每個 Activity 均應圍繞用戶可以執行的特定操作設計,並且能夠啓動其他 Activity。 例如,電子郵件應用可能有一個 Activity 顯示新郵件的列表。用戶選擇某郵件時,會打開一個新 Activity 以查看該郵件。
            一個 Activity 甚至可以啓動設備上其他應用中存在的 Activity。例如,如果應用想要發送電子郵件,則可將 Intent 定義爲執行“發送”操作並加入一些數據,如電子郵件地址和電子郵件。 然後,系統將打開其他應用中聲明自己處理此類 Intent 的 Activity。在這種情況下, Intent 是要發送電子郵件,因此將啓動電子郵件應用的“撰寫”Activity(如果多個 Activity 支持相同 Intent,則系統會讓用戶選擇要使用的 Activity)。發送電子郵件時,Activity 將恢復,看起來好像電子郵件 Activity 是您的應用的一部分。 即使這兩個 Activity 可能來自不同的應用,但是 Android 仍會將 Activity 保留在相同的任務中,以維護這種無縫的用戶體驗。
            任務是指在執行特定作業時與用戶交互的一系列 Activity。 這些 Activity 按照各自的打開順序排列在堆棧(即“返回棧”)中。
            設備主屏幕是大多數任務的起點。當用戶觸摸應用啓動器中的圖標(或主屏幕上的快捷鍵)時,該應用的任務將出現在前臺。 如果應用不存在任務(應用最近未曾使用),則會創建一個新任務,並且該應用的“主”Activity 將作爲堆棧中的根 Activity 打開。
            當前 Activity 啓動另一個 Activity 時,該新 Activity 會被推送到堆棧頂部,成爲焦點所在。 前一個 Activity 仍保留在堆棧中,但是處於停止狀態。Activity 停止時,系統會保持其用戶界面的當前狀態。 用戶按“返回”按鈕時,當前 Activity 會從堆棧頂部彈出(Activity 被銷燬),而前一個 Activity 恢復執行(恢復其 UI 的前一狀態)。 堆棧中的 Activity 永遠不會重新排列,僅推入和彈出堆棧:由當前 Activity 啓動時推入堆棧;用戶使用“返回”按鈕退出時彈出堆棧。 因此,返回棧以“後進先出”對象結構運行。 圖 1 通過時間線顯示 Activity 之間的進度以及每個時間點的當前返回棧,直觀呈現了這種行爲。

            如果用戶繼續按“返回”,堆棧中的相應 Activity 就會彈出,以顯示前一個 Activity,直到用戶返回主屏幕爲止(或者,返回任務開始時正在運行的任意 Activity)。 當所有 Activity 均從堆棧中刪除後,任務即不復存在。

            
    由於返回棧中的 Activity 永遠不會重新排列,因此如果應用允許用戶從多個 Activity 中啓動特定 Activity,則會創建該 Activity 的新實例並推入堆棧中(而不是將 Activity 的任一先前實例置於頂部)。 因此,應用中的一個 Activity 可能會多次實例化(即使 Activity 來自不同的任務)。
            【注意:】後臺可以同時運行多個任務。但是,如果用戶同時運行多個後臺任務,則系統可能會開始銷燬後臺 Activity,以回收內存資源,從而導致 Activity 狀態丟失。

        好了,用了不小篇幅介紹了任務和返回棧的概念,若要改變返回棧的默認行爲,可通過Activity的launchMode以及Intent的Flag標籤,我們今天主要講解的是通過launchMode來改變任務棧的默認行爲,Android系統爲launchMode提供了四種機制,分別是standard,singleTop,singleTask,singleInstance,爲了方便查看任務棧的相關信息,這裏給大家說一個命令:adb shell dumpsys activity,如果有對該命令不熟悉的,請自行查閱並掌握。下面我們來逐一講解launchMode的各個屬性值。

  • standard
            該屬性是Activity默認情況下的啓動模式,也就是說我們如果沒有在manifest.xml中聲明Activity的launchMode屬性,系統會默認爲Activity配置成standard,每次啓動該Activity時系統都會在當前的任務棧中新建一個該Activity的實例並加入任務棧中。
    【例如:A和B都是standard】打開順序爲:A→B→B→B,則任務棧中的順序如下所示:

  • singleTop
            1、如果Activity的launchMode屬性定義成了singleTop,若在當前任務棧中已經存在該Activity的實例並且在棧頂位置,那再次打開該Activity都不會新建該Activity的實例,此時會回調該Activity的onNewIntent()方法。【注意:】如果Activity的launchMode屬性爲singleTop,則taskAffinity屬性無效。
            【例如:B爲singleTop,其它爲默認】打開順序爲:A→B→B→B,則任務棧中的順序如下所示:

            2、如果Activity的launchMode屬性定義成了singleTop,若在當前任務棧中已經存在該Activity的實例且不在棧頂,那此時會繼續新建該Activity的實例
            【例如:B爲singleTop,其它爲默認】打開順序爲:A→B→C→B→C→B→C→B

  • singleTask
            1、如果Activity的launchMode屬性定義成了singleTask,如果此時沒有聲明taskAffinity屬性(當不聲明taskAffinity屬性,那麼Activity就會以包名作爲其默認值)
            【例如:B爲singleTask,其它爲默認】打開順序爲:A→B→C→D→B

            2、如果Activity的launchMode屬性定義成了singleTask,如果此時聲明瞭taskAffinity屬性且該屬性不同於包名,則
            【例如:B爲singleTask,其它爲默認】打開順序爲:A→B

            【例如:B爲singleTask,其它爲默認】打開順序爲:A→B→C

            【例如:B爲singleTask,其它爲默認】打開順序爲:A→B→C→D

            【例如:B爲singleTask,其它爲默認】打開順序爲:A→B→C→D→B

             根據運行結果我們發現,當Activity的launchMode設置成singleTask,singTask保證了當前任務棧中只有一個該Activity的實例,若該Activity不在棧頂,則會清除該Activity之上的所有的Activity並回調該Activity的onNewIntent()方法,singleTask的使用小結如下所示:
    if( 發現一個 Task 的 affinity == Activity 的 affinity ){
        if(此 Activity 的實例已經在這個 Task 中){
            這個 Activity 啓動並且清除頂部的 Acitivity ,通過標識 CLEAR_TOP 
        } else {
            在這個 Task 中新建這個 Activity 實例
        }
    } else { // Task 的 affinity 屬性值與 Activity 不一樣
        新建一個 affinity 屬性值與之相等的 Task
        新建一個 Activity 的實例並且將其放入這個 Task 之中
    }
  • singleInstance
          1、singleInstance稍微比singleTask好理解,singleInstance的Activity只能在一個新的Task中並且這個Task中有且只能有這一個Activity,舉個栗子
             【例如:B爲singleInstance,其它爲默認】打開順序爲:A→B
            【例如:B爲singleInstance,其它爲默認】打開順序爲:A→B→C→D→C

        根據以上結果我們已經大致掌握了launchMode的各種特性了,爲了深刻理解需要小夥伴們自己動手實驗嘗試各種情況下的Activity的打開方式。本篇文章到此就結束了,感謝觀看(*^__^*) ……



        【參考文章:】

        1、https://developer.android.com/guide/components/tasks-and-back-stack.htm

        2、http://www.songzhw.com/2016/08/09/explain-activity-launch-mode-with-examples/



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