Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android 源碼系列之(九)從源碼的角度深入理解Activity的launchModel特性

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

編輯:關於Android編程

隨著公司新業務的起步由於原有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的代碼如下所示:


    
        

        
    
    
        

        
        

        
    
由於我們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 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. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved