四大组件之Activity(二)-StartingWindow流程分析

100%原创,转载注明出处,多谢。

在Android系统中,Activity组件在启动但窗口还未显示出来之时,可以显示一个启动窗口(StartingWindow)。这个启动窗口可以看作是Activity组件的预览窗口。本文就针对starting Window启动和销毁流程进行简单分析, 代码基于android 9.0。过程自己debug一下,也非常简单。

一、显示流程

StartingWindow与Activity的启动流程密切相关,前面Activity启动调用流程如下图所示:

四大组件之Activity(二)-StartingWindow流程分析_第1张图片
StartingWindow启动前流程

从上面流程看出,在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(二)-StartingWindow流程分析_第2张图片
StartingWindow启动流程
二、销毁过程

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。

流程简单示意如下:

四大组件之Activity(二)-StartingWindow流程分析_第3张图片
StartingWindow销毁流程
三、 App StartingWindow 的处理方式:

1 不做任何操作,那么会使用系统默认的StartingWindow. 但是背景是默认的,可能跟app启动页形成色差。

2 自定义StartingWindow

       

主要设置这两个属性,bg可设颜色 或者 与启动页一致的背景图。

3 禁止使用startingWindow

  

4 使用透明背景startingWindow


目前看小部分主流app是禁用的,高配手机上并没有感觉到明显的差别,可能低端手机会有区别吧,这个可以自行验证,如果想增强第一帧的显示体验就加上,另外它本身并不会影响到冷启时间。

你可能感兴趣的:(四大组件之Activity(二)-StartingWindow流程分析)