Android 启动窗口(Starting Window)技术文档

5 阅读8分钟

Android 启动窗口(Starting Window)技术文档

源码依据:android w (AOSP 16)


1 概述

1.1 什么是启动窗口

启动窗口(Starting Window)是系统在应用进程创建和初始化、以及 Activity 真正绘制出第一帧之前,由 WMS 替应用提前显示的一个临时窗口。

核心作用:提供即时视觉反馈,避免用户点击应用图标后看到长时间黑屏。

1.2 两种类型

APPLICATION_STARTING_WINDOW (SplashScreen)SNAPSHOT (任务快照)
触发场景冷启动 / 温启动热启动(从后台恢复)
内容来源应用主题:windowSplashScreenBackground + windowSplashScreenAnimatedIcon应用进入后台时的 TaskSnapshot 截图
对应源码类SplashScreenStartingDataSnapshotStartingData
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 类型SplashScreenStartingDataSnapshotStartingData
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()

核心过程

  1. 创建 Context(解析应用主题、图标资源)
  2. 双线程并行
    • 后台线程:预绘制 AdaptiveIconDrawableBitmapShader(缓存)
    • Shell 线程WindowManagerGlobal.addView() → 等待 Choreographer#doFramesession.relayout()
  3. 第一次 doFrame 前将 SplashScreenView 同步到窗口上
  4. 用户看到启动窗口(背景色 + 图标)

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 的关键区别

SplashScreenSnapshot
添加方式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应用窗口从下方上移

动画结束后调用 finishCallbackmWindowManagerGlobal.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() 时发生三件事:

  1. StartingWindow 的 WindowState 从窗口树删除
  2. InputChannel 被注销
  3. 下一次 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 窗口管理中最典型的跨进程协作场景。