[Android]从源码的角度理解为什么Dialog不能在Application中展示

时间:2021-7-20 作者:qvyue

首先,写一个demo:

[Android]从源码的角度理解为什么Dialog不能在Application中展示
image.png

运行后,一定会报如下的错误:

[Android]从源码的角度理解为什么Dialog不能在Application中展示
image.png

这个错误是怎么来的呢,所谓的token null is not valid中的token又是什么呢?本篇我们来通过源码来分析一下。

1. 错误追踪

首先跟着源码的步伐追踪一下为什么会报这个错误,从Dialog#show开始

public void show() {
    ...
    mWindowManager.addView(mDecor, l);
    ...
   
}

这里调用的mWindowManager的addView方法,其形参mDecor是我们解析到的Dialog的DecorView, l 是WindowManager.LayoutParams 属性; mWindowManager的具体是现实WindowManagerImpl。

WindowManagerImpl调用WindowManagerGlobal#addView继续执行:

public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {

        ....
        root = new ViewRootImpl(view.getContext(), display);

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

这里创建了viewRootImpl并执行了ViewRootImpl#setView方法:

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
        if (mView == null) {
            mView = view;
            ....
            try {
                res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                        getHostVisibility(), mDisplay.getDisplayId(),
                        mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                        mAttachInfo.mOutsets, mInputChannel);
            } catch (RemoteException e) {
            } finally {
                
            }
            ...
            if (res 

在这个方法中,我们找到了抛出异常的地方,当res的值为WindowManagerGlobal.ADD_BAD_APP_TOKEN或则WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN时,会抛出文章开头处的错误。
这个res是IWindowSession#addToDisplay方法的返回值。IWindowSession是一个Binder接口,负责ViewRootImpl和WindowManagerService的通信,在ViewRootImpl对象创建时获取。在这里IWindowSession#addToDisplay最终会调用WindowManagerService#addWindow方法:

// com.android.server.wm.WindowManagerService
public int addWindow(Session session, IWindow client, int seq,
            WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
            Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
            InputChannel outInputChannel) {
    ...
    synchronized(mWindowMap) {
        ...
        if (type >= FIRST_SUB_WINDOW && type = FIRST_SUB_WINDOW
                    && parentWindow.mAttrs.type = FIRST_APPLICATION_WINDOW && rootType 

通过这个方法,可以确定方法返回WindowManagerGlobal.ADD_BAD_APP_TOKEN或则WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN的条件,即Window的类型为子窗口时,parentWindow为空会返回WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN,当Window类型为APPLICATION_WINDOW时,当对应的WindowToken为空就会返回WindowManagerGlobal.ADD_BAD_APP_TOKEN

2. WindowToken是什么

在第一章节的最后一部分,我们引入了一个新概念:WindowToken,它和这个异常的判断息息相关,那么它是在哪里创建的呢?作用又是什么呢?

看下它的获取方式:

    WindowToken getWindowToken(IBinder binder) {
        return mTokenMap.get(binder);
    }

先看下AppWindowToken的继承体系:

[Android]从源码的角度理解为什么Dialog不能在Application中展示
image.png

回顾一下Activity的启动流程,在Activity的启动过程中,会调用ActivityStack#startActivityLocked方法:

final void startActivityLocked(ActivityRecord r, ActivityRecord focusedTopActivity,
        boolean newTask, boolean keepCurTransition, ActivityOptions options) {
   ....
    TaskRecord task = null;
    if (!newTask) {
        boolean startIt = true;
        for (int taskNdx = mTaskHistory.size() - 1; taskNdx >= 0; --taskNdx) {
            task = mTaskHistory.get(taskNdx);
            if (task.getTopActivity() == null) {
                continue;
            }
            if (task == rTask) {
                if (!startIt) {
                    if (DEBUG_ADD_REMOVE) Slog.i(TAG, "Adding activity " + r + " to task "
                            + task, new RuntimeException("here").fillInStackTrace());
                    // zhangyulong 生成WindowContainer对象
                    r.createWindowContainer();
                    ActivityOptions.abort(options);
                    return;
                }
                break;
            } else if (task.numFullscreen > 0) {
                startIt = false;
            }
        }
    }
    ....
}

在这个方法中会调用ActivityRecord#createWindowContainer方法,在这个方法里会创建AppWindowContainer的实例,进而创建AppWindowContainerController实例,在AppWindowContainerController中会创建AppWindowToken的实例,在创建实例时会调用AppWindowToken的构造方法。从上图AppWindowToken的继承关系图我们可以知道,AppWindowToken的父类是WindowToken,其构造方法如下:

    // com.android.server.wm.WindowToken
    WindowToken(WindowManagerService service, IBinder _token, int type, boolean persistOnEmpty,
            DisplayContent dc, boolean ownerCanManageAppTokens) {
        mService = service;
        token = _token;
        windowType = type;
        mPersistOnEmpty = persistOnEmpty;
        mOwnerCanManageAppTokens = ownerCanManageAppTokens;
        onDisplayChanged(dc);
    }

看下onDisplayChanged方法:

     // com.android.server.wm.onDisplayChanged
    void onDisplayChanged(DisplayContent dc) {
        dc.reParentWindowToken(this);
        ...
    }

DisplayContent#reParentWindowToken会将该WindowToken加入到一个叫做mTokenMap的Map中,其定义为HashMap mTokenMap. 而作为key的IBinder对象,就是我们在Activity启动流程中创建的AppToken对象。也就是说,每个AppToken会对应一个AppWindowToken,简单的示例图如下:

[Android]从源码的角度理解为什么Dialog不能在Application中展示
image.png

3. 问题产生的根本原因

要想探寻这个错误产生的根本原因,我们只需要寻确认在异常产生时,Window的Type和appToken的值是什么即可。

Window的Type是比较好确认的,在创建Dialog的Window时,便生成了其对应的WindowManager.LayoutParams, 在其构造方法中已确定了Dialog对应Window的type:

        public LayoutParams() {
            super(LayoutParams.MATCH_PARENT,  LayoutParams.MATCH_PARENT);
            type = TYPE_APPLICATION;
            format = PixelFormat.OPAQUE;
        }

而appToken的情况则稍显复杂,我们首先要确定这个appToken是从哪里获取的。

先看一下Dialog的构造方法:

Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
    ...
    mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    ...
}

这个WindowManger是我们在show方法中用于执行addView的WindowManger。

那么接下来,我们就要确定在Application中启动Dialog和在Activity中启动Dialog这两种方式WindowManger的处理有什么不同就可以了。

首先看Activity的情况:

还是Dialog的构造方法,这个Context的实际类型是Activity的对象,而Activity重写了getSystemService方法:

    @Override
    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);
    }

因此,在Dialog的构造方法中,我们获取到的实际是Activity中定义的WindowMnager,我们看下这个WindowMnager的赋值,这部分逻辑在Activity#attach方法中:

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) {
    ...

    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();
    ...
}

这个WindowMnager是其对应Window的WindowManager, 而其对应的Window的WindowManager创建时,传入了一个parentWindow:

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

在Dialog的show流程中,因为所使用的WindowManager是含有parentWindow的,因此,在WindowMnagerGlobal#addView流程中,会触发以下逻辑:

 if (parentWindow != null) {
      parentWindow.adjustLayoutParamsForSubWindow(wparams);
}

而因为wparam的type是TYPE_APPLICATION, 因此Window#adjustLayoutParamsForSubWindow会调用如下逻辑:

        if (wp.token == null) {
                wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
            }
            if ((curTitle == null || curTitle.length() == 0)
                    && mAppName != null) {
                wp.setTitle(mAppName);
            }

这个mAppToken就是Activity中的token了。

至于Activity中的token是什么,感兴趣的朋友可以看一下我之前写过的文章
AMS源码分析(一)Activity生命周期管理

分析完了Activity的情况,我们继续看一下Dialog在Application中展示的情况:

还是Dialog的构造方法:

Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
    ...
    mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    ...
}

Application并没有对Contex#getSystemService做重载,因此这里获取到的是context的WindowManager, 而Context的WindowMnager是不包含parentWindow的,因此在最终使用token获取AppWindowToken时会获取到空值,再结合Dialog的Window的Type, 即TYPE_APPLICATION,那么就会触发以下判断:

if (rootType >= FIRST_APPLICATION_WINDOW && rootType 

最终抛出异常。

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:qvyue@qq.com 进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。