1. 程式人生 > 其它 >Android四大元件之Activity(四)—— 啟動模式(launchMode)

Android四大元件之Activity(四)—— 啟動模式(launchMode)

技術標籤:android開發javaandroidjava移動開發

1、使用的是Android 7.1.2的原始碼:

https://pan.baidu.com/s/1XcVD68cC_2wKtm8jJkdNQA
przv

2、感謝IT先森的系列部落格:

Android應用程序建立流程大揭祕
Android四大元件之bindService原始碼實現詳解
Android四大元件之Activity啟動流程原始碼實現詳解概要
Activity啟動流程(一)發起端程序請求啟動目標Activity
Activity啟動流程(二)system_server程序處理啟動Activity請求
Activity啟動流程(三)-Activity Task排程演算法覆盤分析


Activity啟動流程(四)-Pause前臺顯示Activity,Resume目標Activity
Activity啟動流程(五)請求並建立目標Activity程序
Activity啟動流程(六)註冊目標Activity程序到system_server程序以及建立目標Activity程序Application
Activity啟動流程(七)初始化目標Activity並執行相關生命週期流程

疑問:
設定啟動模式? ActivityStack棧管理,根據啟動模式來判斷是否啟動棧
在ActivityStarter.startActivityUnChecked方法中進行處理

說實話,startActivityUnChecked函式中對棧處理的這部分內容我沒看懂,就是了解了對於棧處理的內容在這部分。

一、啟動模式(launchMode)

Activity在 AndroidManifest.xml 檔案中定義Activity時,可以通過launchMode屬性來指定這個Activity應該如何與任務進行關聯,取值有如下四種,不同的啟動模式在啟動 Activity時會執行不同的邏輯,系統會按不同的啟動模式將Activity存放到不同的 Activity 棧中:

  • standard(預設的啟動模式):
    LAUNCH_MULTIPLE,每次啟動新Activity,都會建立新的Activity
  • singleTop:
    LAUNCH_SINGLE_TOP,當啟動新Activity,如果在棧頂存在相同的Activity,則不會建立新的Activity,其餘情況同上。
    比如我們使用一款視訊APP觀看短視訊,視訊播放頁面同時還推薦了類似視訊
    當我們點選了推薦視訊,事件順序大概是:開啟視訊播放頁面 -> 點選推薦視訊 -> 複用當前頁面播放推薦視訊,同時重新整理推薦列表
    即棧頂物件可複用,沒必要新建一個例項。
  • singleTask:
    LAUNCH_SINGLE_TASK,當啟動新Activity,在棧中存在相同的Activity(可以是不在棧頂),則不會建立新的Activity,而是移除該Activity之上的所有Activity,其餘情況同上。
    比如我們使用一款購物APP購買商品,Activity開啟順序大概是:商品頁面 - 下單頁面 - 支付頁面 - 交易完成介面
    顯然,我們完成時,需要返回商品頁面,沒必要返回支付頁面,因為訂單已經結束,因此我們需要在回到商品頁面時銷燬下單和支付頁面
    即Activity頂部的任務已過期,沒必要再保留。
  • singleInstance:
    LAUNCH_SINGLE_INSTANCE,單例模式,每個Task棧只有一個Activity,其餘情況同上
    在一個新棧中建立該Activity的例項,並讓多個應用共享該棧中的該Activity例項。一旦該模式的Activity例項已經存在於某個棧中,任何應用再啟用該Activity時都會重用該棧中的例項( 會呼叫例項的 onNewIntent() )。其效果相當於多個應用共享一個應用,不管誰啟用該 Activity 都會進入同一個應用中。使用場景如鬧鈴提醒,將鬧鈴提醒與鬧鈴設定分離。singleInstance不要用於中間頁面,如果用於中間頁面,跳轉會有問題,比如:A -> B (singleInstance) -> C,完全退出後,在此啟動,首先開啟的是B。
//[ActivityInfo.java]
public class ActivityInfo extends ComponentInfo
        implements Parcelable {
    ...
    public static final int LAUNCH_MULTIPLE = 0;
    public static final int LAUNCH_SINGLE_TOP = 1;
    public static final int LAUNCH_SINGLE_TASK = 2;
    public static final int LAUNCH_SINGLE_INSTANCE = 3;
	...
}

