忽然有一天,我想要做一件事:去代码中去验证那些曾经被“灌输”的理论。
-- 服装学院的IT男
根据自己的视觉体验和桌面启动应用的视觉总结知道在桌面点击“电话”图标启动应用整个视觉效果中,在开始“app_transition”动画后不久,就会触发创建一个 Splash Screen 的窗口,本文就是介绍这个Splash Screen。
StartWindow 定义了多种类型,这个 Splash Screen 其实是其中的一种,当前只关注冷启动用到的 “Splash Screen”。
1. StartWindow 介绍
我是 APP 入坑的,写过 APP 同学都知道稍微有规模的应用启动的第一个都是 一个 SplashActivity ,也就是常说的“闪屏页”,而不是会是有复杂业务的 "MainActivity" 。
这个 SplashActivity 的内容一般也很简单,要么是一个图片,或者是一个 logo,有的就是简短的一些文案。比如微信启动的时候会先显示一个“月球”的图片。 为什么要先显示这个 SplashActivity ?
因为包含业务的 MainActivity 要正常显示需要做一些初始化工作,比如会依赖很多数据,而这些数据的加载需要时间,大部分情况是需要等网络请求返回的数据填充页面,而这个时间是不确定的,那么在数据返回之前的页面可能会很杂乱,不美观。如果用户打开一个应用,几秒内看到的一直是一个这样的一个界面,是很影响用户体验的。
为了提升用户体验,就会先写一个 SplashActivity 在这里做一些初始化操作,一切准备就绪后再进入 MainActivity 。
这是 App 端的处理,再回到 WMS 这个角度,冷启动一个应用时,应用的第一个窗口从添加到绘制完成是需要时间的,为了提升这个过程的用户体验, google 的设计也是先显示一个 StartWindow 也就是本次要介绍的 Splash Screen 窗口。
那么为了和后续看到的代码的统一,本篇后续出现的 StartWindow 和 Splash Screen 都是指同一个窗口。
基于这些分析,那么对于 StartWindow 的分析,我认为需要搞懂以下4点:
-
- 存在的意义
-
- 添加的时机
-
- 移除的时机
-
- 窗口的内容
第一点其实已经知道了,至于 StartWindow 添加和删除的时机,后面会从代码中看,现在先抛开代码冲业务角度分析,既然是为了过渡,那么添加的时机肯定是在应用窗口添加前,移除的时机必然是在真正的应用窗口显示之前。
至于 StartWindow 的内容,默认就是个应用 logo , 但是也支持一些自定义效果后续会介绍。
先通过以下命令打开 StartWindow 对应 ProtoLog
adb shell wm logging enable-text WM_SHELL_STARTING_WINDOW WM_DEBUG_STARTING_WINDOW
然后在桌面点击电话图标启动应用在抓取的日志中可以看到以下log:
// ActivityRecord 的挂载 (需要打开 WM_DEBUG_ADD_REMOVE)
WindowManager: Adding activity ActivityRecord{735a13f u0 com.google.android.dialer/.extensions.GoogleDialtactsActivity} t32} to task Task{e1c6d6a #32 type=standard ......
// 创建闪屏页需要的数据类
WindowManager: Creating SplashScreenStartingData
// 打印AddStartingWindow实例和这次用到的 StartingData,其实就是上面创建的 SplashScreenStartingData
WindowManager: Add starting com.android.server.wm.ActivityRecord$AddStartingWindow@b971d3: startingData=com.android.server.wm.SplashScreenStartingData@bd5f110
// 获取当前 StartWindow 的类型
ShellStartingWindow: preferredStartingWindowType newTask=true, taskSwitch=true, processRunning=false, allowTaskSnapshot=true, activityCreated=false, isSolidColorSplashScreen=false, legacySplashScreen=false, activityDrawn=false, topIsHome=false
// 为哪个Activity 添加StartingWindow
ShellStartingWindow: addSplashScreen for package: com.google.android.dialer with theme: 7f160232 for task: 33, suggestType: 1
// 打印几个窗口参数
ShellStartingWindow: getWindowAttrs: window attributes color: 0, replace icon: false
ShellStartingWindow: processAdaptiveIcon: FgMainColor=ff166cfe, BgMainColor=fff8f8f8, IsBgComplex=false, FromCache=true, ThemeColor=ffffffff
ShellStartingWindow: isRgbSimilarInHsv a:ffffffff, b:fff8f8f8, contrast ratio:1.062016
ShellStartingWindow: processAdaptiveIcon: choose fg icon
// StartWindow的窗口挂载到窗口树
WindowManager: addWindow: ActivityRecord{ac4b678 u0 com.google.android.dialer/.extensions.GoogleDialtactsActivity} t33} startingWindow=Window{ff6ec31 u0 Splash Screen com.google.android.dialer}
// 开始要执行移除 StartingWindow 的逻辑了,并打印堆栈
WindowManager: Schedule remove starting ActivityRecord{ac4b678 u0 com.google.android.dialer/.extensions.GoogleDialtactsActivity} t33} startingWindow=null startingView=null Callers=com.android.server.wm.ActivityRecord.removeStartingWindow:2715 com.android.server.wm.ActivityRecord.onFirstWindowDrawn:6456 com.android.server.wm.WindowState.performShowLocked:4710 com.android.server.wm.WindowStateAnimator.commitFinishDrawingLocked:291 com.android.server.wm.DisplayContent.lambda$new$8$com-android-server-wm-DisplayContent:998
// 可以开始移除 StartWindow 的 Surface 了
Line 5389: 09-29 19:14:21.373 23407 23497 V ShellStartingWindow: Task start finish, remove starting surface for task: 28
// 移除 splash screen
Line 5390: 09-29 19:14:21.373 23407 23497 V ShellStartingWindow: Removing splash screen window for task: 28
// 开始移除 StartWindow 动画
Line 5391: 09-29 19:14:21.373 23407 23497 V ShellStartingWindow: applyExitAnimation delayed: 0
可以看到log覆盖了从 add 到 remove 整个周期,那么只需要把日志的调用栈全部联系起来,那么就可以分析到 startWindow 完整的流程了。
StartWindow 的整个流程其实只是是冷启动 Activity 启动流程的中间一小块,Activity 启动流程流程之前已经分析过了,现在的主题是 StartingWindow ,所以还需要再简单的过一遍启动流程 StartingWindow 的处理。
上面日志里 ShellStartingWindow 打印的进程 ID 是 systemui 进程,这是因为 StartingWindow 的内容其实是在 systemui 进程处理的。
2. StartWindow 的添加流程
2.1 system_server进程前期处理
addStartingWindow 方法定义在 TaskOrganizerController 下,加上异常后获取到 system_server 进程打印了这么一个堆栈:
09-29 19:30:11.190 30184 32263 E biubiubiu: TaskOrganizerController addStartingWindow: ActivityRecord{e184810 u0 com.google.android.dialer/.extensions.GoogleDialtactsActivity} t8}
09-29 19:30:11.190 30184 32263 E biubiubiu: java.lang.Exception
09-29 19:30:11.190 30184 32263 E biubiubiu: at com.android.server.wm.TaskOrganizerController.addStartingWindow(TaskOrganizerController.java:495)
09-29 19:30:11.190 30184 32263 E biubiubiu: at com.android.server.wm.StartingSurfaceController.createSplashScreenStartingSurface(StartingSurfaceController.java:85)
09-29 19:30:11.190 30184 32263 E biubiubiu: at com.android.server.wm.SplashScreenStartingData.createStartingSurface(SplashScreenStartingData.java:36)
09-29 19:30:11.190 30184 32263 E biubiubiu: at com.android.server.wm.ActivityRecord$AddStartingWindow.run(ActivityRecord.java:2436)
09-29 19:30:11.190 30184 32263 E biubiubiu: at com.android.server.wm.ActivityRecord.scheduleAddStartingWindow(ActivityRecord.java:2403)
09-29 19:30:11.190 30184 32263 E biubiubiu: at com.android.server.wm.ActivityRecord.addStartingWindow(ActivityRecord.java:2382)
09-29 19:30:11.190 30184 32263 E biubiubiu: at com.android.server.wm.ActivityRecord.showStartingWindow(ActivityRecord.java:7039)
09-29 19:30:11.190 30184 32263 E biubiubiu: at com.android.server.wm.ActivityRecord.showStartingWindow(ActivityRecord.java:6999)
09-29 19:30:11.190 30184 32263 E biubiubiu: at com.android.server.wm.StartingSurfaceController.showStartingWindow(StartingSurfaceController.java:197)
09-29 19:30:11.190 30184 32263 E biubiubiu: at com.android.server.wm.Task.startActivityLocked(Task.java:5233)
09-29 19:30:11.190 30184 32263 E biubiubiu: at com.android.server.wm.ActivityStarter.startActivityInner(ActivityStarter.java:1912)
09-29 19:30:11.190 30184 32263 E biubiubiu: at com.android.server.wm.ActivityStarter.startActivityUnchecked(ActivityStarter.java:1668)
09-29 19:30:11.190 30184 32263 E biubiubiu: at com.android.server.wm.ActivityStarter.executeRequest(ActivityStarter.java:1223)
09-29 19:30:11.190 30184 32263 E biubiubiu: at com.android.server.wm.ActivityStarter.execute(ActivityStarter.java:709)
09-29 19:30:11.190 30184 32263 E biubiubiu: at com.android.server.wm.ActivityTaskManagerService.startActivityAsUser(ActivityTaskManagerService.java:1250)
09-29 19:30:11.190 30184 32263 E biubiubiu: at com.android.server.wm.ActivityTaskManagerService.startActivityAsUser(ActivityTaskManagerService.java:1213)
09-29 19:30:11.190 30184 32263 E biubiubiu: at com.android.server.wm.ActivityTaskManagerService.startActivity(ActivityTaskManagerService.java:1188)
09-29 19:30:11.190 30184 32263 E biubiubiu: at android.app.IActivityTaskManager$Stub.onTransact(IActivityTaskManager.java:893)
09-29 19:30:11.190 30184 32263 E biubiubiu: at com.android.server.wm.ActivityTaskManagerService.onTransact(ActivityTaskManagerService.java:5244)
09-29 19:30:11.190 30184 32263 E biubiubiu: at android.os.Binder.execTransactInternal(Binder.java:1280)
09-29 19:30:11.190 30184 32263 E biubiubiu: at android.os.Binder.execTransact(Binder.java:1244)
整理后如下:
ActivityTaskManagerService::startActivity
ActivityTaskManagerService::startActivityAsUser
ActivityTaskManagerService::startActivityAsUser
ActivityStarter::execute
ActivityStarter::executeRequest -- 构建 ActivityRecord
ActivityStarter::startActivityUnchecked
ActivityStarter::startActivityInner -- 关键函数startActivityInner
ActivityStarter::getOrCreateRootTask -- 创建或者拿到Task
ActivityStarter::setNewTask -- 将task与activityRecord 绑定
当前关注 Task::startActivityLocked -- **本文分析的StartWindow流程**
StartingSurfaceController::showStartingWindow
ActivityRecord::showStartingWindow
ActivityRecord::showStartingWindow
ActivityRecord::showStartingWindow
ActivityRecord::scheduleAddStartingWindow
ActivityRecord.AddStartingWindow::run
SplashScreenStartingData::createStartingSurface
StartingSurfaceController::createSplashScreenStartingSurface
TaskOrganizerController::addStartingWindow -- 开始跨进程
RootWindowContainer::resumeFocusedTasksTopActivities --显示Activity(触发进程创建)
在 Activity 启动流程调用链中关注的 StartWindow 相关流程流程发现在 ActivityStarter::startActivityInner 就触发了,那就是和 Task 创建的同级调用顺序了。另外还可以确定StartWindow 的逻辑处理是在 Task 动画创建之前的。
那就从 ActivityStarter::startActivityInner 这个方法开始看。
# ActivityStarter
int startActivityInner(final ActivityRecord r, ActivityRecord sourceRecord......) {
......
if (mTargetRootTask == null) {
// 创建Task
mTargetRootTask = getOrCreateRootTask(mStartActivity, mLaunchFlags, targetTask,
mOptions);
}
......
将task与activityRecord 绑定
setNewTask(taskToAffiliate);
......
// 重点* startWindow流程
mTargetRootTask.startActivityLocked(mStartActivity, topRootTask, newTask, isTaskSwitch,
mOptions, sourceRecord);
......
}
这里可以看到做了三件事:
-
- 创建 Task
-
- 把 Activity 挂载到 Task 下面
-
- 添加 StartWindow
前面2个处理之前的文章已经分析了,当前关注第三点
# Task
void startActivityLocked(ActivityRecord r, @Nullable Task topTask, boolean newTask,
boolean isTaskSwitch, ActivityOptions options, @Nullable ActivityRecord sourceRecord) {
......
if (r.mLaunchTaskBehind) {
......
} else if (SHOW_APP_STARTING_PREVIEW && doShow) {
......
Task baseTask = r.getTask();
......
final ActivityRecord prev = baseTask.getActivity(
a -> a.mStartingData != null && a.showToCurrentUser());
mWmService.mStartingSurfaceController.showStartingWindow(r, prev, newTask,
isTaskSwitch, sourceRecord);
}
......
}
当前场景这个 Task 就是“电话”应用的刚创建的 Task 。 "r.mLaunchTaskBehind"这条件是启动应用的时候用户看不见直接呈现在最近任务列表里,这种场景很少见,不用管,主要看下面的条件。SHOW_APP_STARTING_PREVIEW是个常量默认为true,可以设置为false表示不显示startWindow,doShow这个变量默认为true。那么主要看最后的那段逻辑即可。
# StartingSurfaceController
void showStartingWindow(ActivityRecord target, ActivityRecord prev,
boolean newTask, boolean isTaskSwitch, ActivityRecord source) {
if (mDeferringAddStartingWindow) {
addDeferringRecord(target, prev, newTask, isTaskSwitch, source);
} else {
// 不延迟,正常走这
target.showStartingWindow(prev, newTask, isTaskSwitch, true /* startActivity */,
source);
}
}
mDeferringAddStartingWindow 可以先不管,根据代码的注释是启动多个活动时推迟添加启动窗口时为 true 。
# ActivityRecord
// 调用的是这个,最后一个参数传递null继续调用
void showStartingWindow(ActivityRecord prev, boolean newTask, boolean taskSwitch,
boolean startActivity, ActivityRecord sourceRecord) {
showStartingWindow(prev, newTask, taskSwitch, isProcessRunning(), startActivity,
sourceRecord, null /* candidateOptions */);
}
void showStartingWindow(ActivityRecord prev, boolean newTask, boolean taskSwitch,
boolean processRunning, boolean startActivity, ActivityRecord sourceRecord,
ActivityOptions candidateOptions) {
......
final boolean scheduled = addStartingWindow(packageName, resolvedTheme,
prev, newTask || newSingleActivity, taskSwitch, processRunning,
allowTaskSnapshot(), activityCreated, mSplashScreenStyleSolidColor, allDrawn);
......
}
// 调用这个
@VisibleForTesting
boolean addStartingWindow(String pkg, int resolvedTheme, ActivityRecord from, boolean newTask,
boolean taskSwitch, boolean processRunning, boolean allowTaskSnapshot,
boolean activityCreated, boolean isSimple,
boolean activityAllDrawn) {
......
// 注意* 在创建 SplashScreenStartingData 并输出日志
ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "Creating SplashScreenStartingData");
mStartingData = new SplashScreenStartingData(mWmService, resolvedTheme, typeParameter);
// * 执行下一步
scheduleAddStartingWindow();
return true;
}
void scheduleAddStartingWindow() {
// 就是执行其 run 方法
mAddStartingWindow.run();
}
private final AddStartingWindow mAddStartingWindow = new AddStartingWindow();
scheduleAddStartingWindow 方法内部是执行了一个Runnable
# ActivityRecord.AddStartingWindow
private class AddStartingWindow implements Runnable {
@Override
public void run() {
// Can be accessed without holding the global lock
final StartingData startingData;
synchronized (mWmService.mGlobalLock) {
......
// mStartingData前面看到是SplashScreenStartingData类型
startingData = mStartingData;
......
}
// 打印log
ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "Add starting %s: startingData=%s",
this, startingData);
// 定义 StartWindow的surface,注意这里是自己定义的StartingSurface,不等于图层的Surface
StartingSurfaceController.StartingSurface surface = null;
try {
// 创建
surface = startingData.createStartingSurface(ActivityRecord.this);
} catch (Exception e) {
Slog.w(TAG, "Exception when adding starting window", e);
}
......
}
}
这里创建的 surface 是 StartWindow 模块自己的定义的类。
# SplashScreenStartingData
@Override
StartingSurface createStartingSurface(ActivityRecord activity) {
return mService.mStartingSurfaceController.createSplashScreenStartingSurface(
activity, mTheme);
}
# StartingSurfaceController
StartingSurface createSplashScreenStartingSurface(ActivityRecord activity, int theme) {
synchronized (mService.mGlobalLock) {
final Task task = activity.getTask();
if (task != null && mService.mAtmService.mTaskOrganizerController.addStartingWindow(
task, activity, theme, null /* taskSnapshot */)) {
// 创建返回StartingSurface,但是这个并不是真正的Surface,上面的addStartingWindow内部才会真正的触发
return new StartingSurface(task);
}
}
return null;
}
这块主要还是看 TaskOrganizerController::addStartingWindow 的流程,当前这个方法只要记住 StartingSurface 内部有个目标应用的 Task 就好了。
# TaskOrganizerController
private final LinkedList<ITaskOrganizer> mTaskOrganizers = new LinkedList<>();
boolean addStartingWindow(Task task, ActivityRecord activity, int launchTheme,
TaskSnapshot taskSnapshot) {
// 拿到Task
final Task rootTask = task.getRootTask();
if (rootTask == null || activity.mStartingData == null) {
return false;
}
// * 拿到对应的TaskOrganizer
final ITaskOrganizer lastOrganizer = mTaskOrganizers.peekLast();
if (lastOrganizer == null) {
return false;
}
final StartingWindowInfo info = task.getStartingWindowInfo(activity);
if (launchTheme != 0) {
// 注意这里传递过来了主题
info.splashScreenThemeResId = launchTheme;
}
// 当前逻辑传递过来的taskSnapshot为null
info.taskSnapshot = taskSnapshot;
// make this happen prior than prepare surface
try {
// 重点* 跨进程触发addStartingWindow
lastOrganizer.addStartingWindow(info, activity.token);
} catch (RemoteException e) {
Slog.e(TAG, "Exception sending onTaskStart callback", e);
return false;
}
return true;
}
直到这里都是在system_server进程,但是这里通过ITaskOrganizer调用后面的逻辑,后续的逻辑就是另一个进程了,目前需要这个lastOrganizer这个值到底是哪来的。这个lastOrganizer实际上就是SystemUI进程和 system_server 进程通信的binder类。这块的介绍在【ITaskOrganizer相关逻辑】。目前知道后续的处理进入SystemUI进程在TaskOrganizer下。
2.2 SystemUI进程创建 StartWindow
SystemUI进程这块的调用链整理如下:
ITaskOrganizer::addStartingWindow
ShellTaskOrganizer::addStartingWindow
StartingWindowController::addStartingWindow
StartingWindowTypeAlgorithm::getSuggestedWindowType -- 获取类型
StartingSurfaceDrawer::addSplashScreenStartingWindow -- 核心方法
SplashscreenContentDrawer::createContentView -- 构建StartWindow的参数
SplashscreenContentDrawer::makeSplashScreenContentView
SplashscreenContentDrawer::getWindowAttrs -- 获取应用配置的StartWindow的属性
StartingWindowViewBuilder::build -- 构建SplashScreenView
SplashScreenViewSupplier::setView -- 保存SplashScreenView
StartingSurfaceDrawer::addWindow -- addWindow流程
继续看代码:
# TaskOrganizer
private final ITaskOrganizer mInterface = new ITaskOrganizer.Stub() {
@Override
public void addStartingWindow(StartingWindowInfo windowInfo,
IBinder appToken) {
// system_server 跨进程是通过mInterface的对象调用到这
mExecutor.execute(() -> TaskOrganizer.this.addStartingWindow(windowInfo, appToken));
}
......
}
// 具体的处理在这
# ShellTaskOrganizer
private StartingWindowController mStartingWindow;
@Override
public void addStartingWindow(StartingWindowInfo info, IBinder appToken) {
if (mStartingWindow != null) {
mStartingWindow.addStartingWindow(info, appToken);
}
}
# StartingWindowController
private final StartingSurfaceDrawer mStartingSurfaceDrawer;
public void addStartingWindow(StartingWindowInfo windowInfo, IBinder appToken) {
mSplashScreenExecutor.execute(() -> {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "addStartingWindow");
......
// 获取当前 StartWindow 的类型
final int suggestionType = mStartingWindowTypeAlgorithm.getSuggestedWindowType(
windowInfo);
......
if (isSplashScreenType(suggestionType)) {
// 主流程
mStartingSurfaceDrawer.addSplashScreenStartingWindow(windowInfo, appToken,
suggestionType);
}......//忽略
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
});
}
这这个方法就是根据当前信息获取一个 StartWindow 的类型,一共定义了下面3种类型:
# StartingWindowInfo
/**
* Prefer nothing or not care the type of starting window.
* 没有类型
* @hide
*/
public static final int STARTING_WINDOW_TYPE_NONE = 0;
/**
* Prefer splash screen starting window.
* 闪屏类型,冷启动用到
* @hide
*/
public static final int STARTING_WINDOW_TYPE_SPLASH_SCREEN = 1;
/**
* Prefer snapshot starting window.
* 应用快照类型,那肯定不是冷启动,应该是多任务切换时相关
* @hide
*/
public static final int STARTING_WINDOW_TYPE_SNAPSHOT = 2;
当前冷启动场景的类型肯定是 STARTING_WINDOW_TYPE_SPLASH_SCREEN ,根据打印的日志也能确定。
ShellStartingWindow: preferredStartingWindowType newTask=true, taskSwitch=true, processRunning=false, allowTaskSnapshot=true, activityCreated=false, isSolidColorSplashScreen=false, legacySplashScreen=false, activityDrawn=false, topIsHome=false
接下来是核心代码了,这里构建StartWindow的参数。
2.2.1 构建 StartWindow 的参数
这个方法 StartWindow 显示的核心方法,内部做了很多事。
# StartingSurfaceDrawer
void addSplashScreenStartingWindow(StartingWindowInfo windowInfo, IBinder appToken,
@StartingWindowType int suggestType) {
......
// replace with the default theme if the application didn't set
// 拿到主题资源
final int theme = getSplashScreenTheme(windowInfo.splashScreenThemeResId, activityInfo);
// 打印日志
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
"addSplashScreen for package: %s with theme: %s for task: %d, suggestType: %d",
activityInfo.packageName, Integer.toHexString(theme), taskId, suggestType);
......
// 设置主题
context.setTheme(theme);
......
// 开始构建需要的参数
final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.TYPE_APPLICATION_STARTING);
......
// window 动画资源
params.windowAnimations = a.getResourceId(R.styleable.Window_windowAnimationStyle, 0);
......
// 这个token是 system_server 传过来的,也就是目标目标应用的ActivityRecord的token
params.token = appToken;
params.packageName = activityInfo.packageName;
......
// 在这设置name
params.setTitle("Splash Screen " + activityInfo.packageName);
......
// 构建一个 SplashScreenViewSupplier ,内部保存真正是 StartWindow 的 ContentView
final SplashScreenViewSupplier viewSupplier = new SplashScreenViewSupplier();
// 定义一个跟布局,可以理解为 rootView (Activity 有个 rootView 然后下面才是contentView)
final FrameLayout rootLayout = new FrameLayout(mSplashscreenContentDrawer.createViewContextWrapper(context));
rootLayout.setPadding(0, 0, 0, 0);
rootLayout.setFitsSystemWindows(false);
// 定义一个 Runnable, 作为下一帧 Vsync 来临执行的回调。 也是
final Runnable setViewSynchronized = () -> {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "addSplashScreenView");
// waiting for setContentView before relayoutWindow
// 拿到保存在 SplashScreenViewSupplier 下的 contentView
SplashScreenView contentView = viewSupplier.get();
// 获取到 StartingWindowRecord
final StartingWindowRecord record = mStartingWindowRecords.get(taskId);
// If record == null, either the starting window added fail or removed already.
// Do not add this view if the token is mismatch.
if (record != null && appToken == record.mAppToken) {
// if view == null then creation of content view was failed.
if (contentView != null) {
try {
// 重点,把 contentView 设置到 rootView下面
rootLayout.addView(contentView);
} catch (RuntimeException e) {
Slog.w(TAG, "failed set content view to starting window "
+ "at taskId: " + taskId, e);
contentView = null;
}
}
// 把 contentView 保存进去,copySplashScreenView 方法使用 (目前没发现场景,可以忽略)
record.setSplashScreenView(contentView);
}
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
};
......
// 重点* 1. 创建具体的内容
mSplashscreenContentDrawer.createContentView(context, suggestType, windowInfo,
viewSupplier::setView, viewSupplier::setUiThreadInitTask);
try {
// 重点* 2. 主流程 (2.3 小节介绍)
if (addWindow(taskId, appToken, rootLayout, display, params, suggestType)) {
......
} else ......
} ......
......
}
这里会打印第2个ShellStartingWindow log
这里有2个重点:
-
- 构建 StartWindow 的 ContentView
-
- 构建 window 参数然后执行 addWindow 流程
StartWindow 的 addWindow 流程 在 2.3 小节看, 下面先看一下怎么构建 StartWindow 的 ContentView 。
2.2.2 构建startWindow的ContentView
根据调用看 SplashscreenContentDrawer 的方法,注意第4个参数,传进来的是一个回调 SplashScreenViewSupplier::setView ,最后创建的 contentView 传递到这个回调中了。
# SplashscreenContentDrawer
void createContentView(Context context, @StartingWindowType int suggestType,
StartingWindowInfo info, Consumer<SplashScreenView> splashScreenViewConsumer,
Consumer<Runnable> uiThreadInitConsumer) {
mSplashscreenWorkerHandler.post(() -> {
SplashScreenView contentView;
try {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "makeSplashScreenContentView");
// 重点* 创建contentView
contentView = makeSplashScreenContentView(context, info, suggestType,
uiThreadInitConsumer);
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
} catch (RuntimeException e) {
Slog.w(TAG, "failed creating starting window content at taskId: "
+ info.taskInfo.taskId, e);
contentView = null;
}
// contentView就被设置到对SplashScreenViewSupplier中了
splashScreenViewConsumer.accept(contentView);
});
}
private SplashScreenView makeSplashScreenContentView(Context context, StartingWindowInfo info,
@StartingWindowType int suggestType, Consumer<Runnable> uiThreadInitConsumer) {
updateDensity();
// 重点* 属性参数在这里获取
getWindowAttrs(context, mTmpAttrs);
mLastPackageContextConfigHash = context.getResources().getConfiguration().hashCode();
final Drawable legacyDrawable = suggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN
? peekLegacySplashscreenContent(context, mTmpAttrs) : null;
final ActivityInfo ai = info.targetActivityInfo != null
? info.targetActivityInfo
: info.taskInfo.topActivityInfo;
final int themeBGColor = legacyDrawable != null
? getBGColorFromCache(ai, () -> estimateWindowBGColor(legacyDrawable))
: getBGColorFromCache(ai, () -> peekWindowBGColor(context, mTmpAttrs));
// 最终构造出一个SplashScreenView返回
return new StartingWindowViewBuilder(context, ai)
.setWindowBGColor(themeBGColor)
.overlayDrawable(legacyDrawable)
.chooseStyle(suggestType)
.setUiThreadInitConsumer(uiThreadInitConsumer)
.setAllowHandleSolidColor(info.allowHandleSolidColorSplashScreen())
.build();
}
上面这2个方法,首先是调用 makeSplashScreenContentView 返回一个真正的 SplashScreenView ,然后再通过回调的方式设置到 SplashScreenViewSupplier 中保存,
至于 SplashScreenView 构造逻辑也不复杂,主要是在SplashscreenContentDrawer::getWindowAttrs方法中获取主题中的资源,然后在通过build方式构建,这里看看getWindowAttrs方法。
2.2.2.1 StartWindow 的属性
这个方法会获取 StartWindow 的属性,如果需要修改 StartWindow 的显示,可以在应用中定义一个主题资源,修改上面代码里的这些属性达到不同的效果。
# SplashscreenContentDrawer
private static void getWindowAttrs(Context context, SplashScreenWindowAttrs attrs) {
final TypedArray typedArray = context.obtainStyledAttributes(
com.android.internal.R.styleable.Window);
attrs.mWindowBgResId = typedArray.getResourceId(R.styleable.Window_windowBackground, 0);
attrs.mWindowBgColor = safeReturnAttrDefault((def) -> typedArray.getColor(
R.styleable.Window_windowSplashScreenBackground, def),
Color.TRANSPARENT);
attrs.mSplashScreenIcon = safeReturnAttrDefault((def) -> typedArray.getDrawable(
R.styleable.Window_windowSplashScreenAnimatedIcon), null);
attrs.mBrandingImage = safeReturnAttrDefault((def) -> typedArray.getDrawable(
R.styleable.Window_windowSplashScreenBrandingImage), null);
attrs.mIconBgColor = safeReturnAttrDefault((def) -> typedArray.getColor(
R.styleable.Window_windowSplashScreenIconBackgroundColor, def),
Color.TRANSPARENT);
typedArray.recycle();
// 打印log
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
"getWindowAttrs: window attributes color: %s, replace icon: %b",
Integer.toHexString(attrs.mWindowBgColor), attrs.mSplashScreenIcon != null);
}
这里打印第3个ShellStartingWindow log,看出来用的就是默认的主题。
ShellStartingWindow: getWindowAttrs: window attributes color: 0, replace icon: false
另外 StartingWindowViewBuilder::build 内部会触发 StartingWindowViewBuilder::processAdaptiveIcon ,和 StartingWindowViewBuilder::isRgbSimilarInHsv 这里会打印d第4,5,6g个ShellStartingWindow log。不过代码量太多和对于分析add流程也不是特别的重要,所以就不看具体代码了。
2.2.2.2 构建 SplashScreenView
StartingWindowViewBuilder 通过 build 模式会构建返回一个 SplashScreenView 对象,具体的就不看了,主要关注一下3个输出 proto 日志的地方。
这里就是看一下 StartingWindowViewBuilder 的几个 build 模式是咋处理的。
# StartingWindowViewBuilder
SplashScreenView build() {
// 定义图标Drawable
Drawable iconDrawable;
if (mSuggestType == STARTING_WINDOW_TYPE_SOLID_COLOR_SPLASH_SCREEN
|| mSuggestType == STARTING_WINDOW_TYPE_LEGACY_SPLASH_SCREEN) {
......
} else if (mTmpAttrs.mSplashScreenIcon != null) {
......
} else {
// 计算图标缩放比例
final float iconScale = (float) mIconSize / (float) mDefaultIconSize;
// 获取设备的密度DPI
final int densityDpi = mContext.getResources().getConfiguration().densityDpi;
// 计算缩放后图标DPI
final int scaledIconDpi =
(int) (0.5f + iconScale * densityDpi * NO_BACKGROUND_SCALE);
// Trace
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "getIcon");
// 获取图标
iconDrawable = mHighResIconProvider.getIcon(
mActivityInfo, densityDpi, scaledIconDpi);
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
if (!processAdaptiveIcon(iconDrawable)) {
// 正常没有这个日志打印,说明不执行进来
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
"The icon is not an AdaptiveIconDrawable");
......
}
}
// 使用图标大小、图标绘制对象和UI线程初始化任务填充视图并返回
return fillViewWithIcon(mFinalIconSize, mFinalIconDrawables, mUiThreadInitTask);
}
# StartingWindowViewBuilder
private boolean processAdaptiveIcon(Drawable iconDrawable) {
if (!(iconDrawable instanceof AdaptiveIconDrawable)) {
return false;
}
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "processAdaptiveIcon");
final AdaptiveIconDrawable adaptiveIconDrawable = (AdaptiveIconDrawable) iconDrawable;
final Drawable iconForeground = adaptiveIconDrawable.getForeground();
final ColorCache.IconColor iconColor = mColorCache.getIconColor(
mActivityInfo.packageName, mActivityInfo.getIconResource(),
mLastPackageContextConfigHash,
() -> new DrawableColorTester(iconForeground,
DrawableColorTester.TRANSLUCENT_FILTER /* filterType */),
() -> new DrawableColorTester(adaptiveIconDrawable.getBackground()));
// 日志
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
"processAdaptiveIcon: FgMainColor=%s, BgMainColor=%s, "
+ "IsBgComplex=%b, FromCache=%b, ThemeColor=%s",
Integer.toHexString(iconColor.mFgColor),
Integer.toHexString(iconColor.mBgColor),
iconColor.mIsBgComplex,
iconColor.mReuseCount > 0,
Integer.toHexString(mThemeColor));
if (!iconColor.mIsBgComplex && mTmpAttrs.mIconBgColor == Color.TRANSPARENT
&& (isRgbSimilarInHsv(mThemeColor, iconColor.mBgColor)
|| (iconColor.mIsBgGrayscale
&& !isRgbSimilarInHsv(mThemeColor, iconColor.mFgColor)))) {
// 日志
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
"processAdaptiveIcon: choose fg icon");
}
}
private static boolean isRgbSimilarInHsv(int a, int b) {
......
// 日志
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
"isRgbSimilarInHsv a:%s, b:%s, contrast ratio:%f",
Integer.toHexString(a), Integer.toHexString(b), contrastRatio);
......
}
这里都是一些构建 SplashScreenView 相关参数的日志打印,一般不会有啥问题,知道在哪执行就好,具体内容我觉得可以忽略,有问题再看。
2.2.3 保存 SplashScreenView
前面看到创建的 SplashScreenView 被以回调的形式传递给了 SplashScreenViewSupplier::setView 。看一下具体实现
# StartingSurfaceDrawer
private static class SplashScreenViewSupplier implements Supplier<SplashScreenView> {
// 闪屏页用到的View
private SplashScreenView mView;
private boolean mIsViewSet;
private Runnable mUiThreadInitTask;
// 设置SplashScreenView
void setView(SplashScreenView view) {
synchronized (this) {
mView = view;
mIsViewSet = true;
notify();
}
}
void setUiThreadInitTask(Runnable initTask) {
synchronized (this) {
mUiThreadInitTask = initTask;
}
}
// 获取SplashScreenView
@Override
public @Nullable SplashScreenView get() {
synchronized (this) {
while (!mIsViewSet) {
try {
wait();
} catch (InterruptedException ignored) {
}
}
if (mUiThreadInitTask != null) {
mUiThreadInitTask.run();
mUiThreadInitTask = null;
}
return mView;
}
}
}
也就是说这个StartWindow 的 View 被保存到了 SplashScreenViewSupplier 下的 mView 对象中
2.3 system_server 执行 StartWindow的addWindow流程
上面小节是构建 StartWindow 用到的 contentView ,现在来看看怎么使用的。
这里再回顾一下2个分支的分歧点, 是在 StartingSurfaceDrawer::addSplashScreenStartingWindow 方法。
# StartingSurfaceDrawer
void addSplashScreenStartingWindow(StartingWindowInfo windowInfo, IBinder appToken,
@StartingWindowType int suggestType) {
......
// replace with the default theme if the application didn't set
// 拿到主题资源
final int theme = getSplashScreenTheme(windowInfo.splashScreenThemeResId, activityInfo);
// 打印日志
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_STARTING_WINDOW,
"addSplashScreen for package: %s with theme: %s for task: %d, suggestType: %d",
activityInfo.packageName, Integer.toHexString(theme), taskId, suggestType);
......
// 设置主题
context.setTheme(theme);
......
// 开始构建需要的参数
final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.TYPE_APPLICATION_STARTING);
......
// window 动画资源
params.windowAnimations = a.getResourceId(R.styleable.Window_windowAnimationStyle, 0);
......
// 这个token是 system_server 传过来的,也就是目标目标应用的ActivityRecord的token
params.token = appToken;
params.packageName = activityInfo.packageName;
......
// 在这设置name
params.setTitle("Splash Screen " + activityInfo.packageName);
......
// 构建一个 SplashScreenViewSupplier ,内部保存真正是 StartWindow 的 ContentView
final SplashScreenViewSupplier viewSupplier = new SplashScreenViewSupplier();
// 定义一个跟布局,可以理解为 rootView (Activity 有个 rootView 然后下面才是contentView)
final FrameLayout rootLayout = new FrameLayout(mSplashscreenContentDrawer.createViewContextWrapper(context));
rootLayout.setPadding(0, 0, 0, 0);
rootLayout.setFitsSystemWindows(false);
// 定义一个 Runnable, 作为下一帧 Vsync 来临执行的回调。 也是
final Runnable setViewSynchronized = () -> {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "addSplashScreenView");
// waiting for setContentView before relayoutWindow
// 拿到保存在 SplashScreenViewSupplier 下的 contentView
SplashScreenView contentView = viewSupplier.get();
// 获取到 StartingWindowRecord
final StartingWindowRecord record = mStartingWindowRecords.get(taskId);
// If record == null, either the starting window added fail or removed already.
// Do not add this view if the token is mismatch.
if (record != null && appToken == record.mAppToken) {
// if view == null then creation of content view was failed.
if (contentView != null) {
try {
// 重点,把 contentView 设置到 rootView下
rootLayout.addView(contentView);
} catch (RuntimeException e) {
Slog.w(TAG, "failed set content view to starting window "
+ "at taskId: " + taskId, e);
contentView = null;
}
}
// 把 contentView 保存进去,copySplashScreenView 方法使用(应用自定义执行 SplashScreen::setOnExitAnimationListener 会触发)
record.setSplashScreenView(contentView);
}
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
};
......
// 重点* 1. 创建具体的内容
mSplashscreenContentDrawer.createContentView(context, suggestType, windowInfo,
viewSupplier::setView, viewSupplier::setUiThreadInitTask);
try {
// 重点* 2. 主流程 (2.3 小节介绍)
if (addWindow(taskId, appToken, rootLayout, display, params, suggestType)) {
......
} else ......
} ......
......
}
现在来看看 addWindow 方法做了什么,首先来看一下这几个参数:
- taskId : ActivityRecord 所在的Task
- appToken : 就是 ActivityRecord ,说明 StartWindow 和应用窗口本身挂载的其实是一个父节点
- rootLayout: 这个就是rootView了,StartWindow 的 contentView 也被设置了进去。
- display:默认一个屏幕的话,那就是主屏幕ID了
- params :contentView 的参数
- suggestType :StartWindow 的类型
弄清这些参数后,再看 addWindow 方法,目的是要把 StartWindow 挂载到窗口树上了,代码如下:
# StartingSurfaceDrawer
protected boolean addWindow(int taskId, IBinder appToken, View view, Display display,
WindowManager.LayoutParams params, @StartingWindowType int suggestType) {
try {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "addRootView");
// 开始触发addView
mWindowManagerGlobal.addView(view, params, display,
null /* parentWindow */, context.getUserId());
}......
}
后面也是通过 WindowManagerGlobal::addView 执行 addWindow 流程,这个和应用的 addWindow 流程是差不多的。
后面就是到WMS了也就是到 system_server 进程,但是具体在 WMS 的 addWindow 方法,针对 startWindow 的处理也是有区别的,其他的一样的就不看了,看对当前type:TYPE_APPLICATION_STARTING 的处理
# WindowManagerService
public int addWindow(Session session, IWindow client, LayoutParams attrs, int viewVisibility,
int displayId, int requestUserId, InsetsVisibilities requestedVisibilities,
InputChannel outInputChannel, InsetsState outInsetsState,
InsetsSourceControl[] outActiveControls) {
......// 省略token, windowState相关
if (type == TYPE_APPLICATION_STARTING && activity != null) {
// 将其加到应用 ActivityRecord下,然后打印log
activity.attachStartingWindow(win);
ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "addWindow: %s startingWindow=%s",
activity, win);
}
}
# ActivityRecord
// 这里就是将ActivityRecord和startingWindow进行一些绑定处理。
void attachStartingWindow(@NonNull WindowState startingWindow) {
// 将之前创建的mStartingData 给 startingWindow
startingWindow.mStartingData = mStartingData;
// 将startingWindow保存到ActivityRecord
mStartingWindow = startingWindow;
// 后面不执行
if (mStartingData != null && mStartingData.mAssociatedTask != null) {
attachStartingSurfaceToAssociatedTask();
}
}
到这里 ActivityRecord 中的 mStartingWindow 就有值了,后面的 mStartingData.mAssociatedTask 没有赋值,debug 的到也是为 null,所以if语句里面的不会执行。 其他的流程和addWindow流程一样了。
3. StartingWindow 的移除
StartingWindow 的移除其实就是在应用窗口绘制完成后,
WindowState::performShowLocked -- 窗口状态设置成 HAS_DRAWN ActivityRecord::onFirstWindowDrawn -- 触发移除 StartWindow
下一篇会详细介绍移除 StartWindow 的流程(主要是移除动画)
4. 总结
-
- 存在的意义 : 提升用户体验
-
- 添加的时机 :ActivityRecord 挂载到 Task 时开始触发
-
- 移除的时机 :应用窗口第一次绘制完成的时候,WindowState::performShowLocked 将应用窗口状态设置为 HAS_DRAWN 的时候,会执行移除 StartWindow 逻辑
-
- 窗口的内容 :默认是应用图标,应用端可以自定义,在 2.2 小节有相关介绍