Splash Screen 是应用冷启动时显示的过渡界面,遮挡应用启动初期的空白 / 加载过程,提升用户体验。
1. Splash Screen 窗口创建
1.1 代码定位
以冷启动图库为例,在显示Splash Screen时,通过adb shell dumpsys window windows,我们看到Splash Screen是在com.android.gallery3d.app.GalleryActivity的上面。
shell
Window #8 Window{d7d90e9 u0 Splash Screen com.android.gallery3d}:
mDisplayId=0 rootTaskId=41 mSession=Session{8e6660c 728:u0a10096} mClient=android.os.BinderProxy@968b570
mOwnerUid=10096 showForAllUsers=true package=com.android.gallery3d appop=NONE
mAttrs={(0,0)(fillxfill) sim={adjust=pan} ty=APPLICATION_STARTING fmt=TRANSLUCENT wanim=0x10302f8
fl=NOT_FOCUSABLE NOT_TOUCHABLE LAYOUT_IN_SCREEN LAYOUT_INSET_DECOR ALT_FOCUSABLE_IM HARDWARE_ACCELERATED DRAWS_SYSTEM_BAR_BACKGROUNDS
pfl=SHOW_FOR_ALL_USERS USE_BLAST APPEARANCE_CONTROLLED FIT_INSETS_CONTROLLED TRUSTED_OVERLAY
bhv=DEFAULT
fitSides=}
Requested w=1440 h=2960 mLayoutSeq=119
mBaseLayer=21000 mSubLayer=0 mToken=ActivityRecord{9d4fcb1 u0 com.android.gallery3d/.app.GalleryActivity t41}
mActivityRecord=ActivityRecord{9d4fcb1 u0 com.android.gallery3d/.app.GalleryActivity t41}
mAppDied=false drawnStateEvaluated=true mightAffectAllDrawn=true
mViewVisibility=0x0 mHaveFrame=true mObscured=false
mGivenContentInsets=[0,0][0,0] mGivenVisibleInsets=[0,0][0,0]
mFullConfiguration={1.0 310mcc260mnc [en_US] ldltr sw411dp w411dp h773dp 560dpi nrml long port finger qwerty/v/v dpad/v winConfig={ mBounds=Rect(0, 0 - 1440, 2960) mAppBounds=Rect(0, 0 - 1440, 2792) mMaxBounds=Rect(0, 0 - 1440, 2960) mWindowingMode=fullscreen mDisplayWindowingMode=fullscreen mActivityType=standard mAlwaysOnTop=undefined mRotation=ROTATION_0} as.2 s.1 fontWeightAdjustment=0}
mLastReportedConfiguration={1.0 310mcc260mnc [en_US] ldltr sw411dp w411dp h773dp 560dpi nrml long port finger qwerty/v/v dpad/v winConfig={ mBounds=Rect(0, 0 - 1440, 2960) mAppBounds=Rect(0, 0 - 1440, 2792) mMaxBounds=Rect(0, 0 - 1440, 2960) mWindowingMode=fullscreen mDisplayWindowingMode=fullscreen mActivityType=standard mAlwaysOnTop=undefined mRotation=ROTATION_0} as.2 s.1 fontWeightAdjustment=0}
mHasSurface=true isReadyForDisplay()=true mWindowRemovalAllowed=false
Frames: containing=[0,0][1440,2960] parent=[0,0][1440,2960] display=[0,0][1440,2960]
mFrame=[0,0][1440,2960] last=[0,0][1440,2960]
surface=[0,0][0,0]
WindowStateAnimator{a1552a5 Splash Screen com.android.gallery3d}:
mSurface=Surface(name=Splash Screen com.android.gallery3d)/@0xbc3e2b
Surface: shown=true layer=0 alpha=1.0 rect=(0.0,0.0) transform=(1.0, 0.0, 0.0, 1.0)
mDrawState=HAS_DRAWN mLastHidden=false
mEnterAnimationPending=false mSystemDecorRect=[0,0][0,0]
mForceSeamlesslyRotate=false seamlesslyRotate: pending=null finishedFrameNumber=0
isOnScreen=true
isVisible=true
Window #9 Window{44ce9da u0 com.android.gallery3d/com.android.gallery3d.app.GalleryActivity}:
...
Window #10 Window{15ea896 u0 com.android.launcher3/com.android.launcher3.uioverrides.QuickstepLauncher}:
...
Window #11 Window{db44ba4 u0 com.android.systemui.ImageWallpaper}:
...
通过在代码中grep Splash Screen,结合代码分析以及Log打印,定位到可能会走到StartingSurfaceController.java中,因此追加log如下:
java
StartingSurface createSplashScreenStartingSurface(ActivityRecord activity, String packageName,
int theme, CompatibilityInfo compatInfo, CharSequence nonLocalizedLabel, int labelRes,
int icon, int logo, int windowFlags, Configuration overrideConfig, int displayId) {
...
synchronized (mService.mGlobalLock) {
final Task task = activity.getTask();
Slog.w("Test111", "createSplashScreenStartingSurface ", new Exception());
if (task != null && mService.mAtmService.mTaskOrganizerController.addStartingWindow(
task, activity, theme, null /* taskSnapshot */)) {
return new ShellStartingSurface(task);
}
}
return null;
log输出如下:
shell
561 942 W Test111 : createSplashScreenStartingSurface
561 942 W Test111 : java.lang.Exception
561 942 W Test111 : at com.android.server.wm.StartingSurfaceController.createSplashScreenStartingSurface(StartingSurfaceController.java:71)
561 942 W Test111 : at com.android.server.wm.SplashScreenStartingData.createStartingSurface(SplashScreenStartingData.java:56)
561 942 W Test111 : at com.android.server.wm.ActivityRecord$AddStartingWindow.run(ActivityRecord.java:2187)
561 942 W Test111 : at com.android.server.wm.ActivityRecord.scheduleAddStartingWindow(ActivityRecord.java:2148)
561 942 W Test111 : at com.android.server.wm.ActivityRecord.addStartingWindow(ActivityRecord.java:2126)
561 942 W Test111 : at com.android.server.wm.ActivityRecord.showStartingWindow(ActivityRecord.java:6666)
561 942 W Test111 : at com.android.server.wm.Task.startActivityLocked(Task.java:5196)
561 942 W Test111 : at com.android.server.wm.ActivityStarter.startActivityInner(ActivityStarter.java:1811)
561 942 W Test111 : at com.android.server.wm.ActivityStarter.startActivityUnchecked(ActivityStarter.java:1584)
561 942 W Test111 : at com.android.server.wm.ActivityStarter.executeRequest(ActivityStarter.java:1185)
561 942 W Test111 : at com.android.server.wm.ActivityStarter.execute(ActivityStarter.java:672)
561 942 W Test111 : at com.android.server.wm.ActivityTaskManagerService.startActivityAsUser(ActivityTaskManagerService.java:1243)
561 942 W Test111 : at com.android.server.wm.ActivityTaskManagerService.startActivityAsUser(ActivityTaskManagerService.java:1215)
561 942 W Test111 : at com.android.server.wm.ActivityTaskManagerService.startActivity(ActivityTaskManagerService.java:1190)
561 942 W Test111 : at android.app.IActivityTaskManager$Stub.onTransact(IActivityTaskManager.java:893)
561 942 W Test111 : at com.android.server.wm.ActivityTaskManagerService.onTransact(ActivityTaskManagerService.java:5124)
561 942 W Test111 : at android.os.Binder.execTransactInternal(Binder.java:1179)
561 942 W Test111 : at android.os.Binder.execTransact(Binder.java:1143)
1.2 startActivity
在log中我们又看到熟悉的startActivityInner,在启动Activity必走的流程,想想也合理,毕竟Splash Screen就是在启动Activity时显示的。
下面结合log分析代码:
java
// ActivityStarter.java
int startActivityInner(final ActivityRecord r, ActivityRecord sourceRecord,
IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
int startFlags, boolean doResume, ActivityOptions options, Task inTask,
TaskFragment inTaskFragment, boolean restrictedBgActivity,
NeededUriGrants intentGrants) {
...
if (mTargetRootTask == null) {
// 1
mTargetRootTask = getLaunchRootTask(mStartActivity, mLaunchFlags, targetTask, mOptions);
}
if (newTask) {
// 2
final Task taskToAffiliate = (mLaunchTaskBehind && mSourceRecord != null)
? mSourceRecord.getTask() : null;
setNewTask(taskToAffiliate);
} else if (mAddingToTask) {
addOrReparentStartingActivity(targetTask, "adding to task");
}
...
// 3
mTargetRootTask.startActivityLocked(mStartActivity,
topRootTask != null ? topRootTask.getTopNonFinishingActivity() : null, newTask,
isTaskSwitch, mOptions, sourceRecord);
}
java
// Task.java
void startActivityLocked(ActivityRecord r, @Nullable ActivityRecord focusedTopActivity,
boolean newTask, boolean isTaskSwitch, ActivityOptions options,
@Nullable ActivityRecord sourceRecord) {
...
r.showStartingWindow(prev, newTask, isTaskSwitch,
true /* startActivity */, sourceRecord);
...
}
startActivityLocked的调用是在Task和ActivityRecord的构造和挂载之后,因此推测Splash Screen这个窗口是挂在他们下面的,最后我们可以通过dump来验证。
r 是本次startActivity过程中创建的ActivityRecord对象:com.android.gallery3d/.app.GalleryActivity,因此下面的操作都是在这个对象中的。最后调用到TaskOrganizerController.java中,
java
boolean addStartingWindow(Task task, ActivityRecord activity, int launchTheme,
TaskSnapshot taskSnapshot) {
...
try {
lastOrganizer.addStartingWindow(info, activity.token);
} catch (RemoteException e) {
Slog.e(TAG, "Exception sending onTaskStart callback", e);
return false;
}
return true;
}
这里是一个binder的AIDL调用,会走到systemUI进程中,但代码还在framework下,
java
// StartingSurfaceDrawer.java
void addSplashScreenStartingWindow(StartingWindowInfo windowInfo, IBinder appToken,
@StartingWindowType int suggestType) {
...
final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.TYPE_APPLICATION_STARTING);
...
params.windowAnimations = a.getResourceId(R.styleable.Window_windowAnimationStyle, 0);
...
params.token = appToken;
params.packageName = activityInfo.packageName;
...
params.setTitle("Splash Screen " + activityInfo.packageName);
...
try {
if (addWindow(taskId, appToken, rootLayout, display, params, suggestType)) {
...
}
} catch (RuntimeException e) {
...
}
}
在这个函数里构造了WindowManager.LayoutParams,这看来和app中实现系统弹窗很类似。
type = TYPE_APPLICATION_STARTING,
Title = Splash Screen + activityInfo.packageName, 正如我们在dump信息中看到的一样。
addWindow:会调用mWindowManagerGlobal.addView,之前分析过,这里会通过ViewRootImpl -> Session, 走到WMS的addWindow。
1.3 addWindow
虽然之前分析过WMS的addWindow,但本次不内部走的流程和之前有所不同,下面分段分析:
java
public int addWindow(Session session, IWindow client, LayoutParams attrs, int viewVisibility,
int displayId, int requestUserId, InsetsVisibilities requestedVisibilities,
InputChannel outInputChannel, InsetsState outInsetsState,
InsetsSourceControl[] outActiveControls) {
...
// 1
WindowToken token = displayContent.getWindowToken(
hasParent ? parentWindow.mAttrs.token : attrs.token);
...
// 2
final WindowState win = new WindowState(this, session, client, token, parentWindow,
appOp[0], attrs, viewVisibility, session.mUid, userId,
session.mCanAddInternalSystemWindow);
...
// 3
win.mToken.addWindow(win);
-
token这次不是null了,因为
attrs.token是上面ActivityRecord对象com.android.gallery3d/.app.GalleryActivity,通过调用TaskOrganizerController.java的addStartingWindow函数时,将自己的token(因为ActivityRecord继承自WindowToken,其实是父类WindowToken的变量)传递进来的,然后经过systemUI进程,再传递到system server进程中WMS的addWindow,所以attrs.token就是com.android.gallery3d/.app.GalleryActivity的token。 -
创建名为
Splash Screen com.android.gallery3d的WindowState对象。 -
将WindowState挂载到对应token(com.android.gallery3d/.app.GalleryActivity)上,我们用
dump activity containers来验证一下。shellACTIVITY MANAGER CONTAINERS (dumpsys activity containers) ROOT type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960] #0 Display 0 name="Built-in Screen" type=undefined mode=fullscreen override-mode=fullscreen requested-bounds=[0,0][1440,2960] bounds=[0,0][1440,2960] #2 Leaf:36:36 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960] ... #1 HideDisplayCutout:32:35 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960] ... #0 WindowedMagnification:0:31 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960] ... #0 HideDisplayCutout:0:16 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960] ... #0 FullscreenMagnification:2:14 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960] ... #0 DefaultTaskDisplayArea type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960] #4 Task=42 type=standard mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960] #0 ActivityRecord{e3569de u0 com.android.gallery3d/.app.GalleryActivity t42} type=standard mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960] #1 fded6b6 Splash Screen com.android.gallery3d type=standard mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960] #0 1f11433 com.android.gallery3d/com.android.gallery3d.app.GalleryActivity type=standard mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960] #3 Task=1 type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960] #0 Task=38 type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960] #0 ActivityRecord{34ea725 u0 com.android.launcher3/.uioverrides.QuickstepLauncher t38} type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960] #0 15ea896 com.android.launcher3/com.android.launcher3.uioverrides.QuickstepLauncher type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960] #2 Task=3 type=undefined mode=fullscreen override-mode=fullscreen requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960] #1 Task=4 type=undefined mode=multi-window override-mode=multi-window requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960] #0 Task=5 type=undefined mode=multi-window override-mode=multi-window requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960] #0 OneHandedBackgroundPanel:0:1 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960] #0 OneHanded:0:1 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960] #0 FullscreenMagnification:0:1 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960] #0 Leaf:0:1 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960] #0 WallpaperWindowToken{92ef6a3 token=android.os.Binder@e3195d2} type=undefined mode=fullscreen override-mode=fullscreen requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960] #0 db44ba4 com.android.systemui.ImageWallpaper type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][1440,2960]
2. Splash Screen 窗口移除
根据上面的调查,知道Splash Screen窗口的创建是从system server进程中,通过ActivityRecord.java,会调用到TaskOrganizerController.java中,这里再走到systemUI进程,systemUI进程的入口在ShellTaskOrganizer.java。
在TaskOrganizerController.java和ShellTaskOrganizer.java中,都有removeStartingWindow函数,查看其调用情况,发现除了ActivityRecord.java外,其他文件也有调用,并且发现源码中有proto log可以利用,那么可以通过adb shell cmd window logging enable-text WM_DEBUG_STARTING_WINDOW打开ProtoLog WM_DEBUG_STARTING_WINDOW来进行分析,另外运行在systemUI进程的代码StartingSurfaceDrawer.java中,也可以通过将DEBUG_SPLASH_SCREEN 设置为true, 来打开一下Slog来分析:
结果发现有如下WindowManager: Removing startingView=com.android.server.wm.StartingSurfaceController$ShellStartingSurface@b209bb7日志输出。那么在输出日志代码的地方再加上调用栈打印:
shell
Test111 : at com.android.server.wm.ActivityRecord.lambda$removeStartingWindowAnimation$5(ActivityRecord.java:2469)
Test111 : at com.android.server.wm.ActivityRecord$$ExternalSyntheticLambda5.run(Unknown Source:6)
Test111 : at com.android.server.wm.ActivityRecord.removeStartingWindowAnimation(ActivityRecord.java:2479)
Test111 : at com.android.server.wm.ActivityRecord.removeStartingWindow(ActivityRecord.java:2418)
Test111 : at com.android.server.wm.ActivityRecord.onFirstWindowDrawn(ActivityRecord.java:6098)
Test111 : at com.android.server.wm.WindowState.performShowLocked(WindowState.java:4777)
Test111 : at com.android.server.wm.WindowStateAnimator.commitFinishDrawingLocked(WindowStateAnimator.java:280)
commitFinishDrawingLocked这个函数我们在WMS FinishDrawing的时候介绍过,那么说明Splash Screen 窗口移除时机是在app finish draw的时候。
system server进程中函数的调用不再分析了,可以参考上面的调用栈。再借助将DEBUG_SPLASH_SCREEN设置为true后,打印的log, 我们分析在systemUI进程中的代码会走到StartingSurfaceDrawer.java的removeWindowSynced,然后里面调用removeWindowInner函数,其中又会调用mWindowManagerGlobal.removeView,来通过WMS移除Splash Screen 窗口。
3. Splash Screen自定义
3.1 Splash Screen简单自定义
3.1.1 app侧配置xml
在我的demo app中配置如下:
xml
// themes.xml
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.MyTestApplication" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
...
<!-- Customize your theme here. -->
<item name="android:windowSplashScreenBackground">#ffffffff</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/news_avd_v02</item>
<item name="android:windowSplashScreenIconBackgroundColor">#ffffffff</item>
<item name="android:windowSplashScreenAnimationDuration">1000</item>
</style>
</resources>
news_avd_v02这个xml可以google官方下载,下载路径可以参考链接 https://blog.csdn.net/CJohn1994/article/details/125966787
之后运行demo app, 可以发现替换了原来的只显示demo app icon的Splash screen。 app只需要通过修改xml就可以改变Splash screen,那一定是系统源码中哪里获取了app的xml配置,继续分析。
3.1.2 系统源码获取app的xml
当创建Splash Screen Window的流程走到systemUI进程中,走到如下代码:
java
// StartingSurfaceDrawer.java
void addSplashScreenStartingWindow(StartingWindowInfo windowInfo, IBinder appToken,
@StartingWindowType int suggestType) {
...
mSplashscreenContentDrawer.createContentView(context, suggestType, activityInfo, taskId,
viewSupplier::setView, viewSupplier::setUiThreadInitTask);
try {
if (addWindow(taskId, appToken, rootLayout, display, params, suggestType)) {
...
}
} catch (RuntimeException e) {
...
}
}
在addWindow之前,会调用SplashscreenContentDrawer.java 的 createContentView -> makeSplashScreenContentView -> getWindowAttrs,
java
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.mAnimationDuration = safeReturnAttrDefault((def) -> typedArray.getInt(
R.styleable.Window_windowSplashScreenAnimationDuration, def), 0);
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();
if (DEBUG) {
Slog.d(TAG, "window attributes color: "
+ Integer.toHexString(attrs.mWindowBgColor)
+ " icon " + attrs.mSplashScreenIcon + " duration " + attrs.mAnimationDuration
+ " brandImage " + attrs.mBrandingImage);
}
}
可以看到在getWindowAttrs函数中,给attrs这个出参赋值的内容,都是我们在上一节中app的xml设置的。
3.2 Splash Screen复杂自定义
3.2.1 log对比
复杂的Splash Screen定义指的是icon的动画与app内的activity动画相结合。谷歌在aosp源码中提供了一个domo app,代码位置在:
development/samples/StartingWindow,我们就已它来分析。
之前分析得知Splash Screen从:
java
Process: system server, File: ActivityStarter.java, Func: startActivityInner
->
Process: systemUI, File: ShellTaskOrganizer.java, Func: addStartingWindow/removeStartingWindow
ShellTaskOrganizer.java,里面其实没有实现,最后也会走到StartingSurfaceDrawer.java,我们不妨把这个文件的log打开来看看。
冷启动图库应用log如下:
shell
D StartingSurfaceDrawer: addSplashScreen com.android.gallery3d theme=7f120034 task=13 suggestType=1
D StartingSurfaceDrawer: window attributes color: 0 icon null duration 0 brandImage null
D StartingSurfaceDrawer: FgMainColor=fff1f1f1 BgMainColor=ff4285f4 IsBgComplex=false FromCache=false ThemeColor=ff000000
D StartingSurfaceDrawer: isRgbSimilarInHsv a: ff000000 b ff4285f4 contrast ratio: 5.8929925
D StartingSurfaceDrawer: hsvDiff -143 ah 0.0 bh 217.41573 as 0.0 bs 0.7295082 av 0.0 bv 0.95686275 sqH 0.6311419710995239 sqS 0.5321822447246269 sqV 0.9155863178770893 root 0.8324483034401676
D StartingSurfaceDrawer: makeSplashScreenContentView: draw whole icon
D StartingSurfaceDrawer: fillViewWithIcon surfaceWindowView android.window.SplashScreenView{beed80 V.E...... ......ID 0,0-0,0}
D StartingSurfaceDrawer: Task start finish, remove starting surface for task 13
V StartingSurfaceDrawer: Removing splash screen window for task: 13
有我们上面提到的addSplashScreen和removeWindowSynced。
冷启动上面3.1 Splash Screen简单自定义 节的demo app, log如下:
shell
StartingSurfaceDrawer: addSplashScreen com.example.mytestapplication theme=7f0f01d3 task=15 suggestType=1
StartingSurfaceDrawer: window attributes color: ffffffff icon android.graphics.drawable.AnimatedVectorDrawable@fe73898 duration 1000 brandImage null
StartingSurfaceDrawer: fillViewWithIcon surfaceWindowView android.window.SplashScreenView{64198f1 V.E...... ......ID 0,0-0,0}
StartingSurfaceDrawer: Task start finish, remove starting surface for task 15
StartingSurfaceDrawer: Removing splash screen window for task: 15
可以看到log时序大体一致,区别是下面的window attributes icon不为空,因为我们在app中定义了。
冷启动StartingWindow,log如下:
sh
StartingSurfaceDrawer: addSplashScreen com.example.android.startingwindow theme=7f0d0005 task=16 suggestType=1
StartingSurfaceDrawer: window attributes color: ffffffff icon android.graphics.drawable.AnimatedVectorDrawable@b2b253f duration 900 brandImage null
StartingSurfaceDrawer: fillViewWithIcon surfaceWindowView android.window.SplashScreenView{cd44355 V.E...... ......ID 0,0-0,0}
StartingSurfaceDrawer: Copying splash screen window view for task: 16 parcelable: android.window.SplashScreenView$SplashScreenViewParcelable@e9c99b3
StartingSurfaceDrawer: Task start finish, remove starting surface for task 16
StartingSurfaceDrawer: Removing splash screen window for task: 16
StartingSurfaceDrawer: App removedthe splash screen. Releasing SurfaceControlViewHost for task:16
这次多了Copying splash screen window view for task这行log,下面具体分析。
3.2.2 copySplashScreenView
分析log发现,冷启动StartingWindow app时,多走了StartingSurfaceDrawer.java的copySplashScreenView函数,那么下面就重点分析下:
3.2.2.1 copySplashScreenView调用栈
目前在systemUI进程中,所以找到ShellTaskOrganizer.java的copySplashScreenView函数,调用它的地方在system server进程,所以去TaskOrganizerController.java中找,发现调用的函数也叫copySplashScreenView。
调用TaskOrganizerController.java的地方在ActivityRecord.java,所以找到requestCopySplashScreen函数,我们不妨在这里加个堆栈调用Log看下:
shell
Test111 : at com.android.server.wm.ActivityRecord.requestCopySplashScreen(ActivityRecord.java:2322)
Test111 : at com.android.server.wm.ActivityRecord.transferSplashScreenIfNeeded(ActivityRecord.java:2311)
Test111 : at com.android.server.wm.ActivityRecord.removeStartingWindow(ActivityRecord.java:2419)
Test111 : at com.android.server.wm.ActivityRecord.onFirstWindowDrawn(ActivityRecord.java:6102)
Test111 : at com.android.server.wm.WindowState.performShowLocked(WindowState.java:4777)
Test111 : at com.android.server.wm.WindowStateAnimator.commitFinishDrawingLocked(WindowStateAnimator.java:280)
...
Test111 : Removing startingView
Test111 : java.lang.Exception
Test111 : at com.android.server.wm.ActivityRecord.lambda$removeStartingWindowAnimation$5(ActivityRecord.java:2473)
Test111 : at com.android.server.wm.ActivityRecord$$ExternalSyntheticLambda5.run(Unknown Source:6)
Test111 : at com.android.server.wm.ActivityRecord.removeStartingWindowAnimation(ActivityRecord.java:2483)
Test111 : at com.android.server.wm.ActivityRecord.onSplashScreenAttachComplete(ActivityRecord.java:2374)
Test111 : at com.android.server.wm.ActivityRecord.splashScreenAttachedLocked(ActivityRecord.java:5656)
Test111 : at com.android.server.wm.ActivityClientController.splashScreenAttached(ActivityClientController.java:764)
Test111 : at android.app.IActivityClientController$Stub.onTransact(IActivityClientController.java:1168)
Test111 : at com.android.server.wm.ActivityClientController.onTransact(ActivityClientController.java:121)
Test111 : at android.os.Binder.execTransactInternal(Binder.java:1184)
Test111 : at android.os.Binder.execTransact(Binder.java:1143)
可以看出来copySplashScreenView也是从FinishDraw流程走过来的,并且和上面分析Remove Splash screen流程时的调用栈基本一致,不同之处在于:
java
// ActivityRecord.java
void removeStartingWindow() {
if (transferSplashScreenIfNeeded()) {
return;
}
removeStartingWindowAnimation(true /* prepareAnimation */);
}
上面分析Remove Splash screen流程时,走了removeStartingWindowAnimation,而此时走了上面的transferSplashScreenIfNeeded,并且返回true,
为什么断定返回true呢,因为根据本次Removing startingView的调用栈看,和上面未自定义Splash Screen时的调用栈不一样了。
java
// ActivityRecord.java
private boolean transferSplashScreenIfNeeded() {
// 1
if (!mWmService.mStartingSurfaceController.DEBUG_ENABLE_SHELL_DRAWER) {
return false;
}
// 2
if (!mHandleExitSplashScreen || mStartingSurface == null || mStartingWindow == null
|| mTransferringSplashScreenState == TRANSFER_SPLASH_SCREEN_FINISH) {
return false;
}
if (isTransferringSplashScreen()) {
return true;
}
requestCopySplashScreen();
return isTransferringSplashScreen();
}
DEBUG_ENABLE_SHELL_DRAWER是根据属性值来的,一般没人设置,所以返回默认值true, 因此走不进去。- 未做复杂自定义的Splash Screen在这里会走进if, 返回false, 通过打log可以看出是取决于
mHandleExitSplashScreen这个值。未做复杂自定义的Splash Screen时,mHandleExitSplashScreen = false, 做复杂自定义的Splash Screen时,mHandleExitSplashScreen = true。这个值是在函数setCustomizeSplashScreenExitAnimation中被设置,而这个函数在activityResumedLocked中被调。
java
// ActivityRecord.java
static void activityResumedLocked(IBinder token, boolean handleSplashScreenExit) {
final ActivityRecord r = ActivityRecord.forTokenLocked(token);
ProtoLog.i(WM_DEBUG_STATES, "Resumed activity; dropping state of: %s", r);
if (r == null) {
// If an app reports resumed after a long delay, the record on server side might have
// been removed (e.g. destroy timeout), so the token could be null.
return;
}
r.setCustomizeSplashScreenExitAnimation(handleSplashScreenExit);
r.setSavedState(null /* savedState */);
r.mDisplayContent.handleActivitySizeCompatModeIfNeeded(r);
r.mDisplayContent.mUnknownAppVisibilityController.notifyAppResumedFinished(r);
}
mHandleExitSplashScreen的值取决与参数handleSplashScreenExit。activityResumedLocked的调用如下:
java
// ActivityClientController.java
@Override
public void activityResumed(IBinder token, boolean handleSplashScreenExit) {
final long origId = Binder.clearCallingIdentity();
synchronized (mGlobalLock) {
ActivityRecord.activityResumedLocked(token, handleSplashScreenExit);
}
Binder.restoreCallingIdentity(origId);
}
我们在AMS-Activity启动流程 这一篇中介绍过Activity的Pause流程,如果app进程处理完Pause时,会在PauseActivityItem.java的postExecute中通过调用
getActivityClientController().activityPaused(token);跨进程调用到system server端。
找到ResumeActivityItem.java的postExecute,果然也是类似的调用:
java
// ResumeActivityItem.java
@Override
public void postExecute(ClientTransactionHandler client, IBinder token,
PendingTransactionActions pendingActions) {
// TODO(lifecycler): Use interface callback instead of actual implementation.
ActivityClient.getInstance().activityResumed(token, client.isHandleSplashScreenExit(token));
}
isHandleSplashScreenExit的实现在ActivityThread.java:
java
@Override
public boolean isHandleSplashScreenExit(@NonNull IBinder token) {
synchronized (this) {
return mSplashScreenGlobal != null && mSplashScreenGlobal.containsExitListener(token);
}
}
mSplashScreenGlobal.containsExitListener(token):如果要返回true, 就需要有人调用SplashScreen.java的setOnExitAnimationListener,
发现在google提供的demo app中会调用setOnExitAnimationListener:
java
//development/samples/StartingWindow/src/com/example/android/startingwindow/CustomizeExitActivity.java
protected void onCreate(Bundle savedInstanceState) {
...
// On Android S, this new method has been added to Activity
SplashScreen splashScreen = getSplashScreen();
// Setting an OnExitAnimationListener on the SplashScreen indicates
// to the system that the application will handle the exit animation.
// This means that the SplashScreen will be inflated in the application
// process once the process has started.
// Otherwise, the splashscreen stays in the SystemUI process and will be
// dismissed once the first frame of the app is drawn
splashScreen.setOnExitAnimationListener(this::onSplashScreenExit);
startingwindow app会在onSplashScreenExit中收到Splash Screen,然后继续做动画效果。
3.2.2.2 copySplashScreenView的实现
分析完copySplashScreenView是如何被调用的,再看下它的实现。
java
// StartingSurfaceDrawer.java
public void copySplashScreenView(int taskId) {
final StartingWindowRecord preView = mStartingWindowRecords.get(taskId);
// 1
SplashScreenViewParcelable parcelable;
SplashScreenView splashScreenView = preView != null ? preView.mContentView : null;
if (splashScreenView != null && splashScreenView.isCopyable()) {
parcelable = new SplashScreenViewParcelable(splashScreenView);
parcelable.setClientCallback(
new RemoteCallback((bundle) -> mSplashScreenExecutor.execute(
() -> onAppSplashScreenViewRemoved(taskId, false))));
// 2
splashScreenView.onCopied();
mAnimatedSplashScreenSurfaceHosts.append(taskId, splashScreenView.getSurfaceHost());
} else {
parcelable = null;
}
if (DEBUG_SPLASH_SCREEN) {
Slog.v(TAG, "Copying splash screen window view for task: " + taskId
+ " parcelable: " + parcelable);
}
// 3
ActivityTaskManager.getInstance().onSplashScreenViewCopyFinished(taskId, parcelable);
}
将splashScreenView封装成一个可序列化的对象。
然后调用splashScreenView.onCopied(),这里并没有做真正的跨进程传递,只是给splashScreenView的内部对象赋值。
调用ATMS的onSplashScreenViewCopyFinished:
java
// ActivityTaskManagerService.java
public void onSplashScreenViewCopyFinished(int taskId, SplashScreenViewParcelable parcelable)
throws RemoteException {
...
final ActivityRecord r = task.getTopWaitSplashScreenActivity();
if (r != null) {
r.onCopySplashScreenFinish(parcelable);
}
}
}
将序列化的splashScreenView对象传给ActivityRecord。
java
// ActivityRecord.java
void onCopySplashScreenFinish(SplashScreenViewParcelable parcelable) {
...
try {
mTransferringSplashScreenState = TRANSFER_SPLASH_SCREEN_ATTACH_TO_CLIENT;
mAtmService.getLifecycleManager().scheduleTransaction(app.getThread(), appToken,
TransferSplashScreenViewStateItem.obtain(parcelable,
windowAnimationLeash));
scheduleTransferSplashScreenTimeout();
...
}
}
将TransferSplashScreenViewStateItem作为ClientTransaction的callback;
ClientLifecycleManager.java的scheduleTransaction会调用ClientTransaction的schedule函数:
java
public void schedule() throws RemoteException {
mClient.scheduleTransaction(this);
}
mClient是app ActivityThread在system server的proxy,所以这里会跨进程调用到app的ActivityThread;
ActivityThread会在切到自己的线程中执行mTransactionExecutor.execute(transaction);
TransactionExecutor.java会从ClientTransaction取出callback对象,也就是我们上面在ActivityRecord.java 设置的TransferSplashScreenViewStateItem,并调用它的execute;
TransferSplashScreenViewStateItem的execute会调用ActivityThread.java的handleAttachSplashScreenView
java
// ActivityThread.java
@Override
public void handleAttachSplashScreenView(@NonNull ActivityClientRecord r,
@Nullable SplashScreenView.SplashScreenViewParcelable parcelable,
@NonNull SurfaceControl startingWindowLeash) {
final DecorView decorView = (DecorView) r.window.peekDecorView();
if (parcelable != null && decorView != null) {
createSplashScreen(r, decorView, parcelable, startingWindowLeash);
...
}
private void createSplashScreen(ActivityClientRecord r, DecorView decorView,
SplashScreenView.SplashScreenViewParcelable parcelable,
@NonNull SurfaceControl startingWindowLeash) {
final SplashScreenView.Builder builder = new SplashScreenView.Builder(r.activity);
final SplashScreenView view = builder.createFromParcel(parcelable).build();
decorView.addView(view);
view.attachHostActivityAndSetSystemUIColors(r.activity, r.window);
view.requestLayout();
view.getViewTreeObserver().addOnDrawListener(new ViewTreeObserver.OnDrawListener() {
private boolean mHandled = false;
@Override
public void onDraw() {
if (mHandled) {
return;
}
mHandled = true;
// Transfer the splash screen view from shell to client.
// Call syncTransferSplashscreenViewTransaction at the first onDraw so we can ensure
// the client view is ready to show and we can use applyTransactionOnDraw to make
// all transitions happen at the same frame.
syncTransferSplashscreenViewTransaction(
view, r.token, decorView, startingWindowLeash);
view.post(() -> view.getViewTreeObserver().removeOnDrawListener(this));
}
});
}
在ActivityThread内,会完成SplashScreenView的拷贝动作,并做显示;
显示完之后,在syncTransferSplashscreenViewTransaction会调用reportSplashscreenViewShown函数:
java
// ActivityClient.java
void reportSplashScreenAttached(IBinder token) {
try {
getActivityClientController().splashScreenAttached(token);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
}
通过ActivityClientController,完成app与system server的跨进程调用, 走到ActivityClientController.java的splashScreenAttached;
在system server内,又会走到ActivityRecord.java的splashScreenAttachedLocked -> onSplashScreenAttachComplete -> removeStartingWindowAnimation, 与我们上面 3.2.2.1 打印的Splash screen退出调用栈是一样的。
4. Disable Splash Screen
可能因为产品要求要要禁止调Splash Screen,有2种方法:
4.1 注释start Splash Screen 流程
在上面第一章节中,创建Splash Screen窗口的流程中,会走到如下代码
java
// Task.java
void startActivityLocked(ActivityRecord r, @Nullable ActivityRecord focusedTopActivity,
boolean newTask, boolean isTaskSwitch, ActivityOptions options,
@Nullable ActivityRecord sourceRecord) {
...
else if (SHOW_APP_STARTING_PREVIEW && doShow) {
...
r.showStartingWindow(prev, newTask, isTaskSwitch,
true /* startActivity */, sourceRecord);
}
}
在调用r.showStartingWindow前会判断SHOW_APP_STARTING_PREVIEW,这个值默认是true,我们改成false 试一下。
发现Splash Screen确实不显示了,但是出现了新的问题。冷启动app的时候,点击icon,会有卡顿的感觉,因为从冷启动app到activity的resume,肯定是要花费时间的。
4.2 更改mSplashScreenIcon赋值
在上面3.1.2 章节中,我们分析了当app自定义splash screen icon时,会走到SplashscreenContentDrawer.java:
java
private static void getWindowAttrs(Context context, SplashScreenWindowAttrs attrs) {
...
attrs.mSplashScreenIcon = safeReturnAttrDefault((def) -> typedArray.getDrawable(
R.styleable.Window_windowSplashScreenAnimatedIcon), null);
...
}
attrs.mSplashScreenIcon首先会从app中取,如果app中未定义,就会默认用app icon,我们不妨在这里做一些更改,
如果app有定义,就不做修改,因为此时app有自己的splash screen,如果没有定义,就定义一个透明的Drawable对象,让系统以为app自定义了splash screen
改动如下:
java
attrs.mSplashScreenIcon = safeReturnAttrDefault((def) -> typedArray.getDrawable(
R.styleable.Window_windowSplashScreenAnimatedIcon), null);
if (attrs.mSplashScreenIcon == null) {
attrs.mSplashScreenIcon = new ColorDrawable(Color.TRANSPARENT);
}
实测下来,没有了显示icon的splash screen,会有一个空白页过度,顺滑了许多。