二、啟動的Flag值

  • FLAG_ACTIVITY_NEW_TASK,將Activity放入一個新啟動的Task,注意屬性task:affinity
  • FLAG_ACTIVITY_CLEAR_TASK,啟動Activity時,將目標Activity關聯的Task清除,再啟動該Task,將該Activity放入該Task,也就是說,這個新啟動的activity變為了這個空Task的根activity.所有老的activity都結束掉。該flags跟FLAG_ACTIVITY_NEW_TASK配合使用
  • FLAG_ACTIVITY_CLEAR_TOP,啟動非棧頂Activity時,先清除該Activity之上的Activity。例如Task已有A、B、C三個Activity,啟動A,則清除B,C。類似於SingleTop
  • FLAG_ACTIVITY_PREVIOUS_IS_TOP,A->B->C,若B啟動C時用了這個標誌位,那在啟動時,B並不會被當作棧頂的Activity,而是用A做棧頂來啟動C,此過程中B充當一個跳轉頁面,典型的場景是在應用選擇頁面。如果在文字中點選一個網址要跳轉到瀏覽器,而系統中又裝了不止一個瀏覽器應用,此時會彈出應用選擇頁面。在應用選擇頁面選擇某一款瀏覽器啟動時,就會用到這個Flag。
  • START_FLAG_ONLY_IF_NEEDED,該flag表示只有在需要的時候才啟動目標Activity。也就是說如果呼叫者和被啟動的是一個,那麼就沒有必要去進行重複的步驟了

Android Intent的FLAG標誌詳解:https://www.jianshu.com/p/537aa221eec4

Activity的flag常用值:

intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

Android上進行多工處理:
點選Home鍵,長按Home鍵或通過其它方式可以看到當前啟動的任務。每個任務都具有自己的Activity堆疊。使用者返回主螢幕並選擇啟動任務A的應用,現在,任務A進入前臺,其堆疊中的所有三個Activity都完好如初,堆疊頂部的Activity恢復執行。此時,使用者仍可通過以下方式切換到任務B:①轉到主螢幕並選擇啟動該任務的應用圖示 ②從最近的應用中選擇該應用的任務。

瞭解任務和返回堆疊:
https://developer.android.google.cn/guide/components/activities/tasks-and-back-stack

疑問:使用intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)的話,回到主螢幕,顯示的還是隻有主程式這一個Task啊?
答:因為沒有使用 android:taskAffinity

三、AMS對Activity的4種啟動模式的處理方式

1、ActivityStarter.startActivityLocked

final int startActivityLocked(IApplicationThread caller, Intent intent, Intent ephemeralIntent,
String resolvedType, ActivityInfo aInfo, ResolveInfo rInfo,
IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
IBinder resultTo, String resultWho, int requestCode, int callingPid, int callingUid,
String callingPackage, int realCallingPid, int realCallingUid, int startFlags,
ActivityOptions options, boolean ignoreTargetSecurity, boolean componentSpecified,
ActivityRecord[] outActivity, ActivityStackSupervisor.ActivityContainer container,
TaskRecord inTask)

  • ActivityInfo中包含各種的activity資訊,都是宣告在AndroidManifest.xml檔案中的,比較重要的包括launchMode、theme、screenOrientation等。
  • ResolveInfo是一個容器類,裡面包含了ActivityInfo、ServiceInfo、ProviderInfo等成員變數來表示四大元件的資訊。activity和broadcast資訊都是用ActivityInfo來表示的。
  • IBinder resultTo是 發起端Activity的ActivityRecord物件中的Token,其Binder實體在AMS中,這個值是在Activity.startActivity中賦值傳遞過來的,具體:Activity中的mToken
  • inTask 指定待啟動的Activity的任務棧,此處為null

    startActivityLocked方法的主要邏輯如下:
  • 進一步對發起端的程序做一些許可權檢查,然後接著確定 sourceRecord 和 resultRecord 的值
  • 接著通過 上述確認的引數構建關於目標Activity的 ActivityRecord (經過複雜的判定,只是建立了一個ActivityRecord)
  • 建立 ActivityRecord 之後,也有一些處理,包括 AppSwitch,優先啟動之前被阻塞的 Activity,然後進入下一階段 startActivityUnchecked(…) ,從該方法名看出該做的檢查已經做完了,剩下的函式呼叫就不需要進行額外的檢查了(Unchecked),在分析Android原始碼中經常會看到類似的命名規則。
  • 呼叫startActivityUnchecked(),開展後續的Activity啟動

