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

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 冷启动调用链

scss 复制代码
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 热启动调用链

scss 复制代码
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 路径(冷启动)

scss 复制代码
StartingWindowController.addStartingWindow()
  → StartingSurfaceDrawer.addSplashScreenStartingWindow()
    → SplashscreenWindowCreator.addSplashScreenStartingWindow()

核心过程:

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

3.2 Snapshot 路径(热启动)

scss 复制代码
StartingWindowController.addStartingWindow()
  → StartingSurfaceDrawer.makeTaskSnapshotWindow()
    → SnapshotWindowCreator.makeTaskSnapshotWindow()

makeTaskSnapshotWindow 完整过程 (TaskSnapshotWindow.java):

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 应用进程:

scss 复制代码
Zygote fork → ActivityThread.main()
  → bindApplication()
  → Application.onCreate()
  → Activity.onCreate() → onStart() → onResume()
  → ViewRootImpl.performTraversals()
     → performMeasure() → performLayout() → performDraw()
  → RenderThread 提交首帧到 SurfaceFlinger
  → mWindowSession.finishDrawing()  ← Binder 通知 WMS

5 首帧就绪 → 触发移除

WMS 收到首帧就绪信号:

java 复制代码
// 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() 内部判断:

java 复制代码
// 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 的简单场景

scss 复制代码
应用首帧绘制完成
  → onFirstWindowDrawn()
  → removeStartingWindow()        ← 不在 collect 中,立即执行
  → WindowState 从窗口树删除       ← 输入层:AppWindow 成为栈顶
  → updateInputWindows()
  → IPC → Shell.removeStartingWindow()
  → playRevealAnimation?
     ├─ SplashScreen: 退出动画播放中... → 动画结束 → leash.remove() → Surface 释放
     └─ Snapshot: 直接移除(无 leash)

这是最简单的路径:应用窗口已就绪,启动窗口使命完成。但注意即使没有 Transition,SplashScreen 的退出动画期间也是 WindowState 已删、Surface 仍在 leash 上的状态。

6.3 有 Transition 的场景(重点)

scss 复制代码
应用首帧绘制完成
  → 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 应用窗口可见性与启动窗口的配合

makefile 复制代码
启动窗口存在期间:
┌─────────────────────────────────┐
│  启动窗口 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:

java 复制代码
// 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 完整时间线

按真实系统行为的顺序:

scss 复制代码
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。

scss 复制代码
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 视觉层

scss 复制代码
removeStartingWindow() 之后、leash 释放之前:

视觉层:                    输入层:
┌──────────────────┐       ┌──────────────────┐
│ splash 还在显示   │       │ AppWindow (栈顶) │  ← 点击直接进入 App
│ (leash 控制动画)  │       │ LauncherWindow    │
└──────────────────┘       └──────────────────┘

用户感觉: "我点在 splash 上,但其实点的是下面的 App"

视觉层级 ≠ 输入层级。这是系统刻意做的优化,用来避免启动动画期间 UI 无法操作。


8 架构总结

css 复制代码
┌─────────────────────────────────────────────────────────────┐
│  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 窗口管理中最典型的跨进程协作场景。

相关推荐
哈基米南北1 小时前
PhoneWindow、DecorView 和 ViewRootImpl 的关系与工作原理
源码
哈基米南北5 天前
ViewRootImpl 事件分发责任链分析
源码
Amos_Web6 天前
Rspack 源码解析(十二):JavaScript Chunk 是如何被渲染出来的
前端·rust·源码
淡海水6 天前
13-04-面试-源码级深度追问链
数据结构·unity·面试·c#·游戏引擎·源码·il2cpp
斑鸠喳喳9 天前
可重入锁 ReentrantLock
java·源码
大胡子大叔11 天前
YOLO26(极速目标检测) + SAM3(精准掩码生成)附源码
目标检测·源码·流水线·分割·yolo26·sam3
源码宝13 天前
一套云PACS商用授权源码
源码·影像系统·云影像
Java后端的Ai之路13 天前
【AI大模型Harness】-Codex源码深度解读
人工智能·源码·ai大模型·codex·harness
vx-程序开发14 天前
【毕设分享】springboot废品上门回收微信小程序---附源码94491
java·spring boot·毕业设计·源码·课程设计·毕设·大作业