Android14 - AMS之Activity启动过程(2)

Android14 - AMS之Activity启动过程(1)-CSDN博客
Android14 - AMS之Activity启动过程(3)-CSDN博客
上篇梳理到:

TaskDisplayArea和Task的复用与创建

TaskDisplayArea

executeRequest后,随后调用startActivityUnchecked,进而调用startActivityInner。

platform/frameworks/base/services/core/java/com/android/server/wm/ActivityStarter.java

复制代码
private` `int` `startActivityUnchecked(final ActivityRecord r,` `ActivityRecord sourceRecord,` `IVoiceInteractionSession voiceSession,` `IVoiceInteractor voiceInteractor,` `int startFlags,` `ActivityOptions options,` `Task inTask,` `TaskFragment inTaskFragment, @BalCode int balCode,` `NeededUriGrants intentGrants,` `int realCallingUid)` `{`
    `...`
`    result =` `startActivityInner(r, sourceRecord, voiceSession, voiceInteractor, startFlags, options, inTask, inTaskFragment, balCode, intentGrants, realCallingUid);`
`}`

`int` `startActivityInner(final ActivityRecord r,` `ActivityRecord sourceRecord,` 
            `IVoiceInteractionSession voiceSession,` `IVoiceInteractor voiceInteractor,`
            `int startFlags,` `ActivityOptions options,` `Task inTask,`
            `TaskFragment inTaskFragment, @BalCode int balCode,`
            `NeededUriGrants intentGrants,` `int realCallingUid)` `{`
        `// 位置1:初始化启动参数`
        `setInitialState(r, options, inTask, inTaskFragment, startFlags, sourceRecord,`
`                voiceSession, voiceInteractor, balCode, realCallingUid);`
        `computeLaunchingTaskFlags();`
`        mIntent.setFlags(mLaunchFlags);`
        
        `...`
        `// 位置2:mPreferredTaskDisplayArea为RootWindowContainer.getDefaultTaskDisplayArea()`
`        final Task prevTopRootTask = mPreferredTaskDisplayArea.getFocusedRootTask();`
`        final Task prevTopTask = prevTopRootTask !=` `null` `? prevTopRootTask.getTopLeafTask()` `:` `null;`
`        final Task reusedTask =` `getReusableTask();`
        `...`
`        final Task targetTask = reusedTask !=` `null` `? reusedTask :` `computeTargetTask();`
`        final boolean newTask = targetTask ==` `null;`
`        mTargetTask = targetTask;`    
        
        `// 位置3:计算WindowingMode`
        `computeLaunchParams(r, sourceRecord, targetTask);`
        
        `int startResult =` `isAllowedToStart(r, newTask, targetTask);`        
        
        `...`
`        final ActivityRecord targetTaskTop = newTask`
                `?` `null` `: targetTask.getTopNonFinishingActivity();`
        `if` `(targetTaskTop !=` `null)` `{`
            `// Removes the existing singleInstance activity in another task (if any) while`
            `// launching a singleInstance activity on sourceRecord's task.`
            `if` `(LAUNCH_SINGLE_INSTANCE == mLaunchMode && mSourceRecord !=` `null`
                    `&& targetTask == mSourceRecord.getTask())` `{`
`                final ActivityRecord activity = mRootWindowContainer.findActivity(mIntent,`
`                        mStartActivity.info,` `false);`
                `if` `(activity !=` `null` `&& activity.getTask()` `!= targetTask)` `{`
`                    activity.destroyIfPossible("Removes redundant singleInstance");`
                `}`
            `}`
            `recordTransientLaunchIfNeeded(targetTaskTop);`
            `// Recycle the target task for this launch.`
`            startResult =` `recycleTask(targetTask, targetTaskTop, reusedTask, intentGrants);`
            `if` `(startResult != START_SUCCESS)` `{`
                `return startResult;`
            `}`
        `}` `else` `{`
`            mAddingToTask =` `true;`
        `}`

        `if` `(mTargetRootTask ==` `null)` `{`
`            mTargetRootTask =` `getOrCreateRootTask(mStartActivity, mLaunchFlags, targetTask,`
`                    mOptions);`
        `}`
        `if` `(newTask)` `{`
`            final Task taskToAffiliate =` `(mLaunchTaskBehind && mSourceRecord !=` `null)`
                    `? mSourceRecord.getTask()` `:` `null;`
            `setNewTask(taskToAffiliate);`
        `}` `else` `if` `(mAddingToTask)` `{`
            `addOrReparentStartingActivity(targetTask,` `"adding to task");`
        `}`
        `...`
`        mTargetRootTask.startActivityLocked(mStartActivity, topRootTask, newTask, isTaskSwitch,`
`                mOptions, sourceRecord);`
    `// **** 位置4`
    `if` `(mDoResume)` `{`
`		final ActivityRecord topTaskActivity = startedTask.topRunningActivityLocked();`
		`if` `(!mTargetRootTask.isTopActivityFocusable()`
				`||` `(topTaskActivity !=` `null` `&& topTaskActivity.isTaskOverlay()`
				`&& mStartActivity != topTaskActivity))` `{`
			`// If the activity is not focusable, we can't resume it, but still would like to`
			`// make sure it becomes visible as it starts (this will also trigger entry`
			`// animation). An example of this are PIP activities.`
			`// Also, we don't want to resume activities in a task that currently has an overlay`
			`// as the starting activity just needs to be in the visible paused state until the`
			`// over is removed.`
			`// Passing {@code null} as the start parameter ensures all activities are made`
			`// visible.`
`			mTargetRootTask.ensureActivitiesVisible(null` `/* starting */,`
					`0` `/* configChanges */,` `!PRESERVE_WINDOWS);`
			`// Go ahead and tell window manager to execute app transition for this activity`
			`// since the app transition will not be triggered through the resume channel.`
`			mTargetRootTask.mDisplayContent.executeAppTransition();`
		`}` `else` `{`
			`// If the target root-task was not previously focusable (previous top running`
			`// activity on that root-task was not visible) then any prior calls to move the`
			`// root-task to the will not update the focused root-task.  If starting the new`
			`// activity now allows the task root-task to be focusable, then ensure that we`
			`// now update the focused root-task accordingly.`
			`if` `(!mAvoidMoveToFront && mTargetRootTask.isTopActivityFocusable()`
					`&&` `!mRootWindowContainer.isTopDisplayFocusedRootTask(mTargetRootTask))` `{`
`				mTargetRootTask.moveToFront("startActivityInner");`
			`}`
`			mRootWindowContainer.resumeFocusedTasksTopActivities(`
`					mTargetRootTask, mStartActivity, mOptions, mTransientLaunch);`
		`}`
	`}`
`	mRootWindowContainer.updateUserRootTask(mStartActivity.mUserId, mTargetRootTask);`

	`// Update the recent tasks list immediately when the activity starts`
`	mSupervisor.mRecentTasks.add(startedTask);`
`	mSupervisor.handleNonResizableTaskIfNeeded(startedTask,`
`			mPreferredWindowingMode, mPreferredTaskDisplayArea, mTargetRootTask);`

	`// If Activity's launching into PiP, move the mStartActivity immediately to pinned mode.`
	`// Note that mStartActivity and source should be in the same Task at this point.`
	`if` `(mOptions !=` `null` `&& mOptions.isLaunchIntoPip()`
			`&& sourceRecord !=` `null` `&& sourceRecord.getTask()` `== mStartActivity.getTask()`
			`&& balCode != BAL_BLOCK)` `{`
`		mRootWindowContainer.moveActivityToPinnedRootTask(mStartActivity,`
`				sourceRecord,` `"launch-into-pip");`
	`}`

	`return START_SUCCESS;`
`}

mPreferredTaskDisplayArea的赋值来自setInitialState,

复制代码
platform/frameworks/base/services/core/java/com/android/server/wm/ActivityStarter.java`

`private void setInitialState(ActivityRecord r, ActivityOptions options, Task inTask,`
`            TaskFragment inTaskFragment, int startFlags,`
`            ActivityRecord sourceRecord, IVoiceInteractionSession voiceSession,`
`            IVoiceInteractor voiceInteractor, @BalCode int balCode, int realCallingUid) {`
            
    `mStartActivity = r;`
`	mIntent = r.intent;`
`	mOptions = options;`
`	mCallingUid = r.launchedFromUid;`
`	mRealCallingUid = realCallingUid;`
`	mSourceRecord = sourceRecord;`
`	mSourceRootTask = mSourceRecord != null ? mSourceRecord.getRootTask() : null;`
`	mVoiceSession = voiceSession;`
`	mVoiceInteractor = voiceInteractor;`
`	mBalCode = balCode;`

`	mLaunchParams.reset();`

`    mSupervisor.getLaunchParamsController().calculate(inTask, r.info.windowLayout, r, sourceRecord, options, mRequest, PHASE_DISPLAY, mLaunchParams);`
`    mPreferredTaskDisplayArea = mLaunchParams.hasPreferredTaskDisplayArea()`
`                ? mLaunchParams.mPreferredTaskDisplayArea`
`                : mRootWindowContainer.getDefaultTaskDisplayArea();`
`    mPreferredWindowingMode = mLaunchParams.mWindowingMode;`

`    mLaunchMode = r.launchMode;`

`    mLaunchFlags = adjustLaunchFlagsToDocumentMode(`
`                r, LAUNCH_SINGLE_INSTANCE == mLaunchMode,`
`                LAUNCH_SINGLE_TASK == mLaunchMode, mIntent.getFlags());

首先注意mStartActivity = r;的赋值。mStartActivity就是新new的ActivityRecord后面会用到。

mSupervisor.getLaunchParamsController()来自ActivityTaskSupervisor的初始化,调用其calculate方法。注意,这里传入的PHASE_DISPLAY:

复制代码
platform/frameworks/base/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java`

`public void initialize() {`
`    mLaunchParamsController = new LaunchParamsController(mService, mLaunchParamsPersister);`
`    mLaunchParamsController.registerDefaultModifiers(this);`
`}`

`void registerDefaultModifiers(ActivityTaskSupervisor supervisor) {`
`        // {@link TaskLaunchParamsModifier} handles window layout preferences.`
`        registerModifier(new TaskLaunchParamsModifier(supervisor));`
`        if (DesktopModeLaunchParamsModifier.isDesktopModeSupported()) {`
`            // {@link DesktopModeLaunchParamsModifier} handles default task size changes`
`            registerModifier(new DesktopModeLaunchParamsModifier());`
`        }`
`    }

我们看到,new了一个LaunchParamsController后,注册了Modifiers为new TaskLaunchParamsModifier。

LaunchParamsController的calculate代码:

复制代码
platform/frameworks/base/services/core/java/com/android/server/wm/LaunchParamsController.java`

`void` `calculate(Task task,` `WindowLayout layout,` `ActivityRecord activity,` `ActivityRecord source,`
            `ActivityOptions options, @Nullable Request request,` `int phase,` `LaunchParams result)` `{`
     `if` `(task !=` `null` `|| activity !=` `null)` `{`
`		mPersister.getLaunchParams(task, activity, result);`
	`}`

	`// We start at the last registered {@link LaunchParamsModifier} as this represents`
	`// The modifier closest to the product level. Moving back through the list moves closer to`
	`// the platform logic.`
	`for` `(int i = mModifiers.size()` `-` `1; i >=` `0;` `--i)` `{`
`		mTmpCurrent.set(result);`
`		mTmpResult.reset();`
`		final LaunchParamsModifier modifier = mModifiers.get(i);`

		`switch(modifier.onCalculate(task, layout, activity, source, options, request, phase,`
`				mTmpCurrent, mTmpResult))` `{`
			`case RESULT_SKIP:`
				`// Do not apply any results when we are told to skip`
				`continue;`
			`case RESULT_DONE:`
				`// Set result and return immediately.`
`				result.set(mTmpResult);`
				`return;`
			`case RESULT_CONTINUE:`
				`// Set result and continue`
`				result.set(mTmpResult);`
				`break;`
		`}`
	`}`

	`if` `(activity !=` `null` `&& activity.requestedVrComponent !=` `null)` `{`
		`// Check if the Activity is a VR activity. If so, it should be launched in main display.`
`		result.mPreferredTaskDisplayArea = mService.mRootWindowContainer`
				`.getDefaultTaskDisplayArea();`
	`}` `else` `if` `(mService.mVr2dDisplayId != INVALID_DISPLAY)` `{`
		`// Get the virtual display ID from ActivityTaskManagerService. If that's set we`
		`// should always use that.`
`		result.mPreferredTaskDisplayArea = mService.mRootWindowContainer`
				`.getDisplayContent(mService.mVr2dDisplayId).getDefaultTaskDisplayArea();`
	`}`           
`}

mModifiers目前只有一个成员,就是TaskLaunchParamsModifier,这里调用其onCalculate:

复制代码
@Override`
    `public` `int` `onCalculate(@Nullable` `Task task,` `@Nullable` `ActivityInfo.WindowLayout layout,`
            `@Nullable` `ActivityRecord activity,` `@Nullable` `ActivityRecord source,`
            `@Nullable` `ActivityOptions options,` `@Nullable` `Request request,` `int phase,`
            `LaunchParams currentParams,` `LaunchParams outParams)` `{`
        `initLogBuilder(task, activity);`
        `final` `int result =` `calculate(task, layout, activity, source, options, request, phase,`
`                currentParams, outParams);`
        `outputLog();`
        `return result;`
    `}`

    `private` `int` `calculate(@Nullable` `Task task,` `@Nullable` `ActivityInfo.WindowLayout layout,`
            `@Nullable` `ActivityRecord activity,` `@Nullable` `ActivityRecord source,`
            `@Nullable` `ActivityOptions options,` `@Nullable` `Request request,` `int phase,`
            `LaunchParams currentParams,` `LaunchParams outParams)` `{`
        `final` `ActivityRecord root;`
        `if` `(task !=` `null)` `{`
`            root = task.getRootActivity()` `==` `null` `? activity : task.getRootActivity();`
        `}` `else` `{`
`            root = activity;`
        `}`

        `if` `(root ==` `null)` `{`
            `// There is a case that can lead us here. The caller is moving the top activity that is`
            `// in a task that has multiple activities to PIP mode. For that the caller is creating a`
            `// new task to host the activity so that we only move the top activity to PIP mode and`
            `// keep other activities in the previous task. There is no point to apply the launch`
            `// logic in this case.`
            `return RESULT_SKIP;`
        `}`

        `// STEP 1: Determine the suggested display area to launch the activity/task.`
        `final` `TaskDisplayArea suggestedDisplayArea =` `getPreferredLaunchTaskDisplayArea(task,`
`                options, source, currentParams, activity, request);`
`        outParams.mPreferredTaskDisplayArea = suggestedDisplayArea;`
        `final` `DisplayContent display = suggestedDisplayArea.mDisplayContent;`
        `if` `(DEBUG)` `{`
            `appendLog("display-id="` `+ display.getDisplayId()`
                    `+` `" task-display-area-windowing-mode="` `+ suggestedDisplayArea.getWindowingMode()`
                    `+` `" suggested-display-area="` `+ suggestedDisplayArea);`
        `}`

        `if` `(phase == PHASE_DISPLAY)` `{`
            `return RESULT_CONTINUE;`
        `}`
        `...`
`}

由于我们一开始的场景是从Context启动一个Activity,source、option、inTask等参数都是空,currentParams(从ActivityStarter.setInitialState构建而来)的mPreferredTaskDisplayArea也是空,所以getPreferredLaunchTaskDisplayArea方法最终会从getFallbackDisplayAreaForActivity方法获取mPreferredTaskDisplayArea:

复制代码
/**`
` * Calculates the default {@link TaskDisplayArea} for a task. We attempt to put the activity`
` * within the same display area if possible. The strategy is to find the display in the`
` * following order:`
` *`
` * <ol>`
` *     <li>The display area of the top activity from the launching process will be used</li>`
` *     <li>The display area of the top activity from the real launching process will be used`
` *     </li>`
` *     <li>Default display area from the associated root window container.</li>`
` * </ol>`
` * @param activityRecord the activity being started`
` * @param request optional {@link Request} made to start the activity record`
` * @return {@link TaskDisplayArea} to house the task`
` */`
`private` `TaskDisplayArea` `getFallbackDisplayAreaForActivity(`
		`@NonNull` `ActivityRecord activityRecord,` `@Nullable` `Request request)` `{`

	`WindowProcessController controllerFromLaunchingRecord = mSupervisor.mService`
			`.getProcessController(activityRecord.launchedFromPid,`
`					activityRecord.launchedFromUid);`
	`final` `TaskDisplayArea displayAreaForLaunchingRecord = controllerFromLaunchingRecord ==` `null`
			`?` `null` `: controllerFromLaunchingRecord.getTopActivityDisplayArea();`
	`if` `(displayAreaForLaunchingRecord !=` `null)` `{`
		`return displayAreaForLaunchingRecord;`
	`}`

	`WindowProcessController controllerFromProcess = mSupervisor.mService.getProcessController(`
`			activityRecord.getProcessName(), activityRecord.getUid());`
	`final` `TaskDisplayArea displayAreaForRecord = controllerFromProcess ==` `null` `?` `null`
			`: controllerFromProcess.getTopActivityDisplayArea();`
	`if` `(displayAreaForRecord !=` `null)` `{`
		`return displayAreaForRecord;`
	`}`

	`WindowProcessController controllerFromRequest = request ==` `null` `?` `null` `: mSupervisor`
			`.mService.getProcessController(request.realCallingPid, request.realCallingUid);`
	`final` `TaskDisplayArea displayAreaFromSourceProcess = controllerFromRequest ==` `null` `?` `null`
			`: controllerFromRequest.getTopActivityDisplayArea();`
	`if` `(displayAreaFromSourceProcess !=` `null)` `{`
		`return displayAreaFromSourceProcess;`
	`}`

	`return mSupervisor.mRootWindowContainer.getDefaultTaskDisplayArea();`
`}

首先会根据activityRecord.launchedFromPid,也就是CallingPid(在ActivityStarter.executeReauest()时设置),或activityRecord.getProcessName,或者realCallingPid,去获取其所在的TopActivity所属的DisplayArea。比如如果是从Laucnher启动的一个新的Activity,那么此时会使用Launcher顶部Activity所属的DisplayArea。如果以上都没有找到, 则使用mRootWindowContainer的默认DefaultTaskDisplayArea。通常这个DefaultTaskDisplayArea是对应一个屏幕的DisplayContent。

回到calculate(方法。现在我们得到了suggestedDisplayArea,赋值给outParams.mPreferredTaskDisplayArea。由于上面我们传入的phase是PHASE_DISPLAY,所以下面直接返回return RESULT_CONTINUE;

再继续返回LaunchParamsController的calculate(..), RESULT_CONTINUE对应的逻辑是result.set(mTmpResult);,就是将刚才outParams的值设置到in-out型参数result,返回给上层。而后面的activity.requestedVrComponent 等判断逻辑,都是跟VR场景有关的,不探讨。

回到ActivityStarter的startActivityInner, 假如我们现在只有一个物理屏幕,那么final Task prevTopRootTask = mPreferredTaskDisplayArea.getFocusedRootTask();返回的是默认DisplayContent的getFocusedRootTask:

复制代码
platform/frameworks/base/services/core/java/com/android/server/wm/DisplayContent.java`

`@Nullable`
`Task getFocusedRootTask() {`
`    return getItemFromTaskDisplayAreas(TaskDisplayArea::getFocusedRootTask);`
`}
复制代码
platform/frameworks/base/services/core/java/com/android/server/wm/WindowContainer.java`

`/**`
` * Finds the first non {@code null} return value from calling the callback on all`
` * {@link TaskDisplayArea} at or below this container.`
` * @param callback Applies on each {@link TaskDisplayArea} found and stops the search if it`
` *                 returns non {@code null}.`
` * @param traverseTopToBottom If {@code true}, traverses the hierarchy from top-to-bottom in`
` *                            terms of z-order, else from bottom-to-top.`
` * @return the first returned object that is not {@code null}. Returns {@code null} if not`
` *         found.`
` */`
`@Nullable`
`<R> R getItemFromTaskDisplayAreas(Function<TaskDisplayArea, R> callback,`
`		boolean traverseTopToBottom) {`
`	int childCount = mChildren.size();`
`	int i = traverseTopToBottom ? childCount - 1 : 0;`
`	while (i >= 0 && i < childCount) {`
`		R result = (R) mChildren.get(i)`
`				.getItemFromTaskDisplayAreas(callback, traverseTopToBottom);`
`		if (result != null) {`
`			return result;`
`		}`
`		i += traverseTopToBottom ? -1 : 1;`
`	}`
`	return null;`
`}
复制代码
platform/frameworks/base/services/core/java/com/android/server/wm/DisplayArea.java`

`@Nullable`
`@Override`
`<R> R getItemFromTaskDisplayAreas(Function<TaskDisplayArea, R> callback,`
`		boolean traverseTopToBottom) {`
`	// Only DisplayArea of Type.ANY may contain TaskDisplayArea as children.`
`	if (mType != DisplayArea.Type.ANY) {`
`		return null;`
`	}`

`	int childCount = mChildren.size();`
`	int i = traverseTopToBottom ? childCount - 1 : 0;`
`	while (i >= 0 && i < childCount) {`
`		T child = mChildren.get(i);`
`		// Only traverse if the child is a DisplayArea.`
`		if (child.asDisplayArea() != null) {`
`			R result = (R) child.asDisplayArea()`
`					.getItemFromTaskDisplayAreas(callback, traverseTopToBottom);`
`			if (result != null) {`
`				return result;`
`			}`
`		}`
`		i += traverseTopToBottom ? -1 : 1;`
`	}`
`	return null;`
`}
复制代码
platform/frameworks/base/services/core/java/com/android/server/wm/TaskDisplayArea.java`

`@Nullable`
`@Override`
`<R> R getItemFromTaskDisplayAreas(Function<TaskDisplayArea, R> callback,`
`		boolean traverseTopToBottom) {`
`	if (traverseTopToBottom) {`
`		final R item = super.getItemFromTaskDisplayAreas(callback, traverseTopToBottom);`
`		return item != null ? item : callback.apply(this);`
`	} else {`
`		final R item = callback.apply(this);`
`		return item != null`
`				? item`
`				: super.getItemFromTaskDisplayAreas(callback, traverseTopToBottom);`
`	}`
`}

getItemFromTaskDisplayAreas是一个递归查询某TaskDisplayArea的首个非空child的方法。其参数callback是一个Function类型的参数,用于将TaskDisplayArea转换为R类型。

首先,getItemFromTaskDisplayAreas是在WindowContainer声明的,DisplayArea、TaskDisplayArea重载了getItemFromTaskDisplayAreas。所有的窗口类型(WindowToken、DisplayContent、DisplayArea、TaskDisplayArea等)都是WindowContainer的子类,而DisplayContent继承于DisplayArea,DisplayArea继承WindowContainer。所以这里首先执行的是DisplayArea的getItemFromTaskDisplayAreas。而TaskDisplayArea是DisplayContent的child,所以接下来会调用TaskDisplayArea的getItemFromTaskDisplayAreas,此时在递归深度优先获取到首个非空child后,会调用callbak。

由于getFocusedRootTask()传入的callback是TaskDisplayArea::getFocusedRootTask,这里是一个lamda语法,看下这个方法:

复制代码
platform/frameworks/base/services/core/java/com/android/server/wm/TaskDisplayArea.java`

`Task getFocusedRootTask() {`
`	if (mPreferredTopFocusableRootTask != null) {`
`		return mPreferredTopFocusableRootTask;`
`	}`

`	for (int i = mChildren.size() - 1; i >= 0; --i) {`
`		final WindowContainer child = mChildren.get(i);`
`		if (child.asTaskDisplayArea() != null) {`
`			final Task rootTask = child.asTaskDisplayArea().getFocusedRootTask();`
`			if (rootTask != null) {`
`				return rootTask;`
`			}`
`			continue;`
`		}`

`		final Task rootTask = mChildren.get(i).asTask();`
`		if (rootTask.isFocusableAndVisible()) {`
`			return rootTask;`
`		}`
`	}`

`	return null;`
`}

如果该TaskDisplayArea还有子TaskDisplayArea,那么会找子TaskDisplayArea的Task,否则就遍历当前TaskDisplayArea的子Task,找到isFocusableAndVisible的,返回。

这里的逻辑有点绕,关于窗口体系,请见Android12 - WMS之WindowContainer树(DisplayArea)-CSDN博客

复用Task

Task有嵌套关系。RootTask包含LeafTask。

回到startActivityInner,拿到prevTopRootTask后,Task prevTopTask = prevTopRootTask != null ? prevTopRootTask.getTopLeafTask() : null;

复制代码
platform/frameworks/base/services/core/java/com/android/server/wm/Task.java`

`/** Return the top-most leaf-task under this one, or this task if it is a leaf. */`
`public Task getTopLeafTask() {`
`	for (int i = mChildren.size() - 1; i >= 0; --i) {`
`		final Task child = mChildren.get(i).asTask();`
`		if (child == null) continue;`
`		return child.getTopLeafTask();`
`	}`
`	return this;`
`}

如果该prevTopRootTask有Child Task,则返回最顶部(最后加入的)Task,否则就返回自身。那么prevTopTask就是当前最顶部task。

final Task reusedTask = getReusableTask();获取可重复使用的Task。

复制代码
`
`/**`
` * Decide whether the new activity should be inserted into an existing task. Returns null`
` * if not or an ActivityRecord with the task into which the new activity should be added.`
` */`
`private Task getReusableTask() {`
`	// If a target task is specified, try to reuse that one`
`	if (mOptions != null && mOptions.getLaunchTaskId() != INVALID_TASK_ID) {`
`		Task launchTask = mRootWindowContainer.anyTaskForId(mOptions.getLaunchTaskId());`
`		if (launchTask != null) {`
`			return launchTask;`
`		}`
`		return null;`
`	}`

`	// We may want to try to place the new activity in to an existing task.  We always`
`	// do this if the target activity is singleTask or singleInstance; we will also do`
`	// this if NEW_TASK has been requested, and there is not an additional qualifier telling`
`	// us to still place it in a new task: multi task, always doc mode, or being asked to`
`	// launch this as a new task behind the current one.`
`	boolean putIntoExistingTask = ((mLaunchFlags & FLAG_ACTIVITY_NEW_TASK) != 0 &&`
`			(mLaunchFlags & FLAG_ACTIVITY_MULTIPLE_TASK) == 0)`
`			|| isLaunchModeOneOf(LAUNCH_SINGLE_INSTANCE, LAUNCH_SINGLE_TASK);`
`	// If bring to front is requested, and no result is requested and we have not been given`
`	// an explicit task to launch in to, and we can find a task that was started with this`
`	// same component, then instead of launching bring that one to the front.`
`	putIntoExistingTask &= mInTask == null && mStartActivity.resultTo == null;`
`	ActivityRecord intentActivity = null;`
`	if (putIntoExistingTask) {`
`		if (LAUNCH_SINGLE_INSTANCE == mLaunchMode) {`
`			// There can be one and only one instance of single instance activity in the`
`			// history, and it is always in its own unique task, so we do a special search.`
`			intentActivity = mRootWindowContainer.findActivity(mIntent, mStartActivity.info,`
`				   mStartActivity.isActivityTypeHome());`
`		} else if ((mLaunchFlags & FLAG_ACTIVITY_LAUNCH_ADJACENT) != 0) {`
`			// For the launch adjacent case we only want to put the activity in an existing`
`			// task if the activity already exists in the history.`
`			intentActivity = mRootWindowContainer.findActivity(mIntent, mStartActivity.info,`
`					!(LAUNCH_SINGLE_TASK == mLaunchMode));`
`		} else {`
`			// Otherwise find the best task to put the activity in.`
`			intentActivity =`
`					mRootWindowContainer.findTask(mStartActivity, mPreferredTaskDisplayArea);`
`		}`
`	}`

`	if (intentActivity != null && mLaunchMode == LAUNCH_SINGLE_INSTANCE_PER_TASK`
`			&& !intentActivity.getTask().getRootActivity().mActivityComponent.equals(`
`			mStartActivity.mActivityComponent)) {`
`		// The task could be selected due to same task affinity. Do not reuse the task while`
`		// starting the singleInstancePerTask activity if it is not the task root activity.`
`		intentActivity = null;`
`	}`

`	if (intentActivity != null`
`			&& (mStartActivity.isActivityTypeHome() || intentActivity.isActivityTypeHome())`
`			&& intentActivity.getDisplayArea() != mPreferredTaskDisplayArea) {`
`		// Do not reuse home activity on other display areas.`
`		intentActivity = null;`
`	}`

`	return intentActivity != null ? intentActivity.getTask() : null;`
`}

如果mOptions.getLaunchTaskId指定了复用的TaskId(通过在startActivity时指定ActivityOption),则直接返回。否则根据Intent flag判断是否需要使用已存在的task。

如果getReusableTask返回空,则继续使用computeTargetTask查看是否有可复用的task

复制代码
platform/frameworks/base/services/core/java/com/android/server/wm/ActivityStarter.java`

`/** Returns the leaf task where the target activity may be placed. */`
`private Task computeTargetTask() {`
`	if (mStartActivity.resultTo == null && mInTask == null && !mAddingToTask`
`			&& (mLaunchFlags & FLAG_ACTIVITY_NEW_TASK) != 0) {`
`		// A new task should be created instead of using existing one.`
`		return null;`
`	} else if (mSourceRecord != null) {`
`		return mSourceRecord.getTask();`
`	} else if (mInTask != null) {`
`		// The task is specified from AppTaskImpl, so it may not be attached yet.`
`		if (!mInTask.isAttached()) {`
`			// Attach the task to display area. Ignore the returned root task (though usually`
`			// they are the same) because "target task" should be leaf task.`
`			getOrCreateRootTask(mStartActivity, mLaunchFlags, mInTask, mOptions);`
`		}`
`		return mInTask;`
`	} else {`
`		final Task rootTask = getOrCreateRootTask(mStartActivity, mLaunchFlags, null /* task */,`
`				mOptions);`
`		final ActivityRecord top = rootTask.getTopNonFinishingActivity();`
`		if (top != null) {`
`			return top.getTask();`
`		} else {`
`			// Remove the root task if no activity in the root task.`
`			rootTask.removeIfPossible("computeTargetTask");`
`		}`
`	}`
`	return null;`
`}

从之前的参数整理部分,我们在我们启动的这个场景,resultTo和mInTask是空,此时mAddingToTask是false。如果用的是FLAG_ACTIVITY_NEW_TASK标签,则直接就放回null了。本方法的其他的条件中,也是想复用task。所以这个方法本意还是想复用。如果没有复用的话,mTargetRootTask还是空。

更新WindowingMode和mBounds

回到startActivityInner继续往下,在【位置3】处,调用了如下代码:

computeLaunchParams(r, sourceRecord, targetTask);

复制代码
platform/frameworks/base/services/core/java/com/android/server/wm/ActivityStarter.java`

`private void computeLaunchParams(ActivityRecord r, ActivityRecord sourceRecord,`
`		Task targetTask) {`
`	mSupervisor.getLaunchParamsController().calculate(targetTask, r.info.windowLayout, r,`
`			sourceRecord, mOptions, mRequest, PHASE_BOUNDS, mLaunchParams);`
`	mPreferredTaskDisplayArea = mLaunchParams.hasPreferredTaskDisplayArea()`
`			? mLaunchParams.mPreferredTaskDisplayArea`
`			: mRootWindowContainer.getDefaultTaskDisplayArea();`
`	mPreferredWindowingMode = mLaunchParams.mWindowingMode;`
`}

这里看到,会再次调用mSupervisor.getLaunchParamsController().calculate, 二此时传入的phase是PHASE_BOUNDS。那么我们回到TaskLaunchParamsModifier.calculate方法。上次我们走到PHASE_DISPLAY后return了。这次继续往下走:

复制代码
platform/frameworks/base/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java`

`private int calculate(@Nullable Task task, @Nullable ActivityInfo.WindowLayout layout,`
`		@Nullable ActivityRecord activity, @Nullable ActivityRecord source,`
`		@Nullable ActivityOptions options, @Nullable Request request, int phase,`
`		LaunchParams currentParams, LaunchParams outParams) {`
`	final ActivityRecord root;`
`	if (task != null) {`
`		root = task.getRootActivity() == null ? activity : task.getRootActivity();`
`	} else {`
`		root = activity;`
`	}`

`	if (root == null) {`
`		// There is a case that can lead us here. The caller is moving the top activity that is`
`		// in a task that has multiple activities to PIP mode. For that the caller is creating a`
`		// new task to host the activity so that we only move the top activity to PIP mode and`
`		// keep other activities in the previous task. There is no point to apply the launch`
`		// logic in this case.`
`		return RESULT_SKIP;`
`	}`

`	// STEP 1: Determine the suggested display area to launch the activity/task.`
`	final TaskDisplayArea suggestedDisplayArea = getPreferredLaunchTaskDisplayArea(task,`
`			options, source, currentParams, activity, request);`
`	outParams.mPreferredTaskDisplayArea = suggestedDisplayArea;`
`	final DisplayContent display = suggestedDisplayArea.mDisplayContent;`
`	if (DEBUG) {`
`		appendLog("display-id=" + display.getDisplayId()`
`				+ " task-display-area-windowing-mode=" + suggestedDisplayArea.getWindowingMode()`
`				+ " suggested-display-area=" + suggestedDisplayArea);`
`	}`

`	if (phase == PHASE_DISPLAY) {`
`		return RESULT_CONTINUE;`
`	}`
`   // *** 本次我们继续往下执行`
`	// STEP 2: Resolve launch windowing mode.`
`	// STEP 2.1: Determine if any parameter can specify initial bounds/windowing mode. That`
`	// might be the launch bounds from activity options, or size/gravity passed in layout. It`
`	// also treats the launch windowing mode in options and source activity windowing mode in`
`	// some cases as a suggestion for future resolution.`
`	int launchMode = options != null ? options.getLaunchWindowingMode()`
`			: WINDOWING_MODE_UNDEFINED;`
`	// In some cases we want to use the source's windowing mode as the default value, e.g. when`
`	// source is a freeform window in a fullscreen display launching an activity on the same`
`	// display.`
`	if (launchMode == WINDOWING_MODE_UNDEFINED`
`			&& canInheritWindowingModeFromSource(display, suggestedDisplayArea, source)) {`
`		// The source's windowing mode may be different from its task, e.g. activity is set`
`		// to fullscreen and its task is pinned windowing mode when the activity is entering`
`		// pip.`
`		launchMode = source.getTask().getWindowingMode();`
`		if (DEBUG) {`
`			appendLog("inherit-from-source="`
`					+ WindowConfiguration.windowingModeToString(launchMode));`
`		}`
`	}`
`	// If the launch windowing mode is still undefined, inherit from the target task if the`
`	// task is already on the right display area (otherwise, the task may be on a different`
`	// display area that has incompatible windowing mode or the task organizer request to`
`	// disassociate the leaf task if relaunched and reparented it to TDA as root task).`
`	if (launchMode == WINDOWING_MODE_UNDEFINED`
`			&& task != null && task.getTaskDisplayArea() == suggestedDisplayArea`
`			&& !task.getRootTask().mReparentLeafTaskIfRelaunch) {`
`		launchMode = task.getWindowingMode();`
`		if (DEBUG) {`
`			appendLog("inherit-from-task="`
`					+ WindowConfiguration.windowingModeToString(launchMode));`
`		}`
`	}`
`	// hasInitialBounds is set if either activity options or layout has specified bounds. If`
`	// that's set we'll skip some adjustments later to avoid overriding the initial bounds.`
`	boolean hasInitialBounds = false;`
`	// hasInitialBoundsForSuggestedDisplayAreaInFreeformWindow is set if the outParams.mBounds`
`	// is set with the suggestedDisplayArea. If it is set, but the eventual TaskDisplayArea is`
`	// different, we should recalculating the bounds.`
`	boolean hasInitialBoundsForSuggestedDisplayAreaInFreeformWindow = false;`
`	// Note that initial bounds needs to be set to fullscreen tasks too as it's used as restore`
`	// bounds.`
`	final boolean canCalculateBoundsForFullscreenTask =`
`			canCalculateBoundsForFullscreenTask(suggestedDisplayArea, launchMode);`
`	final boolean canApplyFreeformWindowPolicy =`
`			canApplyFreeformWindowPolicy(suggestedDisplayArea, launchMode);`
`	final boolean canApplyWindowLayout = layout != null`
`			&& (canApplyFreeformWindowPolicy || canCalculateBoundsForFullscreenTask);`
`	final boolean canApplyBoundsFromActivityOptions =`
`			mSupervisor.canUseActivityOptionsLaunchBounds(options)`
`					&& (canApplyFreeformWindowPolicy`
`					|| canApplyPipWindowPolicy(launchMode)`
`					|| canCalculateBoundsForFullscreenTask);`

`	if (canApplyBoundsFromActivityOptions) {`
`		hasInitialBounds = true;`
`		// |launchMode| at this point can be fullscreen, PIP, MultiWindow, etc. Only set`
`		// freeform windowing mode if appropriate by checking |canApplyFreeformWindowPolicy|.`
`		launchMode = launchMode == WINDOWING_MODE_UNDEFINED && canApplyFreeformWindowPolicy`
`				? WINDOWING_MODE_FREEFORM`
`				: launchMode;`
`		outParams.mBounds.set(options.getLaunchBounds());`
`		if (DEBUG) appendLog("activity-options-bounds=" + outParams.mBounds);`
`	} else if (canApplyWindowLayout) {`
`		mTmpBounds.set(currentParams.mBounds);`
`		getLayoutBounds(suggestedDisplayArea, root, layout, mTmpBounds);`
`		if (!mTmpBounds.isEmpty()) {`
`			launchMode = canApplyFreeformWindowPolicy ? WINDOWING_MODE_FREEFORM : launchMode;`
`			outParams.mBounds.set(mTmpBounds);`
`			hasInitialBounds = true;`
`			hasInitialBoundsForSuggestedDisplayAreaInFreeformWindow = true;`
`			if (DEBUG) appendLog("bounds-from-layout=" + outParams.mBounds);`
`		} else {`
`			if (DEBUG) appendLog("empty-window-layout");`
`		}`
`	} else if (launchMode == WINDOWING_MODE_MULTI_WINDOW`
`			&& options != null && options.getLaunchBounds() != null) {`
`		// TODO: Investigate whether we can migrate this clause to the`
`		//  |canApplyBoundsFromActivityOptions| case above.`
`		outParams.mBounds.set(options.getLaunchBounds());`
`		hasInitialBounds = true;`
`		if (DEBUG) appendLog("multiwindow-activity-options-bounds=" + outParams.mBounds);`
`	}`

`	// STEP 2.2: Check if previous modifier or the controller (referred as "callers" below) has`
`	// some opinions on launch mode and launch bounds. If they have opinions and there is no`
`	// initial bounds set in parameters. Note the check on display ID is also input param`
`	// related because we always defer to callers' suggestion if there is no specific display ID`
`	// in options or from source activity.`
`	//`
`	// If opinions from callers don't need any further resolution, we try to honor that as is as`
`	// much as possible later.`

`	// Flag to indicate if current param needs no further resolution. It's true it current`
`	// param isn't freeform mode, or it already has launch bounds.`
`	boolean fullyResolvedCurrentParam = false;`
`	// We inherit launch params from previous modifiers or LaunchParamsController if options,`
`	// layout and display conditions are not contradictory to their suggestions. It's important`
`	// to carry over their values because LaunchParamsController doesn't automatically do that.`
`	// We only check if display matches because display area can be changed later.`
`	if (!currentParams.isEmpty() && !hasInitialBounds`
`			&& (currentParams.mPreferredTaskDisplayArea == null`
`				|| currentParams.mPreferredTaskDisplayArea.getDisplayId()`
`					== display.getDisplayId())) {`
`		// Only set windowing mode if display is in freeform. If the display is in fullscreen`
`		// mode we should only launch a task in fullscreen mode.`
`		if (currentParams.hasWindowingMode()`
`				&& suggestedDisplayArea.inFreeformWindowingMode()) {`
`			launchMode = currentParams.mWindowingMode;`
`			fullyResolvedCurrentParam = launchMode != WINDOWING_MODE_FREEFORM;`
`			if (DEBUG) {`
`				appendLog("inherit-" + WindowConfiguration.windowingModeToString(launchMode));`
`			}`
`		}`

`		if (!currentParams.mBounds.isEmpty()) {`
`			// Carry over bounds from callers regardless of launch mode because bounds is still`
`			// used to restore last non-fullscreen bounds when launch mode is not freeform.`
`			outParams.mBounds.set(currentParams.mBounds);`
`			fullyResolvedCurrentParam = true;`
`			if (launchMode == WINDOWING_MODE_FREEFORM) {`
`				if (DEBUG) appendLog("inherit-bounds=" + outParams.mBounds);`
`			}`
`		}`
`	}`

`	// STEP 2.3: Adjust launch parameters as needed for freeform display. We enforce the`
`	// policies related to unresizable apps here. If an app is unresizable and the freeform`
`	// size-compat mode is enabled, it can be launched in freeform depending on other properties`
`	// such as orientation. Otherwise, the app is forcefully launched in maximized. The rest of`
`	// this step is to define the default policy when there is no initial bounds or a fully`
`	// resolved current params from callers.`

`	// hasInitialBoundsForSuggestedDisplayAreaInFreeformMode is set if the outParams.mBounds`
`	// is set with the suggestedDisplayArea. If it is set, but the eventual TaskDisplayArea is`
`	// different, we should recalcuating the bounds.`
`	boolean hasInitialBoundsForSuggestedDisplayAreaInFreeformMode = false;`
`	if (suggestedDisplayArea.inFreeformWindowingMode()) {`
`		if (launchMode == WINDOWING_MODE_PINNED) {`
`			if (DEBUG) appendLog("picture-in-picture");`
`		} else if (!root.isResizeable()) {`
`			if (shouldLaunchUnresizableAppInFreeform(root, suggestedDisplayArea, options)) {`
`				launchMode = WINDOWING_MODE_FREEFORM;`
`				if (outParams.mBounds.isEmpty()) {`
`					getTaskBounds(root, suggestedDisplayArea, layout, launchMode,`
`							hasInitialBounds, outParams.mBounds);`
`					hasInitialBoundsForSuggestedDisplayAreaInFreeformMode = true;`
`				}`
`				if (DEBUG) appendLog("unresizable-freeform");`
`			} else {`
`				launchMode = WINDOWING_MODE_FULLSCREEN;`
`				outParams.mBounds.setEmpty();`
`				if (DEBUG) appendLog("unresizable-forced-maximize");`
`			}`
`		}`
`	} else {`
`		if (DEBUG) appendLog("non-freeform-task-display-area");`
`	}`

    `// 更新WindoingMode,集成TaskDisplayArea的WindowMode`
`	// If launch mode matches display windowing mode, let it inherit from display.`
`	outParams.mWindowingMode = launchMode == suggestedDisplayArea.getWindowingMode()`
`			? WINDOWING_MODE_UNDEFINED : launchMode;`

`	if (phase == PHASE_WINDOWING_MODE) {`
`		return RESULT_CONTINUE;`
`	}`

`	// STEP 3: Finalize the display area. Here we allow WM shell route all launches that match`
`	// certain criteria to specific task display areas.`
`	final int resolvedMode = (launchMode != WINDOWING_MODE_UNDEFINED) ? launchMode`
`			: suggestedDisplayArea.getWindowingMode();`
`	TaskDisplayArea taskDisplayArea = suggestedDisplayArea;`
`	// If launch task display area is set in options we should just use it. We assume the`
`	// suggestedDisplayArea has the right one in this case.`
`	if (options == null || (options.getLaunchTaskDisplayArea() == null`
`			&& options.getLaunchTaskDisplayAreaFeatureId() == FEATURE_UNDEFINED)) {`
`		final int activityType =`
`				mSupervisor.mRootWindowContainer.resolveActivityType(root, options, task);`
`		display.forAllTaskDisplayAreas(displayArea -> {`
`			final Task launchRoot = displayArea.getLaunchRootTask(`
`					resolvedMode, activityType, null /* ActivityOptions */,`
`					null /* sourceTask*/, 0 /* launchFlags */);`
`			if (launchRoot == null) {`
`				return false;`
`			}`
`			mTmpDisplayArea = displayArea;`
`			return true;`
`		});`
`		// We may need to recalculate the bounds and the windowing mode if the new`
`		// TaskDisplayArea is different from the suggested one we used to calculate the two`
`		// configurations.`
`		if (mTmpDisplayArea != null && mTmpDisplayArea != suggestedDisplayArea) {`
`			outParams.mWindowingMode = (launchMode == mTmpDisplayArea.getWindowingMode())`
`					? WINDOWING_MODE_UNDEFINED : launchMode;`
`			if (hasInitialBoundsForSuggestedDisplayAreaInFreeformWindow) {`
`				outParams.mBounds.setEmpty();`
`				getLayoutBounds(mTmpDisplayArea, root, layout, outParams.mBounds);`
`				hasInitialBounds = !outParams.mBounds.isEmpty();`
`			} else if (hasInitialBoundsForSuggestedDisplayAreaInFreeformMode) {`
`				outParams.mBounds.setEmpty();`
`				getTaskBounds(root, mTmpDisplayArea, layout, launchMode,`
`						hasInitialBounds, outParams.mBounds);`
`			}`
`		}`

`		if (mTmpDisplayArea != null) {`
`			taskDisplayArea = mTmpDisplayArea;`
`			mTmpDisplayArea = null;`
`			appendLog("overridden-display-area=["`
`					+ WindowConfiguration.activityTypeToString(activityType) + ", "`
`					+ WindowConfiguration.windowingModeToString(resolvedMode) + ", "`
`					+ taskDisplayArea + "]");`
`		}`
`	}`
`	appendLog("display-area=" + taskDisplayArea);`
`	outParams.mPreferredTaskDisplayArea = taskDisplayArea;`

`	if (phase == PHASE_DISPLAY_AREA) {`
`		return RESULT_CONTINUE;`
`	}`

`	// STEP 4: Determine final launch bounds based on resolved windowing mode and activity`
`	// requested orientation. We set bounds to empty for fullscreen mode and keep bounds as is`
`	// for all other windowing modes that's not freeform mode. One can read comments in`
`	// relevant methods to further understand this step.`
`	//`
`	// We skip making adjustments if the params are fully resolved from previous results.`
`	if (fullyResolvedCurrentParam) {`
`		if (resolvedMode == WINDOWING_MODE_FREEFORM) {`
`			// Make sure bounds are in the displayArea.`
`			if (currentParams.mPreferredTaskDisplayArea != taskDisplayArea) {`
`				adjustBoundsToFitInDisplayArea(taskDisplayArea, layout, outParams.mBounds);`
`			}`
`			// Even though we want to keep original bounds, we still don't want it to stomp on`
`			// an existing task.`
`			adjustBoundsToAvoidConflictInDisplayArea(taskDisplayArea, outParams.mBounds);`
`		}`
`	} else {`
`		if (source != null && source.inFreeformWindowingMode()`
`				&& resolvedMode == WINDOWING_MODE_FREEFORM`
`				&& outParams.mBounds.isEmpty()`
`				&& source.getDisplayArea() == taskDisplayArea) {`
`			// Set bounds to be not very far from source activity.`
`			cascadeBounds(source.getConfiguration().windowConfiguration.getBounds(),`
`					taskDisplayArea, outParams.mBounds);`
`		}`
`		getTaskBounds(root, taskDisplayArea, layout, resolvedMode, hasInitialBounds,`
`				outParams.mBounds);`
`	}`
`	return RESULT_CONTINUE;`
`}

首先,对outParams.mBounds进行了设置

outParams.mWindowingMode = launchMode == suggestedDisplayArea.getWindowingMode()

? WINDOWING_MODE_UNDEFINED : launchMode;

则对mWindowingMode进行了设置。这里沿用的是上层TaskDisplayArea的launchMode。

关于TaskDisplayArea的launchMode, 默认值来自DisplayConent的构造方法。在创建DisplayConent时,会调用setWindowingMode(WINDOWING_MODE_FULLSCREEN);

TaskDisplayArea 创建Root Task

到目前,如果没有复用的话,mTargetRootTask还是空。所以会通过下面代码来创建新Task

复制代码
if (mTargetRootTask == null) {`
`	mTargetRootTask = getOrCreateRootTask(mStartActivity, mLaunchFlags, targetTask,`
`			mOptions);`
`}

getOrCreateRootTask会调用mRootWindowContainer.getOrCreateRootTask(r, aOptions, task, sourceTask, onTop, mLaunchParams, launchFlags);来创建Task:

复制代码
Task getOrCreateRootTask(@Nullable ActivityRecord r,`
`	@Nullable ActivityOptions options, @Nullable Task candidateTask,`
`	@Nullable Task sourceTask, boolean onTop,`
`	@Nullable LaunchParamsController.LaunchParams launchParams, int launchFlags) {`
`// First preference goes to the launch root task set in the activity options.`
`if (options != null) {`
`	final Task candidateRoot = Task.fromWindowContainerToken(options.getLaunchRootTask());`
`	if (candidateRoot != null && canLaunchOnDisplay(r, candidateRoot)) {`
`		return candidateRoot;`
`	}`
`}`
`// *** 位置1`
`// Next preference goes to the task id set in the activity options.`
`if (options != null) {`
`	final int candidateTaskId = options.getLaunchTaskId();`
`	if (candidateTaskId != INVALID_TASK_ID) {`
`		// Temporarily set the task id to invalid in case in re-entry.`
`		options.setLaunchTaskId(INVALID_TASK_ID);`
`		final Task task = anyTaskForId(candidateTaskId,`
`				MATCH_ATTACHED_TASK_OR_RECENT_TASKS_AND_RESTORE, options, onTop);`
`		options.setLaunchTaskId(candidateTaskId);`
`		if (canLaunchOnDisplay(r, task)) {`
`			return task.getRootTask();`
`		}`
`	}`
`}`
`// *** 位置2`
`// Next preference goes to the TaskDisplayArea candidate from launchParams`
`// or activity options.`
`TaskDisplayArea taskDisplayArea = null;`
`if (launchParams != null && launchParams.mPreferredTaskDisplayArea != null) {`
`	taskDisplayArea = launchParams.mPreferredTaskDisplayArea;`
`} else if (options != null) {`
`	final WindowContainerToken daToken = options.getLaunchTaskDisplayArea();`
`	taskDisplayArea = daToken != null`
`			? (TaskDisplayArea) WindowContainer.fromBinder(daToken.asBinder()) : null;`
`	if (taskDisplayArea == null) {`
`		final int launchDisplayId = options.getLaunchDisplayId();`
`		if (launchDisplayId != INVALID_DISPLAY) {`
`			final DisplayContent displayContent = getDisplayContent(launchDisplayId);`
`			if (displayContent != null) {`
`				taskDisplayArea = displayContent.getDefaultTaskDisplayArea();`
`			}`
`		}`
`	}`
`}`

`// *** 位置3`
`final int activityType = resolveActivityType(r, options, candidateTask);`
`// *** 位置4`
`if (taskDisplayArea != null) {`
`	if (canLaunchOnDisplay(r, taskDisplayArea.getDisplayId())) {`
		`// *** 位置5`
        `return taskDisplayArea.getOrCreateRootTask(r, options, candidateTask,`
`				sourceTask, launchParams, launchFlags, activityType, onTop);`
`	} else {`
`		taskDisplayArea = null;`
`	}`
`}`

`// Give preference to the root task and display of the input task and activity if they`
`// match the mode we want to launch into.`
`Task rootTask = null;`
`if (candidateTask != null) {`
`	rootTask = candidateTask.getRootTask();`
`}`
`if (rootTask == null && r != null) {`
`	rootTask = r.getRootTask();`
`}`
`int windowingMode = launchParams != null ? launchParams.mWindowingMode`
`		: WindowConfiguration.WINDOWING_MODE_UNDEFINED;`
`if (rootTask != null) {`
`	taskDisplayArea = rootTask.getDisplayArea();`
`	if (taskDisplayArea != null`
`			&& canLaunchOnDisplay(r, taskDisplayArea.mDisplayContent.mDisplayId)) {`
`		if (windowingMode == WindowConfiguration.WINDOWING_MODE_UNDEFINED) {`
`			windowingMode = taskDisplayArea.resolveWindowingMode(r, options, candidateTask);`
`		}`
`		// Always allow organized tasks that created by organizer since the activity type`
`		// of an organized task is decided by the activity type of its top child, which`
`		// could be incompatible with the given windowing mode and activity type.`
`		if (rootTask.isCompatible(windowingMode, activityType)`
`				|| rootTask.mCreatedByOrganizer) {`
`			return rootTask;`
`		}`
`	} else {`
`		taskDisplayArea = null;`
`	}`

`}`

`// Falling back to default task container`
`if (taskDisplayArea == null) {`
`	taskDisplayArea = getDefaultTaskDisplayArea();`
`}`
`return taskDisplayArea.getOrCreateRootTask(r, options, candidateTask, sourceTask,`
`		launchParams, launchFlags, activityType, onTop);`
`}

这里是创建一个RootTask。

首先看位置1,candidateTaskId是空,就不复用了。位置2,launchParams.mPreferredTaskDisplayArea非空,为当前默认的DisplayContent下的TaskDisplayArea,于是taskDisplayArea的到赋值。位置3, activityType返回的是ACTIVITY_TYPE_STANDARD。

复制代码
platform/frameworks/base/services/core/java/com/android/server/wm/RootWindowContainer.java`

`int resolveActivityType(@Nullable ActivityRecord r, @Nullable ActivityOptions options,`
`		@Nullable Task task) {`
`	// Preference is given to the activity type for the activity then the task since the type`
`	// once set shouldn't change.`
`	int activityType = r != null ? r.getActivityType() : ACTIVITY_TYPE_UNDEFINED;`
`	if (activityType == ACTIVITY_TYPE_UNDEFINED && task != null) {`
`		activityType = task.getActivityType();`
`	}`
`	if (activityType != ACTIVITY_TYPE_UNDEFINED) {`
`		return activityType;`
`	}`
`	if (options != null) {`
`		activityType = options.getLaunchActivityType();`
`	}`
`	return activityType != ACTIVITY_TYPE_UNDEFINED ? activityType : ACTIVITY_TYPE_STANDARD;`
`}

位置4,canLaunchOnDisplay的检查,最终调用ActivityTaskSupervisor的canPlaceEntityOnDisplay:

复制代码
platform/frameworks/base/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java`

`private boolean canPlaceEntityOnDisplay(int displayId, int callingPid, int callingUid,`
`		Task task, ActivityInfo activityInfo) {`
`	if (displayId == DEFAULT_DISPLAY) {`
`		// No restrictions for the default display.`
`		return true;`
`	}`
`	if (!mService.mSupportsMultiDisplay) {`
`		// Can't launch on secondary displays if feature is not supported.`
`		return false;`
`	}`

`	if (!isCallerAllowedToLaunchOnDisplay(callingPid, callingUid, displayId, activityInfo)) {`
`		// Can't place activities to a display that has restricted launch rules.`
`		// In this case the request should be made by explicitly adding target display id and`
`		// by caller with corresponding permissions. See #isCallerAllowedToLaunchOnDisplay().`
`		return false;`
`	}`

`	final DisplayContent displayContent =`
`			mRootWindowContainer.getDisplayContentOrCreate(displayId);`
`	if (displayContent != null) {`
`		final ArrayList<ActivityInfo> activities = new ArrayList<>();`
`		if (activityInfo != null) {`
`			activities.add(activityInfo);`
`		}`
`		if (task != null) {`
`			task.forAllActivities((r) -> {`
`				activities.add(r.info);`
`			});`
`		}`
`		return displayContent.mDwpcHelper.canContainActivities(activities,`
`					displayContent.getWindowingMode());`
`	}`

`	return true;`
`}

这里是针对多屏幕情况下,Activity是否可在对应屏幕上启动的合法性检查。如果只有一块屏幕,返回true。

所以,最终会在位置5调用taskDisplayArea.getOrCreateRootTask来创建Root Task:

复制代码
platform/frameworks/base/services/core/java/com/android/server/wm/TaskDisplayArea.java`

`Task getOrCreateRootTask(@Nullable ActivityRecord r, @Nullable ActivityOptions options,`
`		@Nullable Task candidateTask, @Nullable Task sourceTask,`
`		@Nullable LaunchParams launchParams, int launchFlags, int activityType, boolean onTop) {`
`	int windowingMode = WINDOWING_MODE_UNDEFINED;`
`	if (launchParams != null) {`
`		// If launchParams isn't null, windowing mode is already resolved.`
`		windowingMode = launchParams.mWindowingMode;`
`	} else if (options != null) {`
`		// If launchParams is null and options isn't let's use the windowing mode in the`
`		// options.`
`		windowingMode = options.getLaunchWindowingMode();`
`	}`
`	// Validate that our desired windowingMode will work under the current conditions.`
`	// UNDEFINED windowing mode is a valid result and means that the new root task will inherit`
`	// it's display's windowing mode.`
`	windowingMode = validateWindowingMode(windowingMode, r, candidateTask);`
`	return getOrCreateRootTask(windowingMode, activityType, onTop, candidateTask, sourceTask,`
`			options, launchFlags);`
`}`

`/**`
` * When two level tasks are required for given windowing mode and activity type, returns an`
` * existing compatible root task or creates a new one.`
` * For one level task, the candidate task would be reused to also be the root task or create`
` * a new root task if no candidate task.`
` *`
` * @param windowingMode The windowing mode the root task should be created in.`
` * @param activityType  The activityType the root task should be created in.`
` * @param onTop         If true the root task will be created at the top of the display,`
` *                      else at the bottom.`
` * @param candidateTask The possible task the activity might be launched in. Can be null.`
` * @param sourceTask    The task requesting to start activity. Used to determine which of the`
` *                      adjacent roots should be launch root of the new task. Can be null.`
` * @param options       The activity options used to the launch. Can be null.`
` * @param launchFlags   The launch flags for this launch.`
` * @return The root task to use for the launch.`
` * @see #getRootTask(int, int)`
` */`
`Task getOrCreateRootTask(int windowingMode, int activityType, boolean onTop,`
`		@Nullable Task candidateTask, @Nullable Task sourceTask,`
`		@Nullable ActivityOptions options, int launchFlags) {`
`	final int resolvedWindowingMode =`
`			windowingMode == WINDOWING_MODE_UNDEFINED ? getWindowingMode() : windowingMode;`
`	// Need to pass in a determined windowing mode to see if a new root task should be created,`
`	// so use its parent's windowing mode if it is undefined.`
`	if (!alwaysCreateRootTask(resolvedWindowingMode, activityType)) {`
`		Task rootTask = getRootTask(resolvedWindowingMode, activityType);`
`		if (rootTask != null) {`
`			return rootTask;`
`		}`
`	} else if (candidateTask != null) {`
`		final int position = onTop ? POSITION_TOP : POSITION_BOTTOM;`
`		final Task launchParentTask = getLaunchRootTask(resolvedWindowingMode, activityType,`
`				options, sourceTask, launchFlags, candidateTask);`
`		if (launchParentTask != null) {`
`			if (candidateTask.getParent() == null) {`
`				launchParentTask.addChild(candidateTask, position);`
`			} else if (candidateTask.getParent() != launchParentTask) {`
`				candidateTask.reparent(launchParentTask, position);`
`			}`
`		} else if (candidateTask.getDisplayArea() != this`
`				|| candidateTask.getRootTask().mReparentLeafTaskIfRelaunch) {`
`			if (candidateTask.getParent() == null) {`
`				addChild(candidateTask, position);`
`			} else {`
`				candidateTask.reparent(this, onTop);`
`			}`
`		}`
`		// Update windowing mode if necessary, e.g. launch into a different windowing mode.`
`		if (windowingMode != WINDOWING_MODE_UNDEFINED && candidateTask.isRootTask()`
`				&& candidateTask.getWindowingMode() != windowingMode) {`
`			candidateTask.mTransitionController.collect(candidateTask);`
`			candidateTask.setWindowingMode(windowingMode);`
`		}`
`		return candidateTask.getRootTask();`
`	}`
`	return new Task.Builder(mAtmService)`
`			.setWindowingMode(windowingMode)`
`			.setActivityType(activityType)`
`			.setOnTop(onTop)`
`			.setParent(this)`
`			.setSourceTask(sourceTask)`
`			.setActivityOptions(options)`
`			.setLaunchFlags(launchFlags)`
`			.build();`
`}

首先我们知道,本文场景下,launchParams.mWindowingMode之前的代码赋值为了WINDOWING_MODE_FULLSCREEN, 因此这里windowingMode为WINDOWING_MODE_FULLSCREEN。所以接下来alwaysCreateRootTask这里:

复制代码
platform/frameworks/base/services/core/java/com/android/server/wm/DisplayContent.java`

`static` `boolean` `alwaysCreateRootTask(int windowingMode, int activityType)` `{`
	`// Always create a root task for fullscreen, freeform, and multi windowing`
	`// modes so that we can manage visual ordering and return types correctly.`
	`return` `(activityType ==` `ACTIVITY_TYPE_STANDARD` `|| activityType ==` `ACTIVITY_TYPE_RECENTS)`
			`&&` `(windowingMode ==` `WINDOWING_MODE_FULLSCREEN`
			`|| windowingMode ==` `WINDOWING_MODE_FREEFORM`
			`|| windowingMode ==` `WINDOWING_MODE_PINNED`
			`|| windowingMode ==` `WINDOWING_MODE_MULTI_WINDOW);`
`}

由于前面知道activityType是ACTIVITY_TYPE_STANDARD, 所以本方法返回true。

在我们的场景candidateTask为空,因此会走Task.Builder(mAtmService)进行Task的创建。
看下Task.Builder的build方法:

复制代码
`
`Task build() {`
`            if (mParent != null && mParent instanceof TaskDisplayArea) {`
`                validateRootTask((TaskDisplayArea) mParent);`
`            }`

`            if (mActivityInfo == null) {`
`                mActivityInfo = new ActivityInfo();`
`                mActivityInfo.applicationInfo = new ApplicationInfo();`
`            }`

`            mUserId = UserHandle.getUserId(mActivityInfo.applicationInfo.uid);`
`            mTaskAffiliation = mTaskId;`
`            mLastTimeMoved = System.currentTimeMillis();`
`            mNeverRelinquishIdentity = true;`
`            mCallingUid = mActivityInfo.applicationInfo.uid;`
`            mCallingPackage = mActivityInfo.packageName;`
`            mResizeMode = mActivityInfo.resizeMode;`
`            mSupportsPictureInPicture = mActivityInfo.supportsPictureInPicture();`
`            if (!mRemoveWithTaskOrganizer && mActivityOptions != null) {`
`                mRemoveWithTaskOrganizer = mActivityOptions.getRemoveWithTaskOranizer();`
`            }`

`            final Task task = buildInner();`
`            task.mHasBeenVisible = mHasBeenVisible;`

`            // Set activity type before adding the root task to TaskDisplayArea, so home task can`
`            // be cached, see TaskDisplayArea#addRootTaskReferenceIfNeeded().`
`            if (mActivityType != ACTIVITY_TYPE_UNDEFINED) {`
`                task.setActivityType(mActivityType);`
`            }`

`            if (mParent != null) {`
`                if (mParent instanceof Task) {`
`                    final Task parentTask = (Task) mParent;`
`                    parentTask.addChild(task, mOnTop ? POSITION_TOP : POSITION_BOTTOM,`
`                            (mActivityInfo.flags & FLAG_SHOW_FOR_ALL_USERS) != 0);`
`                } else {`
                `//` `**` `将本task加入到parent,也就是TaskDisplayArea的栈顶`
`                    mParent.addChild(task, mOnTop ? POSITION_TOP : POSITION_BOTTOM);`
`                }`
`            }`

`            // Set windowing mode after attached to display area or it abort silently.`
`            if (mWindowingMode != WINDOWING_MODE_UNDEFINED) {`
`                task.setWindowingMode(mWindowingMode, true /* creating */);`
`            }`
`            return task;`
`        }

注意mParent.addChild将本task加入到了TaskDisplayArea的栈顶。

Activi ty 加入 Task

回到startActivityInner。getOrCreateRootTask()创建完Task后,紧接着判断newTask。因为前面newTask = targetTask == null, targetTast是指可复用的task,为空,所以这里newTask为true。因此紧接着调用setNewTask()。注意参数taskToAffiliate是null, 因为mSourceRecord是null。

复制代码
platform/frameworks/base/services/core/java/com/android/server/wm/ActivityStarter.java`

`private void setNewTask(Task taskToAffiliate) {`
`        final boolean toTop = !mLaunchTaskBehind && !mAvoidMoveToFront;`
         `//返回mTargetRootTask自身`
`        final Task task = mTargetRootTask.reuseOrCreateTask(`
`                mStartActivity.info, mIntent, mVoiceSession,`
`                mVoiceInteractor, toTop, mStartActivity, mSourceRecord, mOptions);`
`        task.mTransitionController.collectExistenceChange(task);`
        `//` `将新new的activityRecord加入到mTargetRootTask`
`        addOrReparentStartingActivity(task, "setTaskFromReuseOrCreateNewTask");`

`        ProtoLog.v(WM_DEBUG_TASKS, "Starting new activity %s in new task %s",`
`                mStartActivity, mStartActivity.getTask());`

`        if (taskToAffiliate != null) {`
`            mStartActivity.setTaskToAffiliateWith(taskToAffiliate);`
`        }`
`    }

关注reuseOrCreateTask()方法, 其参数mStartActivity是之前新new的ActivityRecord, mIntent是客户端传过来intent, toTop为true:

复制代码
platform/frameworks/base/services/core/java/com/android/server/wm/Task.java`

`Task reuseOrCreateTask(ActivityInfo info, Intent intent, IVoiceInteractionSession voiceSession,`
`		IVoiceInteractor voiceInteractor,` `boolean toTop, ActivityRecord activity,`
`		ActivityRecord source, ActivityOptions options)` `{`

`	Task task;`
	`if` `(canReuseAsLeafTask())` `{`
       `// 会走这里`
		`// This root task will only contain one task, so just return itself since all root`
		`// tasks ara now tasks and all tasks are now root tasks.`
`		task =` `reuseAsLeafTask(voiceSession, voiceInteractor, intent, info, activity);`
	`}` `else` `{`
		`// Create child task since this root task can contain multiple tasks.`
		`final` `int taskId = activity != null`
				`? mTaskSupervisor.getNextTaskIdForUser(activity.mUserId)`
				`: mTaskSupervisor.getNextTaskIdForUser();`
		`final` `int activityType =` `getActivityType();`
`		task =` `new` `Task.Builder(mAtmService)`
				`.setTaskId(taskId)`
				`.setActivityInfo(info)`
				`.setActivityOptions(options)`
				`.setIntent(intent)`
				`.setVoiceSession(voiceSession)`
				`.setVoiceInteractor(voiceInteractor)`
				`.setOnTop(toTop)`
				`.setParent(this)`
				`.build();`
	`}`

	`int displayId =` `getDisplayId();`
	`if` `(displayId == INVALID_DISPLAY) displayId = DEFAULT_DISPLAY;`
	`final` `boolean isLockscreenShown = mAtmService.mTaskSupervisor.getKeyguardController()`
			`.isKeyguardOrAodShowing(displayId);`
	`if` `(!mTaskSupervisor.getLaunchParamsController()`
			`.layoutTask(task, info.windowLayout, activity, source, options)`
			`&&` `!getRequestedOverrideBounds().isEmpty()`
			`&& task.isResizeable()` `&&` `!isLockscreenShown)` `{`
`		task.setBounds(getRequestedOverrideBounds());`
	`}`

	`return task;`
`}

关注canReuseAsLeafTask():

复制代码
platform/frameworks/base/services/core/java/com/android/server/wm/Task.java`

`private` `boolean` `canReuseAsLeafTask()` `{`
	`// Cannot be reused as leaf task if this task is created by organizer or having child tasks.`
	`if` `(mCreatedByOrganizer ||` `!isLeafTask())` `{`
		`return` `false;`
	`}`

	`// Existing Tasks can be reused if a new root task will be created anyway.`
	`final` `int windowingMode =` `getWindowingMode();`
	`final` `int activityType =` `getActivityType();`
	`return DisplayContent.alwaysCreateRootTask(windowingMode, activityType);`
`}
复制代码
platform/frameworks/base/services/core/java/com/android/server/wm/Task.java`

`boolean` `isLeafTask()` `{`
	`for` `(int i = mChildren.size()` `-` `1; i >=` `0;` `--i)` `{`
		`if` `(mChildren.get(i).asTask()` `!= null)` `{`
			`return` `false;`
		`}`
	`}`
	`return` `true;`
`}

canReuseAsLeafTask()返回true。因为isLeafTask()是true(此时还没有mChildren),而DisplayContent.alwaysCreateRootTask之前看过,返回true。

所以会执行:

task = reuseAsLeafTask(voiceSession, voiceInteractor, intent, info, activity);

这句最终会返回task自身this。

整个setNewTask里的final Task task = mTargetRootTask.reuseOrCreateTask, 可以认为返回了mTargetRootTask自身,随后调用addOrReparentStartingActivity,把新new的mStartActivity加入到新new的mTargetRootTask

复制代码
platform/frameworks/base/services/core/java/com/android/server/wm/Task.java`

`private` `void` `addOrReparentStartingActivity(@NonNull Task task, String reason)` `{`
`	TaskFragment newParent = task;`
	`if` `(mInTaskFragment != null)` `{`
		`int embeddingCheckResult =` `canEmbedActivity(mInTaskFragment, mStartActivity, task);`
		`if` `(embeddingCheckResult == EMBEDDING_ALLOWED)` `{`
`			newParent = mInTaskFragment;`
`			mStartActivity.mRequestedLaunchingTaskFragmentToken =`
`					mInTaskFragment.getFragmentToken();`
		`}` `else` `{`
			`// Start mStartActivity to task instead if it can't be embedded to mInTaskFragment.`
			`sendCanNotEmbedActivityError(mInTaskFragment, embeddingCheckResult);`
		`}`
	`}` `else` `{`
`		TaskFragment candidateTf = mAddingToTaskFragment != null ? mAddingToTaskFragment : null;`
		`if` `(candidateTf == null)` `{`
			`// Puts the activity on the top-most non-isolated navigation TF, unless the`
			`// activity is launched from the same TF.`
			`final TaskFragment sourceTaskFragment =`
`					mSourceRecord != null ? mSourceRecord.getTaskFragment()` `: null;`
			`final ActivityRecord top = task.getActivity(r ->` `{`
				`if` `(!r.canBeTopRunning())` `{`
					`return` `false;`
				`}`
				`final TaskFragment taskFragment = r.getTaskFragment();`
				`return` `!taskFragment.isIsolatedNav()` `||` `(sourceTaskFragment != null`
						`&& sourceTaskFragment == taskFragment);`
			`});`
			`if` `(top != null)` `{`
`				candidateTf = top.getTaskFragment();`
			`}`
		`}`
		`if` `(candidateTf != null && candidateTf.isEmbedded()`
				`&&` `canEmbedActivity(candidateTf, mStartActivity, task)` `== EMBEDDING_ALLOWED)` `{`
			`// Use the embedded TaskFragment of the top activity as the new parent if the`
			`// activity can be embedded.`
`			newParent = candidateTf;`
		`}`
	`}`
	`if` `(mStartActivity.getTaskFragment()` `== null`
			`|| mStartActivity.getTaskFragment()` `== newParent)` `{`
`		newParent.addChild(mStartActivity, POSITION_TOP);`
	`}` `else` `{`
`		mStartActivity.reparent(newParent, newParent.getChildCount()` `/* top */, reason);`
	`}`
`}

此处TaskFragment newParent = task,getTaskFragment为null,newParent通过newParent.addChild(mStartActivity, POSITION_TOP)将activity加入task。

启动Activity

回到startActivityInner,来到位置4。由于上面的mTargetRootTask.startActivityLocked对整体流向没有影响,暂不讨论。下面看mDoResume条件满足内的逻辑:if (!mTargetRootTask.isTopActivityFocusable() ...。

复制代码
platform/frameworks/base/services/core/java/com/android/server/wm/TaskFragment.java`

`boolean isTopActivityFocusable() {`
`	final ActivityRecord r = topRunningActivity();`
`	return r != null ? r.isFocusable()`
`			: (isFocusable() && getWindowConfiguration().canReceiveKeys());`
`}

由于mTargetRootTask此时是刚创建的,所以topRunningActivity是空。所以这里用task本身的isFocusable(),

复制代码
platform/frameworks/base/services/core/java/com/android/server/wm/WindowContainer.java`

`boolean isFocusable() {`
`	final WindowContainer parent = getParent();`
`	return (parent == null || parent.isFocusable()) && mIsFocusable;`
`}

由于mTargetRootTask自身的mIsFocusable属性默认是true,其parent TaskDisplayArea的isFocusable也是true,因此整体返回true。

那么startActivityInner里接下来就会走else分支里的mRootWindowContainer.resumeFocusedTasksTopActivities(

这里,activity将被启动。

相关推荐
Dnelic-1 小时前
【单元测试】【Android】JUnit 4 和 JUnit 5 的差异记录
android·junit·单元测试·android studio·自学笔记
Eastsea.Chen4 小时前
MTK Android12 user版本MtkLogger
android·framework
长亭外的少年11 小时前
Kotlin 编译失败问题及解决方案:从守护进程到 Gradle 配置
android·开发语言·kotlin
建群新人小猿13 小时前
会员等级经验问题
android·开发语言·前端·javascript·php
1024小神15 小时前
tauri2.0版本开发苹果ios和安卓android应用,环境搭建和最后编译为apk
android·ios·tauri
兰琛15 小时前
20241121 android中树结构列表(使用recyclerView实现)
android·gitee
Y多了个想法15 小时前
RK3568 android11 适配敦泰触摸屏 FocalTech-ft5526
android·rk3568·触摸屏·tp·敦泰·focaltech·ft5526
NotesChapter17 小时前
Android吸顶效果,并有着ViewPager左右切换
android
_祝你今天愉快18 小时前
分析android :The binary version of its metadata is 1.8.0, expected version is 1.5.
android
暮志未晚Webgl18 小时前
109. UE5 GAS RPG 实现检查点的存档功能
android·java·ue5