100%原创,转载注明出处,多谢。
在Android系统中,Activity组件在启动但窗口还未显示出来之时,可以显示一个启动窗口(StartingWindow)。这个启动窗口可以看作是Activity组件的预览窗口。本文就针对starting Window启动和销毁流程进行简单分析, 代码基于android 9.0。过程自己debug一下,也非常简单。
一、显示流程
StartingWindow与Activity的启动流程密切相关,前面Activity启动调用流程如下图所示:
从上面流程看出,在ActivityStack执行startActivityLocked的时候,通过ActivityRecord的showStartingWindow方法开始正式进入starting window的显示流程。
从上一篇的概览我们知道,这个Activity启动的这个部分是属于前期准备阶段,借助PMS,确认要启动的Activity,并对intent component 、和权限等等进行验证,同时根据launcheMode和Flag配置task。在这个时候加载预览窗口貌似也能理解,毕竟之后就要处理Activity的正式加载了,在此之前通过StartingWindow过渡能提升用户体验。
那么就从ActivityRecord #showStartingWindow开始看下显示流程:
ActivityRecord #showStartingWindow
2378 void showStartingWindow(ActivityRecord prev, boolean newTask, boolean taskSwitch) {
2379 showStartingWindow(prev, newTask, taskSwitch, false /* fromRecents */);
2380 }
2381
2382 void showStartingWindow(ActivityRecord prev, boolean newTask, boolean taskSwitch,
2383 boolean fromRecents) {
2384 if (mWindowContainerController == null) {
2385 return;
2386 }
2387 if (mTaskOverlay) {
2388 // We don't show starting window for overlay activities.
2389 return;
2390 }
2391
2392 final CompatibilityInfo compatInfo =
2393 service.compatibilityInfoForPackageLocked(info.applicationInfo);
2394 final boolean shown = mWindowContainerController.addStartingWindow(packageName, theme,
2395 compatInfo, nonLocalizedLabel, labelRes, icon, logo, windowFlags,
2396 prev != null ? prev.appToken : null, newTask, taskSwitch, isProcessRunning(),
2397 allowTaskSnapshot(),
2398 mState.ordinal() >= RESUMED.ordinal() && mState.ordinal() <= STOPPED.ordinal(),
2399 fromRecents);
2400 if (shown) {
2401 mStartingWindowState = STARTING_WINDOW_SHOWN;
2402 }
2403 }
PMS获取到一系列的属性与资源,传入AppWindowContainerController的addStartingWindow方法,通过返回值shown来判断是否把StringWindowState状态置为显示。
紧接着再看看 AppWindowContainerController #addStartingWindow
先交代下类相关情况:
AppWindowContainerController extends WindowContainerController:
AppWindowContainerController继承自WindowContainerController。
WindowContainerController内部有几个变量需要了解下:
class WindowContainerController
31 implements ConfigurationContainerListener{
33 final WindowManagerService mService;
34 final RootWindowContainer mRoot;
35 final WindowHashMap mWindowMap;
37 // The window container this controller owns.
38 E mContainer;
}
继续:
public boolean addStartingWindow(String pkg, int theme, CompatibilityInfo compatInfo,
444 CharSequence nonLocalizedLabel, int labelRes, int icon, int logo, int windowFlags,
445 IBinder transferFrom, boolean newTask, boolean taskSwitch, boolean processRunning,
446 boolean allowTaskSnapshot, boolean activityCreated, boolean fromRecents) {
447 synchronized(mWindowMap) {
448 if (DEBUG_STARTING_WINDOW) Slog.v(TAG_WM, "setAppStartingWindow: token=" + mToken
449 + " pkg=" + pkg + " transferFrom=" + transferFrom + " newTask=" + newTask
450 + " taskSwitch=" + taskSwitch + " processRunning=" + processRunning
451 + " allowTaskSnapshot=" + allowTaskSnapshot);
452
453 if (mContainer == null) {
454 Slog.w(TAG_WM, "Attempted to set icon of non-existing app token: " + mToken);
455 return false;
456 }
457
458 // If the display is frozen, we won't do anything until the actual window is
459 // displayed so there is no reason to put in the starting window.
460 if (!mContainer.okToDisplay()) {
461 return false;
462 }
463
464 if (mContainer.startingData != null) {
465 return false;
466 }
467
468 final WindowState mainWin = mContainer.findMainWindow();
469 if (mainWin != null && mainWin.mWinAnimator.getShown()) {
470 // App already has a visible window...why would you want a starting window?
471 return false;
472 }
473
474 final TaskSnapshot snapshot = mService.mTaskSnapshotController.getSnapshot(
475 mContainer.getTask().mTaskId, mContainer.getTask().mUserId,
476 false /* restoreFromDisk */, false /* reducedResolution */);
477 final int type = getStartingWindowType(newTask, taskSwitch, processRunning,
478 allowTaskSnapshot, activityCreated, fromRecents, snapshot);
479
480 if (type == STARTING_WINDOW_TYPE_SNAPSHOT) {
481 return createSnapshot(snapshot);
482 }
483
484 // If this is a translucent window, then don't show a starting window -- the current
485 // effect (a full-screen opaque starting window that fades away to the real contents
486 // when it is ready) does not work for this.
487 if (DEBUG_STARTING_WINDOW) Slog.v(TAG_WM, "Checking theme of starting window: 0x"
488 + Integer.toHexString(theme));
489 if (theme != 0) {
490 AttributeCache.Entry ent = AttributeCache.instance().get(pkg, theme,
491 com.android.internal.R.styleable.Window, mService.mCurrentUserId);
492 if (ent == null) {
493 // Whoops! App doesn't exist. Um. Okay. We'll just pretend like we didn't
494 // see that.
495 return false;
496 }
//这部分主要是获取APP对应的主题style,这也是app端能决定的是否要StartingWindow的设置,为true,后面的判断直接return 不会执行到scheduleAddStartingWindow
497 final boolean windowIsTranslucent = ent.array.getBoolean(
498 com.android.internal.R.styleable.Window_windowIsTranslucent, false);
499 final boolean windowIsFloating = ent.array.getBoolean(
500 com.android.internal.R.styleable.Window_windowIsFloating, false);
501 final boolean windowShowWallpaper = ent.array.getBoolean(
502 com.android.internal.R.styleable.Window_windowShowWallpaper, false);
503 final boolean windowDisableStarting = ent.array.getBoolean(
504 com.android.internal.R.styleable.Window_windowDisablePreview, false);
505 if (DEBUG_STARTING_WINDOW) Slog.v(TAG_WM, "Translucent=" + windowIsTranslucent
506 + " Floating=" + windowIsFloating
507 + " ShowWallpaper=" + windowShowWallpaper);
508 if (windowIsTranslucent) {
509 return false;
510 }
511 if (windowIsFloating || windowDisableStarting) {
512 return false;
513 }
514 if (windowShowWallpaper) {
515 if (mContainer.getDisplayContent().mWallpaperController.getWallpaperTarget()
516 == null) {
517 // If this theme is requesting a wallpaper, and the wallpaper
518 // is not currently visible, then this effectively serves as
519 // an opaque window and our starting window transition animation
520 // can still work. We just need to make sure the starting window
521 // is also showing the wallpaper.
522 windowFlags |= FLAG_SHOW_WALLPAPER;
523 } else {
524 return false;
525 }
526 }
527 }
528
529 if (mContainer.transferStartingWindow(transferFrom)) {
530 return true;
531 }
532
533 // There is no existing starting window, and we don't want to create a splash screen, so
534 // that's it!
535 if (type != STARTING_WINDOW_TYPE_SPLASH_SCREEN) {
536 return false;
537 }
538
539 if (DEBUG_STARTING_WINDOW) Slog.v(TAG_WM, "Creating SplashScreenStartingData");
540 mContainer.startingData = new SplashScreenStartingData(mService, pkg, theme,
541 compatInfo, nonLocalizedLabel, labelRes, icon, logo, windowFlags,
542 mContainer.getMergedOverrideConfiguration());
543 scheduleAddStartingWindow();
544 }
545 return true;
546 }
首先对照下APP style的设置:
这四个设置系统默认为false,从源码判断来看,只要满足一个为true就会return掉.
其中我debug发现:微信是设置了android:windowDisablePreview = true 禁用了StartingWindow.
好了,话不多说,如果条件都满足,那么接下来会初始化SplashScreenStartingData,
并赋值给了mContainer.startingData,执行scheduleAddStartingWindow()。
AppWindowContainerController #scheduleAddStartingWindow
void scheduleAddStartingWindow() {
568 // Note: we really want to do sendMessageAtFrontOfQueue() because we
569 // want to process the message ASAP, before any other queued
570 // messages.
571 if (!mService.mAnimationHandler.hasCallbacks(mAddStartingWindow)) {
572 if (DEBUG_STARTING_WINDOW) Slog.v(TAG_WM, "Enqueueing ADD_STARTING");
573 mService.mAnimationHandler.postAtFrontOfQueue(mAddStartingWindow);
574 }
575 }
加入轮询的消息池,具体执行的Runnable 是mAddStartingWindow。
116 private final Runnable mAddStartingWindow = new Runnable() {
117
118 @Override
119 public void run() {
120 final StartingData startingData;
121 final AppWindowToken container;
122
123 synchronized (mWindowMap) {
...
133 startingData = mContainer.startingData; //获取之前初始化的SplashScreenStartingData
134 container = mContainer;
135 }
...
149 StartingSurface surface = null;
150 try {
151 surface = startingData.createStartingSurface(container); //创建startingwindow的核心部分
152 } catch (Exception e) {
153 Slog.w(TAG_WM, "Exception when adding starting window", e);
154
155 if (surface != null) {
156 boolean abort = false;
157 synchronized (mWindowMap) {
158 // If the window was successfully added, then
159 // we need to remove it.
160 if (container.removed || container.startingData == null) {
161 if (DEBUG_STARTING_WINDOW) Slog.v(TAG_WM,
162 "Aborted starting " + container
163 + ": removed=" + container.removed
164 + " startingData=" + container.startingData);
165 container.startingWindow = null;
166 container.startingData = null;
167 abort = true;
168 } else {
169 container.startingSurface = surface; //并把StartingSurface赋值给container.startingSurface
170 }
...
177 if (abort) {
178 surface.remove();
179 }
180 } else if (DEBUG_STARTING_WINDOW) {
181 Slog.v(TAG_WM, "Surface returned was null: " + mContainer);
182 }
183 }
184 };
这个阶段最核心的就是创建StartingSurface的过程:startingData.createStartingSurface(container)。而StartingData本身是个接口,它的实现类是SplashScreenStartingData。
27 class SplashScreenStartingData extends StartingData {
…
54 @Override
55 StartingSurface createStartingSurface(AppWindowToken atoken) {
56 return mService.mPolicy.addSplashScreen(atoken.token, mPkg, mTheme, mCompatInfo,
57 mNonLocalizedLabel, mLabelRes, mIcon, mLogo, mWindowFlags,
58 mMergedOverrideConfiguration, atoken.getDisplayContent().getDisplayId());
59 }
60}
这里重点关注createStartingSurface的实现,我们看到,返回的是mService.mPolicy.addSplashScreen,其中mService是WindowManagerService, mPolicy是WindowManagerPolicy.WindowManagerPolicy是个接口,想看addSplashScreen得找对应实现类。于是找到了PhoneWindowManager.
PhoneWindowManager # addSplashScreen
3299 public StartingSurface addSplashScreen(IBinder appToken, String packageName, int theme,
3300 CompatibilityInfo compatInfo, CharSequence nonLocalizedLabel, int labelRes, int icon,
3301 int logo, int windowFlags, Configuration overrideConfig, int displayId) {
...
3318 // Obtain proper context to launch on the right display.
3319 final Context displayContext = getDisplayContext(context, displayId);
3320 if (displayContext == null) {
3321 // Can't show splash screen on requested display, so skip showing at all.
3322 return null;
3323 }
3324 context = displayContext;
3325
3326 if (theme != context.getThemeResId() || labelRes != 0) {
3327 try {
3328 context = context.createPackageContext(packageName, CONTEXT_RESTRICTED);
3329 context.setTheme(theme);
3330 } catch (PackageManager.NameNotFoundException e) {
3331 // Ignore
3332 }
3333 }
3334
3335 if (overrideConfig != null && !overrideConfig.equals(EMPTY)) {
3336 if (DEBUG_SPLASH_SCREEN) Slog.d(TAG, "addSplashScreen: creating context based"
3337 + " on overrideConfig" + overrideConfig + " for splash screen");
3338 final Context overrideContext = context.createConfigurationContext(overrideConfig);
3339 overrideContext.setTheme(theme);
3340 final TypedArray typedArray = overrideContext.obtainStyledAttributes(
3341 com.android.internal.R.styleable.Window);
3342 final int resId = typedArray.getResourceId(R.styleable.Window_windowBackground, 0);
3343 if (resId != 0 && overrideContext.getDrawable(resId) != null) {
3344 // We want to use the windowBackground for the override context if it is
3345 // available, otherwise we use the default one to make sure a themed starting
3346 // window is displayed for the app.
3347 if (DEBUG_SPLASH_SCREEN) Slog.d(TAG, "addSplashScreen: apply overrideConfig"
3348 + overrideConfig + " to starting window resId=" + resId);
3349 context = overrideContext;
3350 }
3351 typedArray.recycle();
3352 }
3353
3354 final PhoneWindow win = new PhoneWindow(context);
3355 win.setIsStartingWindow(true);
3356
3357 CharSequence label = context.getResources().getText(labelRes, null);
3358 // Only change the accessibility title if the label is localized
3359 if (label != null) {
3360 win.setTitle(label, true);
3361 } else {
3362 win.setTitle(nonLocalizedLabel, false);
3363 }
3364 //设置窗口类型为启动窗口类型
3365 win.setType(
3366 WindowManager.LayoutParams.TYPE_APPLICATION_STARTING);
3367
3368 synchronized (mWindowManagerFuncs.getWindowManagerLock()) {
3369 // Assumes it's safe to show starting windows of launched apps while
3370 // the keyguard is being hidden. This is okay because starting windows never show
3371 // secret information.
3372 if (mKeyguardOccluded) {
3373 windowFlags |= FLAG_SHOW_WHEN_LOCKED;
3374 }
3375 }
3376
3377 // Force the window flags: this is a fake window, so it is not really
3378 // touchable or focusable by the user. We also add in the ALT_FOCUSABLE_IM
3379 // flag because we do know that the next window will take input
3380 // focus, so we want to get the IME window up on top of us right away.
//设置不可触摸和聚焦
3381 win.setFlags(
3382 windowFlags|
3383 WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE|
3384 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE|
3385 WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM,
3386 windowFlags|
3387 WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE|
3388 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE|
3389 WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
3390
3391 win.setDefaultIcon(icon);
3392 win.setDefaultLogo(logo);
3393
3394 win.setLayout(WindowManager.LayoutParams.MATCH_PARENT,
3395 WindowManager.LayoutParams.MATCH_PARENT);
3396
3397 final WindowManager.LayoutParams params = win.getAttributes();
3398 params.token = appToken;
3399 params.packageName = packageName;
3400 params.windowAnimations = win.getWindowStyle().getResourceId(
3401 com.android.internal.R.styleable.Window_windowAnimationStyle, 0);
3402 params.privateFlags |=
3403 WindowManager.LayoutParams.PRIVATE_FLAG_FAKE_HARDWARE_ACCELERATED;
3404 params.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
3405
3406 if (!compatInfo.supportsScreen()) {
3407 params.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COMPATIBLE_WINDOW;
3408 }
3409
3410 params.setTitle("Splash Screen " + packageName);
3411 addSplashscreenContent(win, context);
3412 //获取WMS
3413 wm = (WindowManager) context.getSystemService(WINDOW_SERVICE);
3414 view = win.getDecorView();
3415
3416 if (DEBUG_SPLASH_SCREEN) Slog.d(TAG, "Adding splash screen window for "
3417 + packageName + " / " + appToken + ": " + (view.getParent() != null ? view : null));
3418 //窗口添加视图
3419 wm.addView(view, params);
3420
3421 // Only return the view if it was successfully added to the
3422 // window manager... which we can tell by it having a parent.
3423 return view.getParent() != null ? new SplashScreenSurface(view, appToken) : null;
3424 } catch (WindowManager.BadTokenException e) {
3425 // ignore
3426 Log.w(TAG, appToken + " already running, starting window not displayed. " +
3427 e.getMessage());
3428 } catch (RuntimeException e) {
3429 // don't crash if something else bad happens, for example a
3430 // failure loading resources because we are loading from an app
3431 // on external storage that has been unmounted.
3432 Log.w(TAG, appToken + " failed creating starting window", e);
3433 } finally {
3434 if (view != null && view.getParent() == null) {
3435 Log.w(TAG, "view not successfully added to wm, removing view");
3436 wm.removeViewImmediate(view);
3437 }
3438 }
3439
3440 return null;
3441 }
3442
这里显然是Starting Window 显示的核心代码了。创建窗口,初始窗口和视图,并将窗口添加到WMS,完成了Starting window的显示。
流程简单示意如下:
二、销毁过程
Activity组件启动完成之后显示对应的窗口时,启动窗口的过渡作用就已经完成了,此时需要先销毁starting window在加载显示对应Activity的窗口。
我们知道,在WindowManagerService服务中,每一个窗口都对应有一个WindowState对象。每当WindowManagerService服务需要显示一个窗口的时候,就会调用一个对应的WindowState对象的成员函数performShowLocked。WindowState类的成员函数performShowLocked在执行的过程中,就会检查当前正在处理的WindowState对象所描述的窗口是否设置有启动窗口。
WindowState# performShowLocked
3793 // This must be called while inside a transaction.
3794 boolean performShowLocked() {
...
3801 logPerformShow("performShow on ");
3802
3803 final int drawState = mWinAnimator.mDrawState;
//HAS_DRAWN = 4; //窗口已经显示在屏幕上 , READY_TO_SHOW = 3;//窗口准备显示
3804 if ((drawState == HAS_DRAWN || drawState == READY_TO_SHOW)
3805 && mAttrs.type != TYPE_APPLICATION_STARTING && mAppToken != null) { // 不是starting window
3806 mAppToken.onFirstWindowDrawn(this, mWinAnimator);
3807 }
...
3847 return true;
3848 }
当前准备显示的Window不是启动窗口类型,那么执行AppWindowToken的onFirstWindowDrawn
AppWindowToken #onFirstWindowDrawn
97 void onFirstWindowDrawn(WindowState win, WindowStateAnimator winAnimator) {
298 firstWindowDrawn = true;
299
300 // We now have a good window to show, remove dead placeholders
301 removeDeadWindows();
302
303 if (startingWindow != null) {
304 if (DEBUG_STARTING_WINDOW || DEBUG_ANIM) Slog.v(TAG, "Finish starting "
305 + win.mToken + ": first real window is shown, no animation");
306 // If this initial window is animating, stop it -- we will do an animation to reveal
307 // it from behind the starting window, so there is no need for it to also be doing its
308 // own stuff.
309 win.cancelAnimation();
310 if (getController() != null) {
311 getController().removeStartingWindow();
312 }
313 }
314 updateReportedVisibilityLocked();
315 }
getContoller获取的是AppWindowContainerController,由它执行removeStartingWindow.
AppWindowContainerController #removeStartingWindow
595 public void removeStartingWindow() {
596 synchronized (mWindowMap) {
597 if (mContainer.startingWindow == null) {
598 if (mContainer.startingData != null) {
599 // Starting window has not been added yet, but it is scheduled to be added.
600 // Go ahead and cancel the request.
601 if (DEBUG_STARTING_WINDOW) Slog.v(TAG_WM,
602 "Clearing startingData for token=" + mContainer);
603 mContainer.startingData = null;
604 }
605 return;
606 }
607
608 final StartingSurface surface;
609 if (mContainer.startingData != null) {
610 surface = mContainer.startingSurface;
611 mContainer.startingData = null;
612 mContainer.startingSurface = null;
613 mContainer.startingWindow = null;
614 mContainer.startingDisplayed = false;
615 if (surface == null) {
616 if (DEBUG_STARTING_WINDOW) {
617 Slog.v(TAG_WM, "startingWindow was set but startingSurface==null, couldn't "
618 + "remove");
619 }
620 return;
621 }
622 } else {
623 if (DEBUG_STARTING_WINDOW) {
624 Slog.v(TAG_WM, "Tried to remove starting window but startingWindow was null:"
625 + mContainer);
626 }
627 return;
628 }
629
630 if (DEBUG_STARTING_WINDOW) Slog.v(TAG_WM, "Schedule remove starting " + mContainer
631 + " startingWindow=" + mContainer.startingWindow
632 + " startingView=" + mContainer.startingSurface
633 + " Callers=" + Debug.getCallers(5));
634
635 // Use the same thread to remove the window as we used to add it, as otherwise we end up
636 // with things in the view hierarchy being called from different threads.
637 mService.mAnimationHandler.post(() -> {
638 if (DEBUG_STARTING_WINDOW) Slog.v(TAG_WM, "Removing startingView=" + surface);
639 try {
640 surface.remove();
641 } catch (Exception e) {
642 Slog.w(TAG_WM, "Exception when removing starting window", e);
643 }
644 });
645 }
646 }
执行销毁startingwindow操作:
mContainer 把对应属性置空,StartingSurface本身remove。
流程简单示意如下:
三、 App StartingWindow 的处理方式:
1 不做任何操作,那么会使用系统默认的StartingWindow. 但是背景是默认的,可能跟app启动页形成色差。
2 自定义StartingWindow
主要设置这两个属性,bg可设颜色 或者 与启动页一致的背景图。
3 禁止使用startingWindow
4 使用透明背景startingWindow
目前看小部分主流app是禁用的,高配手机上并没有感觉到明显的差别,可能低端手机会有区别吧,这个可以自行验证,如果想增强第一帧的显示体验就加上,另外它本身并不会影响到冷启时间。