[源碼分析]為什麼Dialog不能使用Application作為Context進行初始化

語言: CN / TW / HK

1.錯誤發生

當我們通過如下方式構造Dialog並顯示時,就會出現Crash

Dialog構造的時候如果使用的是Application作為Context,當調用show的時候就會報BadTokenException異常

我們跟着這個堆棧來從源碼分析為什麼這裏會報異常

首先發現,不管用的哪個context初始化的時候都不會報錯,因此使用Application和Activity作為context的區別在於調用show方法的時候,堆棧可以看到show的時候會調用WindowManagerImpl的addView方法

2.Activity作為Context和使用Applicatio作為Context區別

其實這裏發生異常的原因就在於mWindowManager(WindowManagerImpl)的不同

``` public void show() { ... mWindowManager.addView(mDecor, l); if (restoreSoftInputMode) { l.softInputMode &= ~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION; }

mShowing = true;

sendShowMessage();

} ```

2.1 Activity重寫getSystemService

首先,Activity會重寫getSystemService方法,可以看到這裏如果請求的是WindowManager,返回的是在Activity內的mWindowManager

這裏的mWindowManager是在Activity 與窗口進行綁定過程中也就是在Attach方法內定義的

``` public Object getSystemService(@ServiceName @NonNull String name) { if (getBaseContext() == null) { throw new IllegalStateException( "System services not available to Activities before onCreate()"); }

    if (WINDOW_SERVICE.equals(name)) {
        return mWindowManager;
    } else if (SEARCH_SERVICE.equals(name)) {
        ensureSearchManager();
        return mSearchManager;
    }
    return super.getSystemService(name);
}

```

2.2 Activity#attach

attach方法主要用來進行Activity和窗口的一些綁定操作

這裏傳來的token就是Activity的token,token的介紹後續再寫一篇blog進行詳細介紹。 這裏可以先理解為該Activity的在系統端的令牌或者説身份標識。如每個窗口都有一個WindowToken,而該窗口容器的子窗口的token是和他父容器一致的。

``` final void attach(Context context, ActivityThread aThread, Instrumentation instr, IBinder token, int ident, Application application, Intent intent, ActivityInfo info, CharSequence title, Activity parent, String id, NonConfigurationInstances lastNonConfigurationInstances, Configuration config, String referrer, IVoiceInteractor voiceInteractor, Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken, IBinder shareableActivityToken) { attachBaseContext(context);

    mFragments.attachHost(null /*parent*/);

    mWindow = new PhoneWindow(this, window, activityConfigCallback);

    ...

    mWindow.setWindowManager(
            (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
            mToken, mComponent.flattenToString(),
            (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
    if (mParent != null) {
        mWindow.setContainer(mParent.getWindow());
    }
    mWindowManager = mWindow.getWindowManager();
    ...
}

```

這裏調用了Window的setWindowManager方法

Window.java public void setWindowManager(WindowManager wm, IBinder appToken, String appName, boolean hardwareAccelerated) { mAppToken = appToken; mAppName = appName; mHardwareAccelerated = hardwareAccelerated; if (wm == null) { wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE); } mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this); }

繼續看這裏的createLocalWindowManager方法

可以看到這裏創建了WindowManagerImpl並傳遞了一個parentWindow參數,代表當前的這個mWindowManager是存在父窗口的

``` public WindowManagerImpl createLocalWindowManager(Window parentWindow) { return new WindowManagerImpl(mContext, parentWindow, mWindowContextToken); }

```

從目前可以看到:通過Activity作為context和使用ApplicationContext區別在於,使用前者的情況下創建的WindowManagerImpl是存在parentWindow的,而後者就是原生的WindowManagerImpl

這個parentWindow作用體現在後續添加Dialog窗口的過程中這個窗口是否存在token

3.Dialog窗口添加流程

Dialog show方法中調用了mWindowManager的addView方法,也就進入了WindowManagerImpl的addView方法

``` public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) { applyTokens(params); mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow, mContext.getUserId()); }

```

繼續進入WindowManagerGlobal的addView方法,可以看到這裏傳遞了mParentWindow,因此只有Activity作為context初始化Dialog的時候,parentWindow是不為null的,而ApplicationContext作為context的時候這裏的mParentWindow為null

addView方法會去初始化ViewRootImpl,並將DecorView作為參數添加到ViewRootImpl,後續Activity與WMS的交互就是通過ViewRootImpl實現的。

