AMS-SplashScreen分析

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);              
  1. token这次不是null了,因为attrs.token是上面ActivityRecord对象com.android.gallery3d/.app.GalleryActivity,通过调用TaskOrganizerController.javaaddStartingWindow函数时,将自己的token(因为ActivityRecord继承自WindowToken,其实是父类WindowToken的变量)传递进来的,然后经过systemUI进程,再传递到system server进程中WMS的addWindow,所以attrs.token 就是com.android.gallery3d/.app.GalleryActivity的token。

  2. 创建名为Splash Screen com.android.gallery3d的WindowState对象。

  3. 将WindowState挂载到对应token(com.android.gallery3d/.app.GalleryActivity)上,我们用dump activity containers来验证一下。

    shell 复制代码
    ACTIVITY 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.javaShellTaskOrganizer.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.javaremoveWindowSynced,然后里面调用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.javacreateContentView -> 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

有我们上面提到的addSplashScreenremoveWindowSynced

冷启动上面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.javacopySplashScreenView函数,那么下面就重点分析下:

3.2.2.1 copySplashScreenView调用栈

目前在systemUI进程中,所以找到ShellTaskOrganizer.javacopySplashScreenView函数,调用它的地方在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();
}
  1. DEBUG_ENABLE_SHELL_DRAWER是根据属性值来的,一般没人设置,所以返回默认值true, 因此走不进去。
  2. 未做复杂自定义的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的值取决与参数handleSplashScreenExitactivityResumedLocked的调用如下:

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.javapostExecute中通过调用

getActivityClientController().activityPaused(token);跨进程调用到system server端。

找到ResumeActivityItem.javapostExecute,果然也是类似的调用:

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.javasetOnExitAnimationListener

发现在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.javascheduleTransaction会调用ClientTransactionschedule函数:

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;

TransferSplashScreenViewStateItemexecute会调用ActivityThread.javahandleAttachSplashScreenView

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.javasplashScreenAttached;

在system server内,又会走到ActivityRecord.javasplashScreenAttachedLocked -> 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,会有一个空白页过度,顺滑了许多。

相关推荐
常利兵3 小时前
AGP 9.0升级攻略:挥别技术旧疾,迎接开发新程
android
轩情吖3 小时前
MySQL内置函数
android·数据库·c++·后端·mysql·开发·函数
Digitally3 小时前
如何在安卓设备上将照片移动到SD卡
android
Kapaseker3 小时前
一杯半 Kotlin 美式详解 value class
android·kotlin
zhouping@3 小时前
[NPUCTF2020]ezinclude
android·web安全
廖圣平3 小时前
Drogon 现代化C ++高性能框架
android·c语言·开发语言
恋猫de小郭3 小时前
Flutter Beta 版本引入 ScrollCacheExtent ,并修复长久存在的 shrinkWrap NaN 问题
android·前端·flutter
黄林晴3 小时前
你写过多少个重复的 @Preview?Compose 终于要解决这个问题了
android
REDcker3 小时前
Android MediaCodec 架构与实现解析
android·架构