在 Android 应用启动过程中,SplashScreen(启动界面)扮演着至关重要的角色。它不仅用于提升用户体验,避免黑屏现象,还涉及 System UI、WindowManagerService (WMS)、ActivityTaskManagerService (AMS) 等多个核心组件的协作。
本文将深入分析 Android SplashScreen 的显示原理、跨进程机制、优化策略,最终提升应用的启动速度和用户体验。
1. Android 启动界面的显示过程
在 Android 应用启动时,系统会采取一系列策略确保界面平滑过渡。整个过程可以分为 Launcher 动画、Starting Window(启动窗口)、Task Snapshot(任务快照)、Activity 渲染 四个阶段。
(1) Launcher 过渡动画
当用户点击应用图标时:
- Launcher(桌面应用) 负责播放 应用图标的放大动画,以提升启动的流畅度。
- 这个动画只是一个过渡,不涉及 App 界面加载。
(2) Starting Window(启动窗口)
- 由 System UI 进程 负责创建,并由
TaskOrganizer
进行管理。 - 作用:填充 App 启动期间的空白时间,避免黑屏现象。
- 显示内容可以是:
- 纯色窗口(默认应用主题背景)
- App Icon(用于视觉衔接)
- 自定义
SplashScreen
(支持动画)
(3) Task Snapshot(任务快照)
- 在 热启动(App 仍然驻留在后台)时,系统会直接显示最近任务的截图 (
Task Snapshot
),而不是重新加载界面。 - 这样可以减少冷启动时间,提高响应速度。
- Task Snapshot 由 WMS(WindowManagerService) 维护,并在 ActivityRecord 级别控制其显示。
(4) Activity 启动并绘制界面
ActivityTaskManagerService (AMS)
负责启动应用的Activity
。ActivityRecord
调用showStartingWindow()
,请求 System UI 创建Starting Window
。- 当
Activity
首次绘制完成 后:onFirstWindowDrawn()
触发,通知ActivityRecord
移除Starting Window
。SplashScreen
退出,显示 Activity 主界面。
2. SplashScreen 是如何创建和管理的?
在 SplashScreen
显示的过程中,涉及 System UI、WindowManagerService (WMS)、ActivityTaskManagerService (AMS) 以及 应用进程。下面是具体流程:
(1) SystemServer 触发 Activity 启动
startActivityLocked() -> showStartingWindow()
- 通过
TaskOrganizerController.addStartingWindow()
向System UI
进程发送创建请求。
(2) System UI 进程创建 SplashScreen
- System UI 负责创建 SplashScreen
SplashscreenWindowCreator.addSplashScreenStartingWindow()
- 解析应用
Theme
,创建SplashScreenView
- 通过
WindowManagerService.addWindow()
将SplashScreen
添加到WMS
public class SplashscreenWindowCreator extends AbsSplashWindowCreator {
void addSplashScreenStartingWindow(StartingWindowInfo windowInfo,
@StartingWindowInfo.StartingWindowType int suggestType) {
final Context context = SplashscreenContentDrawer.createContext(...);
final WindowManager.LayoutParams params = SplashscreenContentDrawer.createLayoutParameters(...);
final FrameLayout rootLayout = new FrameLayout(context);
mSplashscreenContentDrawer.createContentView(context, suggestType, windowInfo, viewSupplier::setView, ...);
addWindow(taskId, windowInfo.appToken, rootLayout, display, params, suggestType);
}
}
(3) SplashScreen 退出
onFirstWindowDrawn()
触发removeStartingWindow()
。- 默认动画:淡入淡出
Fade Out
方式移除SplashScreen
。 - 自定义动画:可通过
setOnExitAnimationListener()
实现动画过渡。
removeStartingWindow可以直接看调用堆栈, 里面同时会触发copySplashScreenView:
copySplashScreenView:765, TaskOrganizerController (com.android.server.wm)
requestCopySplashScreen:2818, ActivityRecord (com.android.server.wm)
transferSplashScreenIfNeeded:2807, ActivityRecord (com.android.server.wm)
removeStartingWindow:2946, ActivityRecord (com.android.server.wm)
onFirstWindowDrawn:6956, ActivityRecord (com.android.server.wm)
performShowLocked:4422, WindowState (com.android.server.wm)
commitFinishDrawingLocked:257, WindowStateAnimator (com.android.server.wm)
lambda$new$8:1015, DisplayContent (com.android.server.wm)
$r8$lambda$-dBz3LtsWovWVT0SE5m__EwNfT4:0, DisplayContent (com.android.server.wm)
accept:0, DisplayContent$$ExternalSyntheticLambda32 (com.android.server.wm)
apply:2832, WindowContainer$ForAllWindowsConsumerWrapper (com.android.server.wm)
apply:2822, WindowContainer$ForAllWindowsConsumerWrapper (com.android.server.wm)
applyInOrderWithImeWindows:4656, WindowState (com.android.server.wm)
copySplashScreenView会跨进程到systemui进程.
(4) SplashScreen 传递到应用进程
System UI
进程执行copySplashScreenView()
生成Parcelable
。- 通过
AMS.onSplashScreenViewCopyFinished()
将SplashScreenView
传递给 应用进程。 - 应用进程的
ActivityThread
负责最终展示SplashScreenView
,确保平滑过渡。
因此,SplashScreenView
最初在 System UI 进程绘制,随后通过copySplashScreenView()
传递给 应用进程 并在ActivityThread
中显示,确保过渡流畅。
应用进程接收并显示
public final class ActivityThread extends ClientTransactionHandler {
public void handleAttachSplashScreenView(ActivityClientRecord r, SplashScreenViewParcelable parcelable, SurfaceControl startingWindowLeash) {
final SplashScreenView view = new SplashScreenView.Builder(r.activity).createFromParcel(parcelable).build();
decorView.addView(view);
// 这个函数看后面有分析
syncTransferSplashscreenViewTransaction(view, r.token, decorView, startingWindowLeash);
}
}
3. 什么是 Leash?为什么它很重要?
什么是 Leash
?
在 Android 窗口管理系统中,Leash
(皮带) 是 SurfaceControl
的一个代理对象,用于 控制和管理窗口(Surface),但不会改变窗口的归属。
简单来说:
Leash
是 系统为某个窗口(Surface)创建的一个控制句柄,开发者或系统可以通过它平滑控制窗口的动画、缩放、移动等操作,而不影响窗口的生命周期。Leash
允许WindowManager
在 不同进程 之间控制窗口,如System UI
在SplashScreen
过渡阶段管理Activity
的窗口。
为什么 Leash
很重要?
在 Android 11(API 30)之后,Google 引入 Leash
机制,目的是:
-
增强窗口动画的平滑性:
Leash
允许WindowManager
预先捕获Activity
的窗口,并在System UI
进程中控制其动画,而不是直接交给App
进程处理,这样可以避免启动时的卡顿。
-
跨进程窗口管理:
- 例如,
SplashScreen
由System UI
进程创建,然后在Activity
窗口加载完成后,将SplashScreen
平滑交给App
进程,这个过程就依赖Leash
。
- 例如,
Leash
在 SplashScreen
过渡中的作用
在 Android 12+(API 31),SplashScreen
退出时涉及 Leash
,具体流程如下:
-
创建
SplashScreen
窗口System UI
创建SplashScreen
(Starting Window
)。SplashScreen
绑定SurfaceControl
并创建Leash
:SurfaceControl leash = new SurfaceControl.Builder() .setName("SplashScreen Leash") .build();
-
窗口
Leash
绑定到Activity
- 当
Activity
的Surface
创建完成,系统会通过Leash
控制SplashScreen
平滑过渡到Activity
。
- 当
-
动画控制和释放
Leash
Leash
允许System UI
控制SplashScreen
的 缩放、淡入淡出、滑动退出 等动画:transaction.setAlpha(leash, 0f); transaction.apply();
- 动画完成后,
Leash
被销毁,窗口归还给Activity
进程。
Leash
相关的源码分析
在 ActivityThread
里,Leash
被用于同步 SplashScreen
过渡:
private void syncTransferSplashscreenViewTransaction(
SplashScreenView view, IBinder token, View decorView, @NonNull SurfaceControl startingWindowLeash) {
final SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
// 隐藏 `SplashScreen` 的 `Leash`
transaction.hide(startingWindowLeash);
// 确保 `Activity` 窗口已经准备好
decorView.getViewRootImpl().applyTransactionOnDraw(transaction);
// 交接窗口控制权
view.syncTransferSurfaceOnDraw();
// 通知 `System UI` 移除 `SplashScreen`
decorView.postOnAnimation(() -> reportSplashscreenViewShown(token, view));
}
- 这里
transaction.hide(startingWindowLeash);
代表 隐藏SplashScreen
,然后交给Activity
来控制新窗口。
Leash
就像溜狗的皮带,System UI 牵着 SplashScreen
(窗口),在 Activity
完全准备好之前,它可以控制窗口的动画、移动、隐藏等;当 Activity
窗口稳定后,Leash
被解除,窗口交给应用本身来管理。
4. 应用如何优化启动速度?
(1) 使用 getSplashScreen()
在 高版本Android,推荐使用:
SplashScreen splashScreen = getSplashScreen();
getSplashScreen() 是 Activity 新增的方法,已经取代了 installSplashScreen()。
内部逻辑:如果 SplashScreen 还未创建,则调用 getOrCreateSplashScreen() 进行初始化:
private SplashScreen getOrCreateSplashScreen() {
synchronized (this) {
if (mSplashScreen == null) {
mSplashScreen = new SplashScreen.SplashScreenImpl(this);
}
return mSplashScreen;
}
}
(2) 让 SplashScreen
退出更平滑
getSplashScreen().setOnExitAnimationListener(splashScreenView -> {
splashScreenView.animate().alpha(0f).setDuration(300).withEndAction(() -> {
splashScreenView.remove();
});
});
(3) 启用 Baseline Profile
Baseline Profile 是一组预定义的类和方法规则,告诉 Android 运行时 提前编译关键代码路径,避免应用首次启动时依赖 JIT 编译. 具体可以参考其他文章。
在 gradle.properties
添加:
android.experimental.art-profile-r8-rewriting=true
这行代码开启 Baseline Profile,并确保 R8(优化编译器)可以正确识别并优化 onCreate() 相关的方法。
然后运行:
./gradlew generateBaselineProfile
Baseline Profile其实也不是什么新技术,其实就是结合ART虚拟机特点,记录应用的热点代码,最终被放在 APK 。
Baseline Profile 介于 JIT 和 AOT 之间,它:
- 定义应用启动时的关键代码路径(包括 onCreate())
- 强制系统在安装或更新应用时提前编译这些代码
- 避免 JIT 重新编译,从而减少 onCreate() 执行时间
SplashScreen
期间的 onCreate()
执行可能会更快,提升启动速度。
5. 总结
SplashScreen
由System UI
进程创建,最终通过Leash
交给应用进程
显示,实现平滑过渡。Leash
机制确保SplashScreen
退出时不会闪屏或黑屏,提高流畅度。- 读者也可以使用
WinScope
分析SplashScreen
生命周期,可优化Activity
启动过程。 - 开发者可以通过
getSplashScreen()
、Baseline Profile
等优化SplashScreen
的性能,减少启动延迟。