``` WindowManagerGlobal.java //他是進程惟一的 他會保存當前Activity的所有RootView等 public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow, int userId) { if (view == null) { throw new IllegalArgumentException("view must not be null"); } if (display == null) { throw new IllegalArgumentException("display must not be null"); } if (!(params instanceof WindowManager.LayoutParams)) { throw new IllegalArgumentException("Params must be WindowManager.LayoutParams"); }

final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;

if (parentWindow != null) {  //activity作為Context進入這裏 見3.1
    parentWindow.adjustLayoutParamsForSubWindow(wparams);
} else {
    // If there's no parent, then hardware acceleration for this view is
    // set from the application's hardware acceleration setting.
    final Context context = view.getContext();
    if (context != null
            && (context.getApplicationInfo().flags
                    & ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
        wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
    }
}

ViewRootImpl root;
View panelParentView = null;

synchronized (mLock) {
    // Start watching for system property changes.
    if (mSystemPropertyUpdater == null) {
        mSystemPropertyUpdater = new Runnable() {
            @Override public void run() {
                synchronized (mLock) {
                    for (int i = mRoots.size() - 1; i >= 0; --i) {
                        mRoots.get(i).loadSystemProperties();
                    }
                }
            }
        };
        SystemProperties.addChangeCallback(mSystemPropertyUpdater);
    }

    int index = findViewLocked(view, false);
    if (index >= 0) {
        if (mDyingViews.contains(view)) {
            // Don't wait for MSG_DIE to make it's way through root's queue.
            mRoots.get(index).doDie();
        } else {
            throw new IllegalStateException("View " + view
                    + " has already been added to the window manager.");
        }
        // The previous removeView() had not completed executing. Now it has.
    }

    // If this is a panel window, then find the window it is being
    // attached to for future reference.
    if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
            wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
        final int count = mViews.size();
        for (int i = 0; i < count; i++) {
            if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
                panelParentView = mViews.get(i);
            }
        }
    }

    //在setView的時候會去創建ViewRootImpl 它是負責view和wms交互橋樑
    //這裏的view就是DecorView
    root = new ViewRootImpl(view.getContext(), display);

    view.setLayoutParams(wparams);

    mViews.add(view);
    mRoots.add(root);
    mParams.add(wparams);

    // do this last because it fires off messages to start doing things
    try {
    //調用ViewRootImpl的setView方法 見3.2
        root.setView(view, wparams, panelParentView, userId);
    } catch (RuntimeException e) {
        // BadTokenException or InvalidDisplayException, clean up.
        if (index >= 0) {
            removeViewLocked(index, true);
        }
        throw e;
    }
}

}

```

3.1 parentWindow.adjustLayoutParamsForSubWindow

Activity作為context會進入parentWindow.adjustLayoutParamsForSubWindow這個分支

Dialog的窗口type是TYPE_APPLICATION

``` void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) { CharSequence curTitle = wp.getTitle(); if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW && wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) { if (wp.token == null) { View decor = peekDecorView(); if (decor != null) { wp.token = decor.getWindowToken(); } } ... } else if (wp.type >= WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW && wp.type <= WindowManager.LayoutParams.LAST_SYSTEM_WINDOW) { ... } else { //進入這個分支 if (wp.token == null) { wp.token = mContainer == null ? mAppToken : mContainer.mAppToken; } if ((curTitle == null || curTitle.length() == 0) && mAppName != null) { wp.setTitle(mAppName); } } if (wp.packageName == null) { wp.packageName = mContext.getPackageName(); } }

```

在這裏會設置WindowManager.LayoutParams的token值,將mAppToken賦值給wp.token

這裏的mToken是在上面setWindowManager時候傳遞來的,也就是Activity的token,是Activity在被launch的時候調用attach方法的時候傳來的。

3.2 ViewRootImpl#setView

``` public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView, int userId) { ...

try { mOrigWindowType = mWindowAttributes.type; mAttachInfo.mRecomputeGlobalAttributes = true; collectViewAttributes(); adjustLayoutParamsForCompatibility(mWindowAttributes); controlInsetsForCompatibility(mWindowAttributes); //Binder調用添加窗口 res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), userId, mInsetsController.getRequestedVisibility(), inputChannel, mTempInsets, mTempControls); ...

if (res < WindowManagerGlobal.ADD_OKAY) { //見3.4 mAttachInfo.mRootView = null; mAdded = false; mFallbackEventHandler.setView(null); unscheduleTraversals(); setAccessibilityFocus(null, null); switch (res) { case WindowManagerGlobal.ADD_BAD_APP_TOKEN: case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN: throw new WindowManager.BadTokenException( "Unable to add window -- token " + attrs.token + " is not valid; is your activity running?"); case WindowManagerGlobal.ADD_NOT_APP_TOKEN: throw new WindowManager.BadTokenException( "Unable to add window -- token " + attrs.token + " is not for an application"); case WindowManagerGlobal.ADD_APP_EXITING: throw new WindowManager.BadTokenException( "Unable to add window -- app for token " + attrs.token + " is exiting"); }

```