2、ActivityStarter.startActivityUnchecked

負責排程ActivityRecord和TaskRecord、resumed相關操作

該方法的具體作用:

  • 初始化Activity啟動狀態
  • 計算launchFlag
  • 計算呼叫者的ActivityStack
  • 檢查是否存在複用的TaskRecord
  • 對於存在複用的TaskRecord則進行相應的ActivityStack、TaskRecord的移動 (有難度)
  • 計算當前啟動Activity所屬的TaskRecord
  • 把當前啟動的Activity放到所屬TaskRecord的棧頂

大致程式碼:

ActivityStarter.startActivityUnchecked()
    setInitialState()  //給要啟動的ActivityRecord(mStartActivity)等賦值ActivityRecord mStartActivity = r;
    computeLaunchingTaskFlags()  //根據launchMode和 Intent 中的 FLAG_ACTIVITY_NEW_TASK 等 flag 綜合計算 Activity 的啟動模式,結果儲存在mLaunchFlags 中,計算的過程不僅要考慮目標 activity 的 launchMode ,也要考慮原來 Activity 的 launchMode 和 Intent 中所帶著的 Flag
    computeSourceStack()  //根據發起方ActivityRecord:mSourceRecord來找到源任務棧:mSourceTask
    getReusableIntentActivity()  //查詢可重用的Activity,只對啟動模式LAUNCH_SINGLE_INSTANCE和LAUNCH_SINGLE_TASK或者FLAG_ACTIVITY_NEW_TASK不為0的Activity才有用,對於standard的activity,該方法永遠返回null。
   
    //if(mReusedActivity != null)
    performClearTaskForReuseLocked() //遍歷TaskRecord物件例項中的ActivityRecord列表,然後根據一定的規則清除可複用的activity上面的activity
    
    //經過上面步驟後,不管是否進行了棧頂資料的清除,接下來就要將我們可以複用的Activity所在的TaskRecord移動到其所在的ActivityStack的頂部
    
    setTargetStackAndMoveToFrontIfNeeded() //將複用ActivityRecord所屬的TaskRecord和ActivityStack移動到頂端,必要時會進行task的清理工作
    	ActivityStack.moveTaskToFrontLocked()
    setTaskFromIntentActivity()
    ...
    //在同一個應用中從Activity A啟動 Activity B不會走此分支
	if (mStartActivity.resultTo == null && mInTask == null && !mAddingToTask
                && (mLaunchFlags & FLAG_ACTIVITY_NEW_TASK) != 0) {
         ··· ···
    }else if(mSourceRecord != null){
        // 不是新建task的,重用原activity的task
            final int result = setTaskFromSourceRecord();
			/*****************************************************************************/	
				//這裡小夥們就不要關注排版問題了,主要是為了演示整個流程,各位就將就一下 
			    private int setTaskFromSourceRecord() {
					//獲取啟動Activity的任務棧
			        final TaskRecord sourceTask = mSourceRecord.task;
			        //此時的發起端Actiivty所在的TaskRecord就是處於sourceStack棧頂,所以sourceStack.topTask就是要啟動的Activity所在的棧
			        //如果目標Activity不允許在螢幕上顯示或者源任務棧和目標任務不在同一個棧
			        final boolean moveStackAllowed = sourceTask.stack.topTask() != sourceTask;
					//獲取當前要啟動activity所屬的ActivityStack棧
			        if (moveStackAllowed) {//不會進入此分支						
						...
			        }
					
					//目標ActivityStack為空
			        if (mTargetStack == null) {
			            mTargetStack = sourceTask.stack;//進入此分支
			        } else if (mTargetStack != sourceTask.stack) {
			        	//把啟動方的任務棧繫結到目標ActivityStack上
						...
			        }
			        if (mDoResume) {
			            mTargetStack.moveToFront("sourceStackToFront");
			        }
			
					//獲取目標ActivityStack的頂部task
			        final TaskRecord topTask = mTargetStack.topTask();
			        if (topTask != sourceTask && !mAvoidMoveToFront) {//不會走入此分支
						
			        }
			
					//如果目標activity還沒有加入到棧中,而且啟動標誌設定了CLEAR_TOP,那麼我們將Activity新增到已經存在的任務棧中,並呼叫clear方法清空對應的activity
			        if (!mAddingToTask && (mLaunchFlags & FLAG_ACTIVITY_CLEAR_TOP) != 0) {//很明顯不會進入此分支
						...
			        } else if (!mAddingToTask && (mLaunchFlags & FLAG_ACTIVITY_REORDER_TO_FRONT) != 0) {//不會進入此分支
						...
			        }
			        mStartActivity.setTask(sourceTask, null);//設定目標Activity B的Task為A Activity所屬的Task

			        return START_SUCCESS;
			    }				
			/*****************************************************************************/						            
            if (result != START_SUCCESS) {
                return result;
            }
        } else if (mInTask != null) {//啟動時指定了目標棧(mInTask),ActivityRecord繫結到mInTask,此場景下不會進入此分支
			...
        } else {//不會進入此分支,忽略
			..,
        }		
		...
		/*把當前啟動的Activity加入TaskRecord以及繫結WindowManagerService*/
        mTargetStack.startActivityLocked(mStartActivity, newTask, mKeepCurTransition, mOptions);	
		/*****************************************************************************/	
			//這裡小夥們就不要關注排版問題了,主要是為了演示整個流程,各位就將就一下  
		    final void startActivityLocked(	ActivityRecord r, //此時的r為目標Activity
		    								boolean newTask, //newTask表示是否要建立Task,為true
		    								boolean keepCurTransition,
		            						ActivityOptions options) 
		   {
		        TaskRecord rTask = r.task;
		        final int taskId = rTask.taskId;
				
		        if (!r.mLaunchTaskBehind && (taskForIdLocked(taskId) == null || newTask)) {//不會進入此分支
					...
		        }
		        TaskRecord task = null;
		        if (!newTask) {//newTask為false會走入此分支
					...
		        }
		
				...
		
		        task = r.task;
		
				//將Activity移動到Stack的頂端 ====== 重點
		        task.addActivityToTop(r);
		        task.setFrontOfTask();
		
		        r.putInHistory();
		        if (!isHomeStack() || numActivities() > 0) {//會進入此分支,此時的ActivityStack不是HomeStack
					//這個地方很重要
		            addConfigOverride(r, task);
		        } else {//不會進入此分支
					...
		        }
		        ...
		    }	
    }
   
    ···
  
    if (mDoResume) {
            if (!mLaunchTaskBehind) {
			/*
			  *設定當前focused,因為經過以上幾步,啟動的activity已經轉移到
			  *棧頂端,這時候設定AMS當前focused的Activity
			  *另外呼叫這個函式也會有ActivityStack、Task棧的移動,即呼叫各自棧把當
			  *前正在啟動的Activity所屬的Task、ActivityStack移動到棧頂
			  */
                mService.setFocusedActivityLocked(mStartActivity, "startedActivity");
            }
            final ActivityRecord topTaskActivity = mStartActivity.task.topRunningActivityLocked();
            if (!mTargetStack.isFocusable()//當前的目標Stack被設定成了焦點所以不會走此分支
                    || (topTaskActivity != null && topTaskActivity.mTaskOverlay
                    && mStartActivity != topTaskActivity)) {
                    ...
            } else {
            	//開始resume,詳見 ======8========
                mSupervisor.resumeFocusedStackTopActivityLocked(mTargetStack, mStartActivity,
                        mOptions);
            }
        } else {
            mTargetStack.addRecentActivityLocked(mStartActivity);
        }

    return START_SUCCESS;
        ...

其中,computeLaunchingTaskFlags():根據發起端/目的端的launchMode和以及Intent中的攜帶的FLAG_ACTIVITY_NEW_TASK等flag綜合計算activity的啟動模式或者說調整啟動目標Activiyt的啟動模式。

Activity Task排程演算法覆盤分析中分析了三種情況:

1、從Launcher桌面第一次啟動應用時的任務排程情況

任務排程時會建立新task,並將新的ActivityRecord加入這個新的task,然後將task放入合適的Stack的棧頂

2、應用內Activity跳轉時的任務排程情況

任務排程時會將新的ActivityRecord加入已有的task,然後將該ActivityRecord移動到Task頂端,然後將task放入合適的Stack的棧頂

3、 然後按Home鍵,再開啟應用程式時的排程情況:

任務排程時會先找到已有的相關task,並顯示棧頂的Activity