参考:
Splash Screen
Android S上推出全新的启动画面 API SplashScreen,其实在较低版本中(例如Android R),也是有SplashScreen对应的一些功能模块代码,但是不是强制性使用;
SplashScreen意为闪屏页,也是就是我们常见的应用进程启动画面,在该画面中,国内的一些厂商绝大多数都会进行广告的显示;
我们常见的微信的启动界面就是上述的效果;
当我们打开各种桌面软件或移动端 App 的时候,首先会看到的经常是一段过渡画面,其作用为:
- 用以展示产品所属的公司、品牌的图标、公司的 Slogan 等,借以宣传公司的调性和传达一些特色;
- 在节日庆典、特殊活动的时候展示广告页面或活动海报;
- 在启动画面展示的同时,背后的内容得以加载;
其核心作用其实就是用于在该时段内用于启动对应的应用进程以及加载应用进程需要准备的资源等,通过SplashScreen在一定程度上缓解了用户的等待,启动画面设计良好的话,还使得这个枯燥的过程显得有趣和充满期待;
应用程序 SplashScreen 属性配置
App 启动画面流程
点击 Launcher 上的 Logo 之后,首先用户将看到一个启动画面,如果是初次启动的话接着会展示 Guide 画面引导用户了解如何使用;如果是节日或广告需要的话展示的是广告宣传画面,最后才是展示内容的主画面;
但是我们需要注意一个核心点:
- 这个启动动画界面,即这个SplashScreen不是由应用进程创建的,我们可以想象,如果该Window是由即将启动的App创建的话,那这个SplashScreen则是完全没有必要,因为SplashScreen Window就是为了过渡App启动过慢导致的App界面无法快速显示的问题;
- SplashScreen Window的创建,针对不同的Android版本,所负责的进程也不同;
应用层 xml 配置
在style.xml中配置 SplashScreen Theme,并将 对应的主题设置到启动页 Activity 应用;
ini
<item name="android:windowSplashScreenBackground">@color/...</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/...</item>
<item name="android:windowSplashScreenAnimationDuration">1000</item>
<item name="android:windowSplashScreenIconBackgroundColor">@color/...</item>
<item name="android:windowSplashScreenBrandingImage">@drawable/...</item>
........................
- windowSplashScreenBackground:启动画面背景颜色;
- windowSplashScreenAnimatedIcon:启动画面icon图标:这里可以是图片、帧动画等;
- windowSplashScreenAnimationDuration:icon动画在关闭之前显示的时长;
- windowSplashScreenIconBackgroundColor:启动画面icon图标的背景色;
- windowSplashScreenBrandingImage:启动画面底部的 Brand 图片;
xml 配置实例
xml
<!-- Splash启动页Style -->
<!-- 这里需要定义parent属性,这样后续在Framework框架中进行查询的时候,才能适配到应用进程自定义的SplashScreen Theme -->
<style name="Theme.SplashScreen.Demo" parent="Theme.SplashScreen">
<!--启动画面背景颜色-->
<item name="windowSplashScreenBackground">@color/splashscreen_bg</item>
<!-- 启动画面icon图标:这里可以是图片、帧动画等-->
<item name="windowSplashScreenAnimatedIcon">@drawable/splash_anim_icon</item>
<item name="windowSplashScreenIconBackgroundColor">@color/splashscreen_icon_bg</item>
<!-- icon动画在关闭之前显示的时长:最长时间为1000毫秒-->
<item name="windowSplashScreenAnimationDuration">1000</item>
<!-- 启动画面底部的 Brand 图片-->
<item name="android:windowSplashScreenBrandingImage">@drawable/brand_img</item>
<!-- Splash退出后的主题-->
<item name="postSplashScreenTheme">@style/Theme.Android12_Splash</item>
</style>
AndroidManifest 配置实例
ini
<activity
android:name=".SplashActivity"
android:exported="true"
android:theme="@style/Theme.SplashScreen.Demo">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
SplashScreen 工作原理
SplashScreen Window创建
Android R
scala
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.server.wm;
import android.content.res.CompatibilityInfo;
import android.content.res.Configuration;
import com.android.server.policy.WindowManagerPolicy.StartingSurface;
/**
* Represents starting data for splash screens, i.e. "traditional" starting windows.
*/
class SplashScreenStartingData extends StartingData {
private final String mPkg;
private final int mTheme;
private final CompatibilityInfo mCompatInfo;
private final CharSequence mNonLocalizedLabel;
private final int mLabelRes;
private final int mIcon;
private final int mLogo;
private final int mWindowFlags;
private final Configuration mMergedOverrideConfiguration;
SplashScreenStartingData(WindowManagerService service, String pkg, int theme,
CompatibilityInfo compatInfo, CharSequence nonLocalizedLabel, int labelRes, int icon,
int logo, int windowFlags, Configuration mergedOverrideConfiguration) {
super(service);
mPkg = pkg;
mTheme = theme;
mCompatInfo = compatInfo;
mNonLocalizedLabel = nonLocalizedLabel;
mLabelRes = labelRes;
mIcon = icon;
mLogo = logo;
mWindowFlags = windowFlags;
mMergedOverrideConfiguration = mergedOverrideConfiguration;
}
// 这个方法就是用于创建SplashScreen Window的逻辑
@Override
StartingSurface createStartingSurface(ActivityRecord activity) {
return mService.mPolicy.addSplashScreen(activity.token, activity.mUserId, mPkg, mTheme,
mCompatInfo, mNonLocalizedLabel, mLabelRes, mIcon, mLogo, mWindowFlags,
mMergedOverrideConfiguration, activity.getDisplayContent().getDisplayId());
}
}
createStartingSurface()方法创建了SplashScreen Window,最终调用了WindowManagerPolicy中的addSplashScreen()方法,而WindowManagerPolicy本身为interface,真正的实现类为PhoneWindowManager,可以向上追溯到SystemServer类中,在创建WindowManagerService的时候,创建了PhoneWindowManager实例,然后将其传入了WindowManagerService的main()函数中;
java
/** {@inheritDoc} */
@Override
public StartingSurface addSplashScreen(IBinder appToken, int userId, String packageName,
int theme, CompatibilityInfo compatInfo, CharSequence nonLocalizedLabel, int labelRes,
int icon, int logo, int windowFlags, Configuration overrideConfig, int displayId) {
if (!SHOW_SPLASH_SCREENS) {
return null;
}
if (packageName == null) {
return null;
}
WindowManager wm = null;
View view = null;
try {
Context context = mContext;
........................
final PhoneWindow win = new PhoneWindow(context);
win.setIsStartingWindow(true);
// 这一部分省略的就是win以及对应params参数配置的逻辑
..................
params.setTitle("Splash Screen " + packageName);
// 1.调用该方法,通过传入的context创建对应的View实例,然后将创建好的View实例set到对应的win实例中,使用的是setContentView()方法实现的
addSplashscreenContent(win, context);
wm = (WindowManager) context.getSystemService(WINDOW_SERVICE);
view = win.getDecorView();
if (DEBUG_SPLASH_SCREEN) Slog.d(TAG, "Adding splash screen window for "
+ packageName + " / " + appToken + ": " + (view.getParent() != null ? view : null));
// 调用WMS的addView方法,将PhoneWindow实例中对应的View实例,这个View实例其实就是刚刚创建好的View实例,绑定LayoutParams参数配置传入到WMS中进行对应WindowState图层的添加
wm.addView(view, params);
// Only return the view if it was successfully added to the
// window manager... which we can tell by it having a parent.
return view.getParent() != null ? new SplashScreenSurface(view, appToken) : null;
} catch (WindowManager.BadTokenException e) {
// ignore
Log.w(TAG, appToken + " already running, starting window not displayed. " +
e.getMessage());
} catch (RuntimeException e) {
// don't crash if something else bad happens, for example a
// failure loading resources because we are loading from an app
// on external storage that has been unmounted.
Log.w(TAG, appToken + " failed creating starting window", e);
} finally {
if (view != null && view.getParent() == null) {
Log.w(TAG, "view not successfully added to wm, removing view");
wm.removeViewImmediate(view);
}
}
return null;
}
由此,我们可以看出,SplashScreen Window的创建是在SystemServer进程中创建的;
Android S
在Android R 中存在SplashScreen Window的创建过程是在SystemServer进程中,但是在Android S版本,Google强制所有的App默认是存在启动动画(SplashScreen)的,同时在SplashScreen Window的创建方式上进行了优化;
在添加启动画面的过程中,会进入到 SystemUI 进程进行一些特殊处理,主要涉及到SysUI的 WMShell 组件;
SystemUI为系统处理各种业务逻辑的关键代码,包含有20多个组件,从中可以看出 SystemUI的复杂程度;
其中的 WMShell 也是复杂多样的,其中包括:
- SplitScreen分屏模式
- OneHanded单手模式
- Freeform自由窗口模式
- Bubble气泡通知窗口(Android Q)
- PIP画中画模式 等等;
SystemUI引用framework的系统库,通过Dagger2 依赖注入,将WMComponent 、WMShellModule 、WMShellBaseModule 整合构建出StartingWindowController 、ShellTaskOrganizer 、StartingSurfaceDrawer等实例实现启动画面的过渡作用;
java
/**
* Managing to create and release a starting window surface.
*/
public class StartingSurfaceController {
private static final String TAG = TAG_WITH_CLASS_NAME
? StartingSurfaceController.class.getSimpleName() : TAG_WM;
/** Set to {@code true} to enable shell starting surface drawer. */
static final boolean DEBUG_ENABLE_SHELL_DRAWER =
SystemProperties.getBoolean("persist.debug.shell_starting_surface", true);
private final WindowManagerService mService;
private final SplashScreenExceptionList mSplashScreenExceptionsList;
public StartingSurfaceController(WindowManagerService wm) {
mService = wm;
mSplashScreenExceptionsList = new SplashScreenExceptionList(wm.mContext.getMainExecutor());
}
StartingSurface createSplashScreenStartingSurface(ActivityRecord activity, String packageName,
int theme, CompatibilityInfo compatInfo, CharSequence nonLocalizedLabel, int labelRes,
int icon, int logo, int windowFlags, Configuration overrideConfig, int displayId) {
if (!DEBUG_ENABLE_SHELL_DRAWER) {
return mService.mPolicy.addSplashScreen(activity.token, activity.mUserId, packageName,
theme, compatInfo, nonLocalizedLabel, labelRes, icon, logo, windowFlags,
overrideConfig, displayId);
}
synchronized (mService.mGlobalLock) {
final Task task = activity.getTask();
if (task != null && mService.mAtmService.mTaskOrganizerController.addStartingWindow(
task, activity, theme, null /* taskSnapshot */)) {
return new ShellStartingSurface(task);
}
}
return null;
}
..................
}
- DEBUG_ENABLE_SHELL_DRAWER:这个标志是用于判断是否使用Shell TaskOrganizer 方式创建SplashScreen Window,该标志默认为true,即代表了默认使用Shell TaskOrganizer方式创建;
但是针对低版本应用适配高版本系统,可能就会存在SplashScreen Window创建方式不同导致的处理响应逻辑不同,Android S为了适配兼容低版本,通过该SystemProperties属性值来兼容低版本创建方式;
应用进程可以根据自己的需求进行选择,但是需要注意的是,这个SystemProperties属性是全局的,所以针对既存在低版本也存在高版本的情况下,还是需要统一SplashScreen Window的创建方式,以保证后续的响应逻辑是正确的;
针对Android S版本中SplashScreen Window的创建:
java
/**
* Called when a task need a splash screen starting window.
*
* @param suggestType The suggestion type to draw the splash screen.
*/
void addSplashScreenStartingWindow(StartingWindowInfo windowInfo, IBinder appToken,
@StartingWindowType int suggestType) {
........................
final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.TYPE_APPLICATION_STARTING);
params.setFitInsetsSides(0);
params.setFitInsetsTypes(0);
........................
// TODO(b/173975965) tracking performance
// Prepare the splash screen content view on splash screen worker thread in parallel, so the
// content view won't be blocked by binder call like addWindow and relayout.
// 1. Trigger splash screen worker thread to create SplashScreenView before/while
// Session#addWindow.
// 2. Synchronize the SplashscreenView to splash screen thread before Choreographer start
// traversal, which will call Session#relayout on splash screen thread.
// 3. Pre-draw the BitmapShader if the icon is immobile on splash screen worker thread, at
// the same time the splash screen thread should be executing Session#relayout. Blocking the
// traversal -> draw on splash screen thread until the BitmapShader of the icon is ready.
// Record whether create splash screen view success, notify to current thread after
// create splash screen view finished.
final SplashScreenViewSupplier viewSupplier = new SplashScreenViewSupplier();
final FrameLayout rootLayout = new FrameLayout(context);
rootLayout.setPadding(0, 0, 0, 0);
rootLayout.setFitsSystemWindows(false);
final Runnable setViewSynchronized = () -> {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "addSplashScreenView");
// waiting for setContentView before relayoutWindow
SplashScreenView contentView = viewSupplier.get();
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 {
rootLayout.addView(contentView);
} catch (RuntimeException e) {
Slog.w(TAG, "failed set content view to starting window "
+ "at taskId: " + taskId, e);
contentView = null;
}
}
record.setSplashScreenView(contentView);
}
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
};
mSplashscreenContentDrawer.createContentView(context, suggestType, activityInfo, taskId,
viewSupplier::setView);
try {
final WindowManager wm = context.getSystemService(WindowManager.class);
if (addWindow(taskId, appToken, rootLayout, wm, params, suggestType)) {
// We use the splash screen worker thread to create SplashScreenView while adding
// the window, as otherwise Choreographer#doFrame might be delayed on this thread.
// And since Choreographer#doFrame won't happen immediately after adding the window,
// if the view is not added to the PhoneWindow on the first #doFrame, the view will
// not be rendered on the first frame. So here we need to synchronize the view on
// the window before first round relayoutWindow, which will happen after insets
// animation.
mChoreographer.postCallback(CALLBACK_INSETS_ANIMATION, setViewSynchronized, null);
// Block until we get the background color.
final StartingWindowRecord record = mStartingWindowRecords.get(taskId);
final SplashScreenView contentView = viewSupplier.get();
record.mBGColor = contentView.getInitBackgroundColor();
}
} catch (RuntimeException e) {
// don't crash if something else bad happens, for example a
// failure loading resources because we are loading from an app
// on external storage that has been unmounted.
Slog.w(TAG, "failed creating starting window at taskId: " + taskId, e);
}
}
protected boolean addWindow(int taskId, IBinder appToken, View view, WindowManager wm,
WindowManager.LayoutParams params, @StartingWindowType int suggestType) {
boolean shouldSaveView = true;
try {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "addRootView");
wm.addView(view, params);
} catch (WindowManager.BadTokenException e) {
// ignore
Slog.w(TAG, appToken + " already running, starting window not displayed. "
+ e.getMessage());
shouldSaveView = false;
} finally {
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
if (view != null && view.getParent() == null) {
Slog.w(TAG, "view not successfully added to wm, removing view");
wm.removeViewImmediate(view);
shouldSaveView = false;
}
}
if (shouldSaveView) {
removeWindowNoAnimate(taskId);
saveSplashScreenRecord(appToken, taskId, view, suggestType);
}
return shouldSaveView;
}
上述的过程,我们可以大致的分为4步:
- WindowManager.LayoutParams参数配置;
- FrameLayout rootLayout布局的创建;
- 创建SplashScreenView实例,将传入好的View实例传入到rootLayout中;
- 将创建、配置好的FrameLayout View实例通过WMS的addView()函数添加到WMS,即创建对应的WindowState窗口;
SplashScreen Window移除
Android R
我们在分析DrawState概念的时候,提及了DrawState的五个阶段:
- NO_SURFACE
- DRAW_PENDING
- COMMIT_DRAW_PENDING
- READY_TO_SHOW
- HAS_DRAWN
我们重点关注最后两个状态;
- READY_TO_SHOW:代表了窗口已绘制完成,但是还没有进行显示,此时的visible属性值 = false;
- HAS_DRAWN:代表窗口已经show,visible = true;
我们以目标进程的Activity窗口绘制进度为例,当目标进程Activity的窗口绘制完成之后,就会将mDrawState设置为READY_TO_SHOW;
Starting Window添加后,待窗口绘制完毕,GOOD TO GO流程会触发打开动效,动效后续流程讲解。Starting Window的正常移除时机是activity主窗口绘制完毕(启动过程中因一些原因移除activity也能触发,非唯一途径);
remove View
这里面又涉及到一个新的概念:SplashScreenSurface
java
/**
* Holds the contents of a splash screen starting window, i.e. the {@link DecorView} of a
* {@link PhoneWindow}. This is just a wrapper such that we can return it from
* {@link WindowManagerPolicy#addSplashScreen}.
*/
class SplashScreenSurface implements StartingSurface {
private static final String TAG = PhoneWindowManager.TAG;
private final View mView;
private final IBinder mAppToken;
SplashScreenSurface(View view, IBinder appToken) {
mView = view;
mAppToken = appToken;
}
@Override
public void remove() {
if (DEBUG_SPLASH_SCREEN) Slog.v(TAG, "Removing splash screen window for " + mAppToken + ": "
+ this + " Callers=" + Debug.getCallers(4));
final WindowManager wm = mView.getContext().getSystemService(WindowManager.class);
wm.removeView(mView);
}
}
其实这个类就是用于维护当前Splash Screen View以及对应的所属ActivityRecord的AppToken句柄;