[源码分析]为什么Dialog不能使用Application作为Context进行初始化

910 阅读4分钟

1.错误发生

当我们通过如下方式构造Dialog并显示时,就会出现Crash

Dialog构造的时候如果使用的是Application作为Context,当调用show的时候就会报BadTokenException异常

我们跟着这个堆栈来从源码分析为什么这里会报异常

首先发现,不管用的哪个context初始化的时候都不会报错,因此使用Application和Activity作为context的区别在于调用show方法的时候,堆栈可以看到show的时候会调用WindowManagerImpl的addView方法

2.Activity作为Context和使用Application作为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无法被正常管理,他在整个窗口结构内就是一个“黑户”,所以必须要抛出异常。

参考文章:juejin.cn/post/696876…