這裏就是根據mWindowSession.addToDisplayAsUser的返回值res進行結果判斷,最終拋出異常的就是這裏。

首先這裏的IWindowSession是一個Binder接口,對應的是系統進程的Session.java,ViewRootImpl通過mWindowSession來和WMS進行通信,實現添加、刪除窗口、relayout等操作,這裏會binder調用到系統進程的addToDisplayAsUser方法,

public int addToDisplay(IWindow window, WindowManager.LayoutParams attrs, int viewVisibility, int displayId, InsetsState requestedVisibility, InputChannel outInputChannel, InsetsState outInsetsState, InsetsSourceControl[] outActiveControls) { return mService.addWindow(this, window, attrs, viewVisibility, displayId, UserHandle.getUserId(mUid), requestedVisibility, outInputChannel, outInsetsState, outActiveControls); } 繼續調用WMS的addWindow方法添加窗口

3.3 WMS#addWindow

```

public int addWindow(Session session, IWindow client, LayoutParams attrs, int viewVisibility, int displayId, int requestUserId, InsetsState requestedVisibility, InputChannel outInputChannel, InsetsState outInsetsState, InsetsSourceControl[] outActiveControls) {

WindowState parentWindow = null;

...

if (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW) { //只有當窗口類型是SUBWINDOW的時候才會賦值parentWindow 見C1 parentWindow = windowForClientLocked(null, attrs.token, false); if (parentWindow == null) { ProtoLog.w(WM_ERROR, "Attempted to add window with token that is not a window: " + "%s. Aborting.", attrs.token); return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN; } if (parentWindow.mAttrs.type >= FIRST_SUB_WINDOW && parentWindow.mAttrs.type <= LAST_SUB_WINDOW) { ProtoLog.w(WM_ERROR, "Attempted to add window with token that is a sub-window: " + "%s. Aborting.", attrs.token); return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN; } }

final boolean hasParent = parentWindow != null; // Use existing parent window token for child windows since they go in the same token // as there parent window so we can apply the same policy on them. //獲取當前新建窗口token 見C2 WindowToken token = displayContent.getWindowToken( hasParent ? parentWindow.mAttrs.token : attrs.token); // If this is a child window, we want to apply the same type checking rules as the // parent window type. final int rootType = hasParent ? parentWindow.mAttrs.type : type;

        boolean addToastWindowRequiresToken = false;

        final IBinder windowContextToken = attrs.mWindowContextToken;

        if (token == null) { //見C3
        //該方法主要是針對不同的窗口類型輸出log 都返回false
            if (!unprivilegedAppCanCreateTokenWith(parentWindow, callingUid, type,
                    rootType, attrs.token, attrs.packageName)) {
                return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
            }
            if (hasParent) {
                // Use existing parent window token for child windows.
                token = parentWindow.mToken;

```

C1 :首先根據窗口TYPE類型進行一些特殊判斷,這裏Dialog類型是TYPE_APPLICATION, 所以這裏的parentWindow不會進行賦值。

C2:接下來就是要獲取當前窗口的token,如果當前窗口沒有父窗口就從窗口參數attrs內獲取token

由於Activity作為context時之前在adjustLayoutParamsForSubWindow方法內將Activity的token賦值給了wp.token(見3.1),所以此時這裏的attrs.token不為null,而以application作為context構造時則這裏的token為null

C3:當token為null時 這裏直接會返回WindowManagerGlobal.ADD_BAD_APP_TOKEN作為res

3.4 直接看3.2的代碼,當返回res為ADD_BAD_APP_TOKEN,則拋出異常

case WindowManagerGlobal.ADD_BAD_APP_TOKEN: case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN: throw new WindowManager.BadTokenException( "Unable to add window -- token " + attrs.token + " is not valid; is your activity running?");

這也就是我們看到堆棧內拋出的異常。

總結

所以在初始化Dialog的時候,一定要注意使用Activity作為context,否則會出現異常。

其實該問題主要原因就是Dialog使用application作為context構造的話,會導致**在系統端該dialog對應的窗口沒有token,沒有token會使得該窗口在WMS無法被正常管理,他在整個窗口結構內就是一個“黑户”,所以必須要拋出異常。

參考文章:https://juejin.cn/post/6968760157954113549