100%原創(chuàng),轉(zhuǎn)載注明出處,多謝。
在Android系統(tǒng)中,Activity組件在啟動但窗口還未顯示出來之時,可以顯示一個啟動窗口(StartingWindow)。這個啟動窗口可以看作是Activity組件的預(yù)覽窗口。本文就針對starting Window啟動和銷毀流程進(jìn)行簡單分析, 代碼基于android 9.0。過程自己debug一下,也非常簡單。
一、顯示流程
StartingWindow與Activity的啟動流程密切相關(guān),前面Activity啟動調(diào)用流程如下圖所示:
從上面流程看出,在ActivityStack執(zhí)行startActivityLocked的時候,通過ActivityRecord的showStartingWindow方法開始正式進(jìn)入starting window的顯示流程。
從上一篇的概覽我們知道,這個Activity啟動的這個部分是屬于前期準(zhǔn)備階段,借助PMS,確認(rèn)要啟動的Activity,并對intent component 、和權(quán)限等等進(jìn)行驗證,同時根據(jù)launcheMode和Flag配置task。在這個時候加載預(yù)覽窗口貌似也能理解,畢竟之后就要處理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狀態(tài)置為顯示。
緊接著再看看 AppWindowContainerController #addStartingWindow
先交代下類相關(guān)情況:
AppWindowContainerController extends WindowContainerController:
AppWindowContainerController繼承自WindowContainerController。
WindowContainerController內(nèi)部有幾個變量需要了解下:
class WindowContainerController<E extends WindowContainer, I extends WindowContainerListener>
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;
}
繼續(xù):
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對應(yīng)的主題style,這也是app端能決定的是否要StartingWindow的設(shè)置,為true,后面的判斷直接return 不會執(zhí)行到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的設(shè)置:
<style name="AppTheme.StartingWindowTheme" parent="AppTheme">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowIsFloating">true</item>
<item name="android:windowShowWallpaper">true</item>
<item name="android:windowDisablePreview">true</item>
</style>
這四個設(shè)置系統(tǒng)默認(rèn)為false,從源碼判斷來看,只要滿足一個為true就會return掉.
其中我debug發(fā)現(xiàn):微信是設(shè)置了android:windowDisablePreview = true 禁用了StartingWindow.
好了,話不多說,如果條件都滿足,那么接下來會初始化SplashScreenStartingData,
并賦值給了mContainer.startingData,執(zhí)行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 }
加入輪詢的消息池,具體執(zhí)行的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); //創(chuàng)建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 };
這個階段最核心的就是創(chuàng)建StartingSurface的過程:startingData.createStartingSurface(container)。而StartingData本身是個接口,它的實現(xiàn)類是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}
這里重點關(guān)注createStartingSurface的實現(xiàn),我們看到,返回的是mService.mPolicy.addSplashScreen,其中mService是WindowManagerService, mPolicy是WindowManagerPolicy.WindowManagerPolicy是個接口,想看addSplashScreen得找對應(yīng)實現(xiàn)類。于是找到了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 //設(shè)置窗口類型為啟動窗口類型
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.
//設(shè)置不可觸摸和聚焦
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 顯示的核心代碼了。創(chuàng)建窗口,初始窗口和視圖,并將窗口添加到WMS,完成了Starting window的顯示。
流程簡單示意如下:
二、銷毀過程
Activity組件啟動完成之后顯示對應(yīng)的窗口時,啟動窗口的過渡作用就已經(jīng)完成了,此時需要先銷毀starting window在加載顯示對應(yīng)Activity的窗口。
我們知道,在WindowManagerService服務(wù)中,每一個窗口都對應(yīng)有一個WindowState對象。每當(dāng)WindowManagerService服務(wù)需要顯示一個窗口的時候,就會調(diào)用一個對應(yīng)的WindowState對象的成員函數(shù)performShowLocked。WindowState類的成員函數(shù)performShowLocked在執(zhí)行的過程中,就會檢查當(dāng)前正在處理的WindowState對象所描述的窗口是否設(shè)置有啟動窗口。
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; //窗口已經(jīng)顯示在屏幕上 , READY_TO_SHOW = 3;//窗口準(zhǔn)備顯示
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 }
當(dāng)前準(zhǔn)備顯示的Window不是啟動窗口類型,那么執(zhí)行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,由它執(zhí)行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 }
執(zhí)行銷毀startingwindow操作:
mContainer 把對應(yīng)屬性置空,StartingSurface本身remove。
流程簡單示意如下:
三、 App StartingWindow 的處理方式:
1 不做任何操作,那么會使用系統(tǒng)默認(rèn)的StartingWindow. 但是背景是默認(rèn)的,可能跟app啟動頁形成色差。
2 自定義StartingWindow
<style name="WelcomeTheme" parent="@style/AppTheme">
<item name="android:windowBackground">@mipmap/ic_splash</item>
</style>
主要設(shè)置這兩個屬性,bg可設(shè)顏色 或者 與啟動頁一致的背景圖。
3 禁止使用startingWindow
<style name="WelcomeTheme" parent="AppTheme">
<item name="android:windowDisablePreview">true</item>
</style>
4 使用透明背景startingWindow
<style name="WelcomeTheme" parent="AppTheme">
<item name="android:windowIsTranslucent">true</item>
</style>
目前看小部分主流app是禁用的,高配手機(jī)上并沒有感覺到明顯的差別,可能低端手機(jī)會有區(qū)別吧,這個可以自行驗證,如果想增強(qiáng)第一幀的顯示體驗就加上,另外它本身并不會影響到冷啟時間。