一、这篇接着解决什么问题
上一篇把 WMS 的基础骨架搭了起来:
text
RootWindowContainer
└── DisplayContent
└── Feature DisplayArea
├── DisplayArea.Tokens
├── DefaultTaskDisplayArea
└── ImeContainer
这棵树已经能接住 Task 和窗口,但此时还没有 Launcher 对应的 Task、ActivityRecord 和 WindowState。
这一篇只追一条线:Launcher 被解析为 HOME Activity 后,系统怎样把它放进 DefaultTaskDisplayArea,又怎样接住 Launcher 进程发回来的窗口请求。最终补齐的层级如下:
text
DefaultTaskDisplayArea
└── Root Home Task
└── Launcher Task
└── ActivityRecord(同时也是 WindowToken)
└── WindowState
本文以 Android 14 为准,代码均做了裁剪。分析停在:
java
win.mToken.addWindow(win);
执行到这里,只能说明 WindowState 已经挂进 WindowContainer 树。Launcher 的 Surface、首帧以及 BootAnimation 退出放到下一篇。
二、先把几个对象对上
2.1 它们不在同一进程,也不在同一时刻创建
| 对象 | 所在进程 | 创建时机 |
|---|---|---|
| Root Home Task | system_server |
RootWindowContainer.setWindowManager() 阶段 |
ActivityRecord |
system_server |
ActivityStarter 解析启动请求时 |
| Launcher Task | system_server |
首次启动 HOME、没有可复用 Task 时 |
PhoneWindow |
Launcher 进程 | Activity.attach() |
DecorView |
Launcher 进程 | PhoneWindow.installDecor() |
ViewRootImpl |
Launcher 进程 | WindowManagerGlobal.addView() |
WindowState |
system_server |
WindowManagerService.addWindow() |
顺序上,Task 和 ActivityRecord 先在 system_server 中入树。Launcher Activity 执行到 Resume 后,应用进程才向 WMS 添加窗口,WindowState 是这时创建的。
2.2 ActivityRecord 为什么能成为 WindowState 的父节点
ActivityRecord 的定义在 frameworks/base/services/core/java/com/android/server/wm/ActivityRecord.java:
java
final class ActivityRecord extends WindowToken
implements WindowManagerService.AppFreezeListener {
...
}
它不是单纯的 Activity 状态记录。对 WMS 来说,ActivityRecord 本身就是应用窗口的 WindowToken。
构造 ActivityRecord 时还会创建一枚 Binder Token:
java
private ActivityRecord(...) {
super(_service.mWindowManager,
new Token(),
TYPE_APPLICATION,
true,
null /* displayContent */,
false /* ownerCanManageAppTokens */);
...
}
这里需要区分:
ActivityRecord是system_server中的 WindowContainer 节点;ActivityRecord.token是跨进程使用的 Binder 标识。
后面的窗口添加流程,本质上是在传递并找回这枚 Token:
text
ActivityRecord.token
→ ClientTransaction
→ Activity.attach(token)
→ PhoneWindow.mAppToken
→ WindowManager.LayoutParams.token
→ WindowManagerService.addWindow()
→ ActivityRecord
抓住这条线,后面的类虽然多,关系并不乱。
三、system_server 先建立 Launcher 的 Activity 层级
3.1 Root Home Task 已经提前创建
Root Home Task 不是启动 Launcher 时才临时创建的。RootWindowContainer.setWindowManager() 创建完 DisplayContent 和 DefaultTaskDisplayArea 后,会立即保证默认显示区域存在一个 HOME 类型的根 Task。
源码:
frameworks/base/services/core/java/com/android/server/wm/RootWindowContainer.javaframeworks/base/services/core/java/com/android/server/wm/TaskDisplayArea.java
java
// RootWindowContainer.setWindowManager()
final TaskDisplayArea defaultTaskDisplayArea =
getDefaultTaskDisplayArea();
defaultTaskDisplayArea.getOrCreateRootHomeTask(ON_TOP);
java
Task getOrCreateRootHomeTask(boolean onTop) {
Task homeTask = getRootHomeTask();
if (homeTask == null && canHostHomeTask()) {
homeTask = createRootTask(
WINDOWING_MODE_UNDEFINED,
ACTIVITY_TYPE_HOME,
onTop);
}
return homeTask;
}
这个 Root Home Task 此时还是空的。它是 HOME 的根容器,不是最终承载 Launcher Activity 的叶子 Task。
3.2 startHomeActivity 创建 ActivityRecord
上一篇已经走到 AMS.systemReady() 解析 HOME Activity。解析出 Launcher 后,调用链进入:
text
RootWindowContainer.startHomeOnTaskDisplayArea()
→ ActivityStartController.startHomeActivity()
→ ActivityStarter.execute()
ActivityStartController.startHomeActivity() 在启动参数里指定 HOME 类型和目标 TaskDisplayArea:
java
options.setLaunchWindowingMode(WINDOWING_MODE_FULLSCREEN);
options.setLaunchActivityType(ACTIVITY_TYPE_HOME);
options.setLaunchTaskDisplayArea(
taskDisplayArea.mRemoteToken
.toWindowContainerToken());
taskDisplayArea.getOrCreateRootHomeTask(ON_TOP);
obtainStarter(intent, "startHomeActivity: " + reason)
.setActivityInfo(aInfo)
.setActivityOptions(options.toBundle())
.execute();
源码:frameworks/base/services/core/java/com/android/server/wm/ActivityStartController.java
这里真正影响后续 Task 选择的是两个约束:这次启动属于 ACTIVITY_TYPE_HOME,目标区域是当前的 TaskDisplayArea。
execute() 随后进入 ActivityStarter.executeRequest()。完成 Intent、权限和 ActivityInfo 检查后,系统先创建 Launcher 的 ActivityRecord:
java
final ActivityRecord r =
new ActivityRecord.Builder(mService)
.setCaller(callerApp)
.setIntent(intent)
.setActivityInfo(aInfo)
.setActivityOptions(checkedOptions)
.setSourceRecord(sourceRecord)
.build();
源码:frameworks/base/services/core/java/com/android/server/wm/ActivityStarter.java
现在只有 ActivityRecord 对象,还没有父节点。
3.3 Root Home Task 下面为什么还有一个 Launcher Task
ActivityStarter.startActivityInner() 先确定目标根 Task。首次启动 Launcher、没有可复用 Task 时,mTargetRootTask 就是前面创建的 Root Home Task:
java
if (mTargetRootTask == null) {
mTargetRootTask = getOrCreateRootTask(
mStartActivity,
mLaunchFlags,
targetTask,
mOptions);
}
if (newTask) {
setNewTask(taskToAffiliate);
}
setNewTask() 再调用 Root Home Task 的 reuseOrCreateTask():
java
final Task task = mTargetRootTask.reuseOrCreateTask(
mStartActivity.info,
mIntent,
mVoiceSession,
mVoiceInteractor,
toTop,
mStartActivity,
mSourceRecord,
mOptions);
addOrReparentStartingActivity(
task, "setTaskFromReuseOrCreateNewTask");
Task.reuseOrCreateTask() 的分支很直接:当前 Task 能作为叶子复用,就直接使用;不能复用,就创建一个子 Task。
java
if (canReuseAsLeafTask()) {
task = reuseAsLeafTask(...);
} else {
task = new Task.Builder(mAtmService)
.setTaskId(taskId)
.setActivityInfo(info)
.setIntent(intent)
.setParent(this)
.build();
}
源码:frameworks/base/services/core/java/com/android/server/wm/Task.java
在 Android 14 默认实现中,HOME 类型的根 Task 不满足 canReuseAsLeafTask() 的条件,所以 this,也就是 Root Home Task,会多出一个子 Task:
text
DefaultTaskDisplayArea
└── Root Home Task
└── Launcher Task
厂商分支可能调整 Task 组织方式,实际设备仍应以 dumpsys activity containers 为准。
3.4 ActivityRecord 挂到 Launcher Task
Task 确定后,addOrReparentStartingActivity() 才把 ActivityRecord 放进去:
java
if (mStartActivity.getTaskFragment() == null
|| mStartActivity.getTaskFragment() == newParent) {
newParent.addChild(mStartActivity, POSITION_TOP);
} else {
mStartActivity.reparent(
newParent,
newParent.getChildCount(),
reason);
}
普通 Launcher 场景没有嵌入式 TaskFragment,newParent 就是 Launcher Task:
text
DefaultTaskDisplayArea
└── Root Home Task
└── Launcher Task
└── ActivityRecord
ActivityRecord 入树时会感知自己所在的 DisplayContent,随后走到:
text
ActivityRecord.onDisplayChanged()
→ WindowToken.onDisplayChanged()
→ DisplayContent.reParentWindowToken()
→ DisplayContent.addWindowToken()
DisplayContent.addWindowToken() 先登记 Binder Token:
java
mTokenMap.put(binder, token);
if (token.asActivityRecord() == null) {
final DisplayArea.Tokens da =
findAreaForToken(token).asTokens();
da.addChild(token);
}
源码:frameworks/base/services/core/java/com/android/server/wm/DisplayContent.java
这里没有把 ActivityRecord 再挂到 DisplayArea.Tokens。原因很简单:它已经在 Launcher Task 下面,WMS 只需把 ActivityRecord.token 和对象登记到 mTokenMap。if 中的挂载逻辑留给没有 Task 父节点的非应用 WindowToken。
到这一步,Launcher 的 Activity 层级已经建立,应用进程还没有创建窗口。
四、Activity Token 传到 Launcher 进程
4.1 realStartActivityLocked 下发启动事务
Launcher 进程不存在时,ATMS 会先通过 AMS 请求 Zygote 创建进程。进程完成 Binder attach 后,ActivityTaskSupervisor.realStartActivityLocked() 把 ActivityRecord 与进程关联,并下发 ClientTransaction。
源码:frameworks/base/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
java
r.setProcess(proc);
final ClientTransaction clientTransaction =
ClientTransaction.obtain(
proc.getThread(), r.token);
clientTransaction.addCallback(
LaunchActivityItem.obtain(
new Intent(r.intent),
System.identityHashCode(r),
r.info,
...));
clientTransaction.setLifecycleStateRequest(
ResumeActivityItem.obtain(...));
mService.getLifecycleManager()
.scheduleTransaction(clientTransaction);
r.token 跟着事务跨进程进入 Launcher。LaunchActivityItem 创建 Activity,最终状态请求则把 Activity 推进到 Resume:
text
LaunchActivityItem.execute()
→ ActivityThread.handleLaunchActivity()
ResumeActivityItem.execute()
→ ActivityThread.handleResumeActivity()
4.2 Activity.attach 把 Token 交给 PhoneWindow
ActivityThread.performLaunchActivity() 反射创建 Launcher Activity,然后调用 attach():
java
Activity activity = mInstrumentation.newActivity(
classLoader,
component.getClassName(),
r.intent);
activity.attach(
appContext,
this,
getInstrumentation(),
r.token,
...);
mInstrumentation.callActivityOnCreate(
activity, r.state);
源码:frameworks/base/core/java/android/app/ActivityThread.java
Activity.attach() 创建 PhoneWindow,并把收到的 Token 传给它:
java
mWindow = new PhoneWindow(
this, window, activityConfigCallback);
mToken = token;
mWindow.setWindowManager(
(WindowManager) context.getSystemService(
Context.WINDOW_SERVICE),
mToken,
mComponent.flattenToString(),
(info.flags
& ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
mWindowManager = mWindow.getWindowManager();
源码:frameworks/base/core/java/android/app/Activity.java
PhoneWindow 此时已经持有 Activity Token。随后 Launcher 在 onCreate() 中调用 setContentView(),PhoneWindow.installDecor() 创建 DecorView 并填充布局。
源码:frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java
java
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();
}
mLayoutInflater.inflate(
layoutResID, mContentParent);
...
}
View 树已经有了,WMS 侧还没有 WindowState。
4.3 onResume 之后才发起 addView
ResumeActivityItem 最终进入 ActivityThread.handleResumeActivity()。onResume() 执行完,DecorView 才被交给 WindowManager:
java
if (r.window == null
&& !a.mFinished
&& willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l =
r.window.getAttributes();
l.type = WindowManager.LayoutParams
.TYPE_BASE_APPLICATION;
if (a.mVisibleFromClient
&& !a.mWindowAdded) {
a.mWindowAdded = true;
wm.addView(decor, l);
}
}
这几个时点不要混在一起:onCreate() 结束时没有 WindowState;onResume() 结束后才开始 addView();addView() 返回也不等于首帧已经显示。
五、Activity Token 再回到 WMS
5.1 addView 创建 ViewRootImpl
客户端的窗口添加主线是:
text
WindowManagerImpl.addView()
→ WindowManagerGlobal.addView()
→ new ViewRootImpl()
→ ViewRootImpl.setView()
WindowManagerImpl 先补充窗口参数,再交给进程内唯一的 WindowManagerGlobal:
java
public void addView(
View view,
ViewGroup.LayoutParams params) {
applyTokens(params);
mGlobal.addView(
view,
params,
mContext.getDisplayNoVerify(),
mParentWindow,
mContext.getUserId());
}
源码:frameworks/base/core/java/android/view/WindowManagerImpl.java
WindowManagerGlobal.addView() 为 DecorView 创建 ViewRootImpl:
java
root = new ViewRootImpl(
view.getContext(), display);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
root.setView(
view, wparams, panelParentView, userId);
源码:frameworks/base/core/java/android/view/WindowManagerGlobal.java
WindowManagerGlobal 和 ViewRootImpl 都在 Launcher 进程中,不是 WindowContainer 树上的节点。
5.2 LayoutParams.token 从哪里来
Activity.attach() 传给 PhoneWindow 的 Activity Token,最终会被写入 WindowManager.LayoutParams.token。
这一步发生在 Window.adjustLayoutParamsForSubWindow():
java
if (wp.token == null) {
wp.token = mContainer == null
? mAppToken
: mContainer.mAppToken;
}
源码:frameworks/base/core/java/android/view/Window.java
Launcher 主窗口没有上级 Window,所以这里取的是 PhoneWindow.mAppToken。它就是最初的 ActivityRecord.token。
5.3 IWindowSession 把请求送回 system_server
ViewRootImpl.setView() 通过 IWindowSession 添加窗口:
java
res = mWindowSession.addToDisplayAsUser(
mWindow,
mWindowAttributes,
getHostVisibility(),
mDisplay.getDisplayId(),
userId,
mInsetsController
.getRequestedVisibleTypes(),
inputChannel,
...);
源码:frameworks/base/core/java/android/view/ViewRootImpl.java
mWindow 是应用侧的 IWindow Binder 对象,mWindowAttributes.token 是 Activity Token。服务端的 Session 只做一次转发:
java
public int addToDisplayAsUser(
IWindow window,
WindowManager.LayoutParams attrs,
...) {
return mService.addWindow(
this, window, attrs, ...);
}
源码:frameworks/base/services/core/java/com/android/server/wm/Session.java
调用重新回到 system_server,目标是 WindowManagerService.addWindow()。
六、WMS 把 WindowState 挂到 ActivityRecord
6.1 先用 attrs.token 找回 ActivityRecord
WindowManagerService.addWindow() 根据 LayoutParams.token 查询 DisplayContent.mTokenMap:
java
WindowToken token = displayContent.getWindowToken(
hasParent
? parentWindow.mAttrs.token
: attrs.token);
if (rootType >= FIRST_APPLICATION_WINDOW
&& rootType <= LAST_APPLICATION_WINDOW) {
activity = token.asActivityRecord();
if (activity == null) {
return WindowManagerGlobal.ADD_NOT_APP_TOKEN;
}
}
源码:frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java
第三节已经把映射写入 mTokenMap:
text
ActivityRecord.token → ActivityRecord
所以 WMS 能根据 attrs.token 找回原来的 ActivityRecord。应用窗口如果携带未知 Token,或者 Token 对应的不是 ActivityRecord,会在这里被拒绝。
6.2 创建 WindowState,然后 addChild
Token 和窗口类型校验通过后,WMS 创建 WindowState:
java
final WindowState win = new WindowState(
this,
session,
client,
token,
parentWindow,
appOp[0],
attrs,
viewVisibility,
session.mUid,
userId,
session.mCanAddInternalSystemWindow);
这里的 client 是 Launcher 进程中的 IWindow,token 已经被解析为 Launcher 的 ActivityRecord。WindowState 创建完成后,WMS 执行:
java
win.attach();
mWindowMap.put(client.asBinder(), win);
...
win.mToken.addWindow(win);
对于 Launcher 主窗口:
java
win.mToken == ActivityRecord
ActivityRecord 继承了 WindowToken.addWindow():
java
void addWindow(final WindowState win) {
if (win.isChildWindow()) {
return;
}
...
if (!mChildren.contains(win)) {
addChild(win, mWindowComparator);
mWmService.mWindowsChanged = true;
}
}
源码:frameworks/base/services/core/java/com/android/server/wm/WindowToken.java
addChild() 执行后,Launcher 窗口进入正式层级:
text
DefaultTaskDisplayArea
└── Root Home Task
└── Launcher Task
└── ActivityRecord
└── WindowState
现在 WMS 已经知道这个窗口属于哪个 Activity、Task 和 DisplayArea。至于窗口的大小、Surface 以及画面何时可见,还要继续走下一篇的流程。
七、顺便对照一下系统窗口
Launcher 的 WindowState 挂在 ActivityRecord 下。非应用窗口没有 Activity 和 Task,它们的父节点通常是第二篇创建的 DisplayArea.Tokens:
text
DisplayArea.Tokens
└── WindowToken
└── WindowState
非应用 WindowToken 有两种创建方式,容易混淆的地方就在这里。
| 创建方式 | 典型窗口 | 入口 |
|---|---|---|
| 预先注册 | Wallpaper、IME、VoiceInteraction、Accessibility Overlay、QS Dialog | 系统服务先调用 addWindowToken(),再把 Token 交给窗口进程 |
| 动态创建 | TYPE_APPLICATION_OVERLAY 等 |
客户端直接 addView(),WMS 在 addWindow() 中创建 Token |
7.1 Wallpaper:先注册,再添加窗口
Wallpaper 连接建立时,WallpaperManagerService 先登记 Token,然后把同一枚 Token 传给 Wallpaper 进程:
java
mWindowManagerInternal.addWindowToken(
mToken,
TYPE_WALLPAPER,
mDisplayId,
null /* options */);
connection.mService.attach(
connection,
mToken,
TYPE_WALLPAPER,
...);
源码:frameworks/base/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
WMS 创建 WallpaperWindowToken,构造过程最终进入 DisplayContent.addWindowToken():
java
mTokenMap.put(binder, token);
if (token.asActivityRecord() == null) {
final DisplayArea.Tokens da =
findAreaForToken(token).asTokens();
da.addChild(token);
}
Wallpaper 进程后续通过 LayoutParams.token 带回这枚 Token,WMS 找到已有的 WallpaperWindowToken,再把 WindowState 加进去。
7.2 Application Overlay:通常不需要预注册
系统应用使用标准 API 添加 TYPE_APPLICATION_OVERLAY 时,一般直接调用:
java
params.type = WindowManager.LayoutParams
.TYPE_APPLICATION_OVERLAY;
windowManager.addView(view, params);
如果 attrs.token 没有对应的 WindowToken,并且窗口类型允许动态创建,WindowManagerService.addWindow() 会补建一个:
java
if (token == null) {
if (!unprivilegedAppCanCreateTokenWith(...)) {
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
final IBinder binder = attrs.token != null
? attrs.token
: client.asBinder();
token = new WindowToken.Builder(
this, binder, type)
.setDisplayContent(displayContent)
.setOwnerCanManageAppTokens(
session.mCanAddInternalSystemWindow)
.build();
}
这条路径不会先经过 IWindowManager.addWindowToken():
text
WindowManager.addView()
→ WindowManagerService.addWindow()
→ 创建 WindowToken
→ DisplayArea.Tokens.addChild(WindowToken)
→ WindowToken.addChild(WindowState)
系统应用身份只影响权限检查。能否主动调用隐藏的 IWindowManager.addWindowToken(),仍取决于 UID、签名和 MANAGE_APP_TOKENS 权限。
IME、Wallpaper、VoiceInteraction、Accessibility Overlay、QS Dialog 等类型更严格。它们使用未知或类型不匹配的 Token 时,addWindow() 会返回 ADD_BAD_APP_TOKEN 一类错误。
应用窗口和非应用窗口的区别可以收成一张表:
| 对比项 | Launcher 应用窗口 | 非应用窗口 |
|---|---|---|
| Token 容器 | ActivityRecord |
普通 WindowToken 或专用子类 |
| Token 的父节点 | Launcher Task | 对应 Layer 的 DisplayArea.Tokens |
| Token 创建方式 | Activity 启动时创建 ActivityRecord |
预先注册或由 addWindow() 动态创建 |
| 最终结构 | Task → ActivityRecord → WindowState |
DisplayArea.Tokens → WindowToken → WindowState |
八、如何验证
8.1 查看 Task 和 ActivityRecord
shell
adb shell dumpsys activity containers
在输出中搜索 Launcher 包名或 Activity 名称,确认它所在的 TaskDisplayArea、Root Home Task、Launcher Task 和 ActivityRecord。
8.2 查看 WindowToken 和 WindowState
shell
adb shell dumpsys window windows
Launcher 主窗口通常能看到:
mAttrs.type=TYPE_BASE_APPLICATION;- 对应的
WindowToken和WindowState; mToken与 Launcher Activity 的关联。
九、关键源码位置
| 环节 | 源码文件 | 核心方法 |
|---|---|---|
| 创建 Root Home Task | frameworks/base/services/core/java/com/android/server/wm/RootWindowContainer.java |
setWindowManager() |
| 获取 Root Home Task | frameworks/base/services/core/java/com/android/server/wm/TaskDisplayArea.java |
getOrCreateRootHomeTask() |
| 发起 HOME 启动 | frameworks/base/services/core/java/com/android/server/wm/ActivityStartController.java |
startHomeActivity() |
创建、挂载 ActivityRecord |
frameworks/base/services/core/java/com/android/server/wm/ActivityStarter.java |
executeRequest()、startActivityInner()、addOrReparentStartingActivity() |
| 创建 Launcher Task | frameworks/base/services/core/java/com/android/server/wm/Task.java |
reuseOrCreateTask() |
| 下发启动事务 | frameworks/base/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java |
realStartActivityLocked() |
| 创建 Launcher Activity | frameworks/base/core/java/android/app/ActivityThread.java |
handleLaunchActivity()、performLaunchActivity() |
创建 PhoneWindow |
frameworks/base/core/java/android/app/Activity.java |
attach() |
创建 DecorView |
frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java |
setContentView()、installDecor() |
发起 addView() |
frameworks/base/core/java/android/app/ActivityThread.java |
handleResumeActivity() |
创建 ViewRootImpl |
frameworks/base/core/java/android/view/WindowManagerGlobal.java |
addView() |
| 请求 WMS 添加窗口 | frameworks/base/core/java/android/view/ViewRootImpl.java |
setView() |
| Binder 服务端转发 | frameworks/base/services/core/java/com/android/server/wm/Session.java |
addToDisplayAsUser() |
创建并挂载 WindowState |
frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java |
addWindow() |
| 将窗口挂到 Token | frameworks/base/services/core/java/com/android/server/wm/WindowToken.java |
addWindow() |
| 非应用 Token 选择 DisplayArea | frameworks/base/services/core/java/com/android/server/wm/DisplayContent.java |
addWindowToken()、findAreaForToken() |
十、总结
实际上只做了两次挂载。
第一次发生在 Activity 启动阶段:Root Home Task 创建 Launcher Task,ActivityStarter 把 ActivityRecord 加到 Task 下。
第二次发生在窗口添加阶段:Launcher 进程把 Activity Token 放进 LayoutParams 送回 WMS,WMS 找回同一个 ActivityRecord,再把新建的 WindowState 加到它下面。
text
DefaultTaskDisplayArea
└── Root Home Task
└── Launcher Task
└── ActivityRecord
└── WindowState
现在树上的归属关系已经完整。下一篇从 ViewRootImpl 的首次 Traversal 和 relayoutWindow() 接着往下,分析 Launcher Surface、第一帧以及 BootAnimation 退出。