Android 启动窗口(Starting Window)技术文档
源码依据:android w (AOSP 16)
1 概述
1.1 什么是启动窗口
启动窗口(Starting Window)是系统在应用进程创建和初始化、以及 Activity 真正绘制出第一帧之前,由 WMS 替应用提前显示的一个临时窗口。
核心作用:提供即时视觉反馈,避免用户点击应用图标后看到长时间黑屏。
1.2 两种类型
| APPLICATION_STARTING_WINDOW (SplashScreen) | SNAPSHOT (任务快照) | |
|---|---|---|
| 触发场景 | 冷启动 / 温启动 | 热启动(从后台恢复) |
| 内容来源 | 应用主题:windowSplashScreenBackground + windowSplashScreenAnimatedIcon | 应用进入后台时的 TaskSnapshot 截图 |
| 对应源码类 | SplashScreenStartingData | SnapshotStartingData |
| needRevealAnimation() | true — 退出时播放揭示动画 | false — 直接移除 |
| 是否纯色 | 不一定,默认包含图标,可配置动画图标 | 反映应用离开时的真实界面 |
2 完整启动流程
2.1 冷启动调用链
ATMS.startActivity()
→ ActivityStarter.startActivityInner()
→ Task.startActivityLocked() ← 冷启动入口
→ StartingSurfaceController.showStartingWindow()
→ ActivityRecord.showStartingWindow()
→ ActivityRecord.addStartingWindow()
→ 条件检查(见 2.3)
→ mStartingData = new SplashScreenStartingData(theme, typeParams)
→ scheduleAddStartingWindow()
→ mStartingData.createStartingSurface() ← 多态分发
→ StartingSurfaceController.createSplashScreenStartingSurface()
→ TaskOrganizerController.addStartingWindow()
→ ITaskOrganizer.addStartingWindow() ← Binder IPC → Shell
2.2 热启动调用链
ATMS.startActivity()
→ ActivityStarter.startActivityInner()
→ ActivityStarter.recycleTask() ← 热启动入口
→ ActivityRecord.showStartingWindow()
→ ActivityRecord.createSnapshot()
→ mStartingData = new SnapshotStartingData(snapshot, typeParams)
→ scheduleAddStartingWindow()
→ mStartingData.createStartingSurface()
→ StartingSurfaceController.createTaskSnapshotSurface()
→ TaskOrganizerController.addStartingWindow()
→ ITaskOrganizer.addStartingWindow() ← Binder IPC → Shell
差异与收敛点:
| 环节 | 冷启动 | 热启动 |
|---|---|---|
| ActivityStarter 入口 | Task.startActivityLocked() | ActivityStarter.recycleTask() |
| StartingData 类型 | SplashScreenStartingData | SnapshotStartingData |
| Surface 创建 | createSplashScreenStartingSurface() | createTaskSnapshotSurface() |
| 收敛点 | ActivityRecord.scheduleAddStartingWindow() | 同上 |
| IPC 之后 | Shell 侧 addSplashScreenStartingWindow() | Shell 侧 makeTaskSnapshotWindow() |
2.3 不创建启动窗口的条件
来自 ActivityRecord.addStartingWindow() 实际源码判断:
!okToDisplay()— Display 不可见(锁屏)hasStartingWindow()— 已有启动窗口mainWin.isDrawn()— 主窗口已绘制resolvedTheme == 0 && theme != 0— 主题明确不要启动窗口windowDisablePreview— 开发者主动禁用mTaskOverlay— TaskOverlay 模式ANIM_SCENE_TRANSITION— 使用共享元素转场
3 Shell 侧绘制:SplashScreen 与 Snapshot 的区别
IPC 到达 Shell 进程后,两种类型的绘制路径完全不同。
3.1 SplashScreen 路径(冷启动)
StartingWindowController.addStartingWindow()
→ StartingSurfaceDrawer.addSplashScreenStartingWindow()
→ SplashscreenWindowCreator.addSplashScreenStartingWindow()
核心过程:
- 创建 Context(解析应用主题、图标资源)
- 双线程并行:
- 后台线程:预绘制
AdaptiveIconDrawable→BitmapShader(缓存) - Shell 线程:
WindowManagerGlobal.addView()→ 等待Choreographer#doFrame→session.relayout()
- 后台线程:预绘制
- 第一次
doFrame前将SplashScreenView同步到窗口上 - 用户看到启动窗口(背景色 + 图标)
3.2 Snapshot 路径(热启动)
StartingWindowController.addStartingWindow()
→ StartingSurfaceDrawer.makeTaskSnapshotWindow()
→ SnapshotWindowCreator.makeTaskSnapshotWindow()
makeTaskSnapshotWindow 完整过程(TaskSnapshotWindow.java):
// 1. 创建 Window 和 LayoutParams
final WindowManager.LayoutParams layoutParams = SnapshotDrawerUtils.createLayoutParameters(
info, "SnapshotStartingWindow for taskId=" + taskId, TYPE_APPLICATION_STARTING,
snapshot.getHardwareBuffer().getFormat(), appToken);
// 2. 通过 IWindowSession 添加窗口(Binder → WMS)
session.addToDisplay(window, layoutParams, View.GONE, displayId, ...);
// 3. relayout 获取 Surface
session.relayout(window, layoutParams, -1, -1, View.VISIBLE, ...);
// 4. 将快照图片绘制到 Surface 上
SnapshotDrawerUtils.drawSnapshotOnSurface(layoutParams, surfaceControl, snapshot,
info.taskBounds, true /* releaseAfterDraw */);
// 5. 通知 WMS 已绘制完成
session.finishDrawing(mWindow, null, Integer.MAX_VALUE);
与 SplashScreen 的关键区别:
| SplashScreen | Snapshot | |
|---|---|---|
| 添加方式 | WindowManagerGlobal.addView()(完整 View 树) | session.addToDisplay() + session.relayout()(轻量 Window) |
| 内容 | 动态构建的 SplashScreenView(图标+背景) | TaskSnapshot 的 HardwareBuffer 直接绘制到 Surface |
| 复杂度 | 重(View 层级、Choreographer、双线程) | 轻(一个 Surface + 一张图) |
4 应用进程启动
Shell 显示启动窗口的同时,系统异步 fork 应用进程:
Zygote fork → ActivityThread.main()
→ bindApplication()
→ Application.onCreate()
→ Activity.onCreate() → onStart() → onResume()
→ ViewRootImpl.performTraversals()
→ performMeasure() → performLayout() → performDraw()
→ RenderThread 提交首帧到 SurfaceFlinger
→ mWindowSession.finishDrawing() ← Binder 通知 WMS
5 首帧就绪 → 触发移除
WMS 收到首帧就绪信号:
// ActivityRecord.onFirstWindowDrawn() (line 7971)
void onFirstWindowDrawn(WindowState win) {
firstWindowDrawn = true;
// 判断:是否在 Transition 收集中?
final Task associatedTask = task.mSharedStartingData != null ? task : null;
if (associatedTask == null) {
removeStartingWindow(); // 不在 Transition 中,直接移除
} else if (associatedTask.getActivity(
r -> r.isVisibleRequested() && !r.firstWindowDrawn) == null) {
// 共享启动窗口场景:等 Task 内所有可见 Activity 都绘制完再移除
final ActivityRecord r = associatedTask.getActivity(ar -> ar.mStartingData != null);
if (r != null) { r.removeStartingWindow(); }
}
}
removeStartingWindow() 内部判断:
// ActivityRecord.removeStartingWindowAnimation() (line 3645)
void removeStartingWindowAnimation(boolean prepareAnimation) {
if (mStartingData != null) {
// 关键判断:正在 collect Transition 或等待 Transaction commit
if (getSyncTransactionCommitCallbackDepth() > 0 || mSyncState != SYNC_STATE_NONE) {
// 不立即移除,标记为 Transaction 后移除
mStartingData.mRemoveAfterTransaction = AFTER_TRANSACTION_REMOVE_DIRECTLY;
return;
}
}
// 不在 Transition 中,立即发起移除 IPC
mStartingSurface.remove(animate, hasImeSurface);
}
6 启动窗口与 ShellTransition 的关系
6.1 核心结论
启动窗口的移除时机取决于是否有 Shell Transition:
| 场景 | 首帧就绪后 | 启动窗口何时消失 |
|---|---|---|
| 无 Transition(普通冷启动) | 立即发起移除 IPC | 首帧就绪后直接移除,无动画 |
| 有 Transition(应用启动伴随转场动画) | 延迟到 Transaction ready 后 | 在 Transition 动画播放过程中消失 |
6.2 无 Transition 的简单场景
应用首帧绘制完成
→ onFirstWindowDrawn()
→ removeStartingWindow() ← 不在 collect 中,立即执行
→ WindowState 从窗口树删除 ← 输入层:AppWindow 成为栈顶
→ updateInputWindows()
→ IPC → Shell.removeStartingWindow()
→ playRevealAnimation?
├─ SplashScreen: 退出动画播放中... → 动画结束 → leash.remove() → Surface 释放
└─ Snapshot: 直接移除(无 leash)
这是最简单的路径:应用窗口已就绪,启动窗口使命完成。但注意即使没有 Transition,SplashScreen 的退出动画期间也是 WindowState 已删、Surface 仍在 leash 上的状态。
6.3 有 Transition 的场景(重点)
应用首帧绘制完成
→ onFirstWindowDrawn()
→ removeStartingWindow()
→ 发现正在 collect Transition
→ 设置 mRemoveAfterTransaction = AFTER_TRANSACTION_REMOVE_DIRECTLY
→ 返回(不立即移除)
─── Transition 继续推进 ───
→ Transition.onTransactionReady()
→ commitVisibleActivities(transaction) ← 应用窗口变为可见
→ calculateTransitionInfo() ← 计算动画目标
→ Transaction 发送到 Shell
→ Shell 开始播放 Transition 动画
─── 此时:启动窗口 WindowState 已被删除,但 Surface 仍在 leash 上 ───
─── 输入层:AppWindow 已成为栈顶(可以点击) ───
─── 视觉层:splash 仍在 leash 上播放动画 ───
→ Transition 动画播放中...
→ Shell 播放退出动画(仅 SplashScreen)
→ 动画结束后 removeView()
→ leash.remove()
→ StartingWindowSurface 从 SurfaceFlinger 合成树释放
关键点:
removeStartingWindow()删除的是 WindowState(窗口层),不是 Surface(视觉层)- Surface 仍然挂在 animation leash 上,由 ShellTransition 控制动画
- 输入系统只看 InputWindowInfo,不看 leash。WindowState 删除后,AppWindow 立即成为输入栈顶
- 所以动画没结束,但已经能点击 App
6.4 mRemoveAfterTransaction 的作用
这是控制 WindowState 何时被删除 的同步机制:
| 值 | 含义 | WindowState 何时删除 |
|---|---|---|
IDLE (0) | 默认,不延迟 | onFirstWindowDrawn() 时立即删除 |
REMOVE_DIRECTLY (1) | 在 collect Transition 中被打断 | finishTransition() 回调中删除 |
COPY_TO_CLIENT (2) | 需要复制给应用侧 | Transaction 完成后删除 |
AFTER_TRANSITION_FINISH (3) | 锁屏场景 | Transition 完全结束后删除 |
注意:WindowState 删除 ≠ Surface 释放。删除后输入立即转到 AppWindow,但 Surface 仍在 leash 上播放动画直到 leash 释放。
6.5 应用窗口可见性与启动窗口的配合
启动窗口存在期间:
┌─────────────────────────────────┐
│ 启动窗口 Surface(可见) │
│ 启动窗口 WindowState(在树中) │
│ 应用窗口 Surface(尚未创建) │
└─────────────────────────────────┘
onTransactionReady 之后:
┌─────────────────────────────────┐
│ 应用窗口 Surface(可见,上层) │ ← commitVisibleActivities 使其可见
│ 启动窗口 Surface(可见,leash) │ ← 仍在,但被应用窗口覆盖
└─────────────────────────────────┘
removeStartingWindow 之后(动画仍在播放):
┌─────────────────────────────────┐
│ 应用窗口 Surface(可见) │ ← 输入栈顶,可点击
│ 启动窗口 Surface(可见,leash) │ ← 视觉在,但不在 InputWindowInfo 中
│ 启动窗口 WindowState(已删除) │ ← 输入系统不再看到它
└─────────────────────────────────┘
leash 释放之后:
┌─────────────────────────────────┐
│ 应用窗口 Surface(可见) │
│ 启动窗口 Surface(已释放) │ ← 视觉和输入终于一致
└─────────────────────────────────┘
Layer 配合:
- 启动窗口的 Z-order 始终在应用窗口之下(
TYPE_APPLICATION_STARTING) commitVisibleActivities(transaction)在onTransactionReady()中提交应用窗口的可见性removeStartingWindow()删除 WindowState,下一次updateInputWindows后 AppWindow 成为输入栈顶- leash 释放后,启动窗口的 Surface 才从 SurfaceFlinger 合成树中彻底消失
- 应用窗口可见后,启动窗口虽然还没被移除,但已被覆盖,用户看不到
- 移除启动窗口是释放资源,不影响用户感知
6.6 退出动画(仅 SplashScreen)
Shell 侧收到移除请求后,如果是 SplashScreen 且 playRevealAnimation=true:
// SplashscreenContentDrawer.applyExitAnimation()
void applyExitAnimation(SplashScreenView view, SurfaceControl leash, ...) {
// 计算是否需要延迟(等待应用侧 icon 动画完成)
final long delayed = minimumShowingDuration - appReadyDuration;
if (delayed > 0) {
view.postDelayed(playAnimation, delayed);
} else {
playAnimation.run();
}
}
退出动画包含三个并行效果:
| 动画 | 作用 |
|---|---|
| Icon Fade Out | 启动图标淡出 |
| Radial Vanish | 背景径向消失 |
| Shift Up | 应用窗口从下方上移 |
动画结束后调用 finishCallback → mWindowManagerGlobal.removeView() → 启动窗口 Surface 释放。
Snapshot 不需要退出动画(needRevealAnimation() = false),直接移除。
7 三条时间线:窗口、输入、动画 Surface
7.1 核心结论
启动窗口涉及三个独立的系统,它们的变化发生在不同时间点:
| 系统 | 控制对象 | 启动窗口何时消失 |
|---|---|---|
| 窗口生命周期 | WindowState(WMS 窗口树) | removeStartingWindow() 时立即删除 |
| 输入系统 | InputWindowInfo(InputDispatcher) | 下一次 updateInputWindows 后不再参与命中测试 |
| 动画/视觉 | SurfaceControl(animation leash) | ShellTransition 动画结束、leash 被释放时 |
一句话总结:WMS 很早就删掉 StartingWindow(影响输入),Shell 很晚才删掉 StartingWindow Surface(影响视觉)。所以动画没结束,但已经能点击 App。
7.2 完整时间线
按真实系统行为的顺序:
1. App first draw
→ ViewRootImpl.finishDrawing() → WMS
2. Transition 已经开始
→ onTransactionReady() → commitVisibleActivities()
→ 应用窗口变为可见
3. WMS.removeStartingWindow()
→ StartingWindow 的 WindowState 从窗口树删除
→ InputChannel 被注销
4. WMS.updateInputWindows()
→ 生成新的 InputWindowInfo 列表
→ StartingWindow 不再出现在列表中
5. SurfaceFlinger apply transaction
→ InputDispatcher 收到新的窗口列表
→ 栈顶窗口变成 AppWindow
===== 从这里开始可以点击 App =====
6. ShellTransition 动画继续播放
→ StartingWindowSurface 仍然挂在 animation leash 上
→ 屏幕上还能看到 splash
7. 动画结束
→ Shell 收到 finish
→ remove animation leash
8. leash.remove()
→ StartingWindowSurface 从 SurfaceFlinger 合成树释放
===== StartingWindow 真正完全消失 =====
7.3 输入系统为什么不会拦截
InputDispatcher 在做 hit-test 时只认识 InputWindowInfo,不认识 leash 或 Surface。
InputDispatcher hit-test 遍历:
InputWindowInfo[]
├── token
├── layer (Z-order)
├── visible
├── touchableRegion
├── flags
└── inputChannel
注意:没有 Surface、leash、animation
removeStartingWindow() 时发生三件事:
- StartingWindow 的 WindowState 从窗口树删除
- InputChannel 被注销
- 下一次 updateInputWindows 不再生成这个 InputWindowInfo
然后通过 setInputWindowInfo 提交给 SurfaceFlinger,SurfaceFlinger 在下一帧 apply transaction 并同步给 InputDispatcher。
从这一帧开始,InputDispatcher 的窗口栈顶就是 AppWindow。
7.4 窗口层 vs 视觉层
removeStartingWindow() 之后、leash 释放之前:
视觉层: 输入层:
┌──────────────────┐ ┌──────────────────┐
│ splash 还在显示 │ │ AppWindow (栈顶) │ ← 点击直接进入 App
│ (leash 控制动画) │ │ LauncherWindow │
└──────────────────┘ └──────────────────┘
用户感觉: "我点在 splash 上,但其实点的是下面的 App"
视觉层级 ≠ 输入层级。这是系统刻意做的优化,用来避免启动动画期间 UI 无法操作。
8 架构总结
┌─────────────────────────────────────────────────────────────┐
│ ATMS/WMS(system_server)—— 裁判 │
│ 决定何时创建、何时触发移除 │
│ 通过 mRemoveAfterTransaction 控制 WindowState 删除时机 │
│ │
│ Shell 进程(SystemUI)—— 替身演员 │
│ 创建启动窗口(SplashScreen 双线程 / Snapshot 轻量 Surface)│
│ 负责退出动画(仅 SplashScreen) │
│ 负责释放 animation leash(Surface 彻底消失) │
│ │
│ App 进程 —— 真正的主角 │
│ 后台加载,首帧完成后通知 WMS │
│ │
│ 流程:裁判让替身上场 → 主角准备 → 主角喊"准备好了" │
│ → 裁判根据 Transition 状态决定何时让替身退场 │
│ → 替身退场动画 → 主角完全亮相 │
└─────────────────────────────────────────────────────────────┘
启动窗口跨越 ATMS → WMS → TaskOrganizerController → Shell → SurfaceFlinger → App 进程,通过 ShellTransition 框架统一管理,是 Android 窗口管理中最典型的跨进程协作场景。