刨根究底之在onCreate()方法里显示PopupWindow的正确姿势

可以我们都遇到这样一个bug,在Activity的onCreate()里调用PopupWindow的showAsDropDown或showAtLocation就会报异常

java.lang.RuntimeException: Unable to start activity ComponentInfo{com.anysoft.tyyd/com.anysoft.tyyd.activities.PlayerControlActivity}: android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?

解决方案就是找一个View去post一个Runnable,或者把显示popupwindow的逻辑放在onWindowFocusChanged()方法里。

在Runnable的run方法里执行显示PopupWindow的逻辑伪代码:
Activity
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
        mView.post(new Runnable{ @Override public void run(){ showPopupWindow() }})
    }

下面就从源码的角度分析这个bug。
这段异常的源码在ViewRootImpl里面:

ViewRootImpl
  public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            if (mView == null) {
            ...
            int res;
            res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(),
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mInputChannel);
            ...
            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?");
            ...
            }
        }
  }

原因便是在ViewRootImpl的setView时用过Session调用addToDisplay()返回码是WindowManagerGlobal.ADD_BAD_APP_TOKEN。
在看问题之前先看几个经我测试过的结论:

  1. 同样是在onCreate()去show,Dialog就不会报错,而PopupWindow却会报错。
  2. 用View的post方法可以showPopupWindow,而用Handler的post却不行。
    我们一步一步来看吧。
  • 分析原因No.1
    既然res是WindowManagerGlobal.ADD_BAD_APP_TOKEN,有人会问为什么不是WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN?别着急,我会给大家讲清楚的。
    我们进入到 mWindowSession.addToDisplay()
Session:
    @Override
    public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
            int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets,
            Rect outOutsets, InputChannel outInputChannel) {
        return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
                outContentInsets, outStableInsets, outOutsets, outInputChannel);
    }

这里的mService就是WindowManagerService。这里return了mService.addWindow()

    public int addWindow(Session session, IWindow client, int seq,
            WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
            Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
            InputChannel outInputChannel) {
            ...
            final int type = attrs.type;
            //tag1 tag1 tag1
            if (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW) {
                parentWindow = windowForClientLocked(null, attrs.token, false);
                if (parentWindow == null) {
                    Slog.w(TAG_WM, "Attempted to add window with token that is not a window: "
                          + attrs.token + ".  Aborting.");
                    return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
                }
                if (parentWindow.mAttrs.type >= FIRST_SUB_WINDOW
                        && parentWindow.mAttrs.type <= LAST_SUB_WINDOW) {
                    Slog.w(TAG_WM, "Attempted to add window with token that is a sub-window: "
                            + attrs.token + ".  Aborting.");
                    return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
                }
            }
            ...
            final int rootType = hasParent ? parentWindow.mAttrs.type : type;
            if (token == null) {
                if (rootType >= FIRST_APPLICATION_WINDOW && rootType <= LAST_APPLICATION_WINDOW) {
                    Slog.w(TAG_WM, "Attempted to add application window with unknown token "
                          + attrs.token + ".  Aborting.");
                    return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
                } else if(){...}
                ...
            }
            ...
}

这里我仅列出了可能出现的逻辑。先来看是不是WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN。
如果type>=FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW就会返回WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;。这个type是哪里传过来的呢?其实这个type就是WindowManager.LayoutParam()生成时默认的,没有其他地方给他赋值,为WindowManager.LayoutParam.TYPE_APPLICATION。

WindowManager:
      public static class LayoutParams extends ViewGroup.LayoutParams implements Parcelable {
          ...
          public LayoutParams() {
            super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
            type = TYPE_APPLICATION;//值为2
            format = PixelFormat.OPAQUE;
          }
          ...
      }

TYPE_APPLICATION的值为2而FIRST_SUB_WINDOW为1000,所以就不会返回WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN了。
也就是说在addWindow()方法中返回的只可能是WindowManagerGlobal.ADD_BAD_APP_TOKEN了。那么我们来看,这里的rootType就是原来的type,当token是null时他就肯定返回WindowManagerGlobal.ADD_BAD_APP_TOKEN了。
这个token是什么呢?

WindowToken token = displayContent.getWindowToken(
                    hasParent ? parentWindow.mAttrs.token : attrs.token);
再来看
DisplayContent:
    WindowToken getWindowToken(IBinder binder) {
        return mTokenMap.get(binder);
    }

这里的mToken经过我层层查找其实就是调用PopupWindow的showAtLocation时传进来的View锚点的getWindowToken()

PopupWindow:
    public void showAtLocation(View parent, int gravity, int x, int y) {
        mParentRootView = new WeakReference<>(parent.getRootView());
        showAtLocation(parent.getWindowToken(), gravity, x, y);
    }

    public void showAtLocation(IBinder token, int gravity, int x, int y) {
        if (isShowing() || mContentView == null) {
            return;
        }

        TransitionManager.endTransitions(mDecorView);

        detachFromAnchor();

        mIsShowing = true;
        mIsDropdown = false;
        mGravity = gravity;

        final WindowManager.LayoutParams p = createPopupLayoutParams(token);
        preparePopup(p);

        p.x = x;
        p.y = y;

        invokePopup(p);
    }

我们知道在Activity onCreate()的时候,这时候的View都是没有灵魂的View,他们没有根(ViewRootImpl)。这个时候View.getWindowToken()一定是null的所以会报错,而Dialog show的时候他在调用WindowManagerGlobal.addView()时会调用parentWindow. adjustLayoutParamsForSubWindow(wparams)给wparams传递mAppToken。首先这个parentWindow就是宿主Activity对应的PhoneWindow,而他的mAppToken就是Activity用于进程间通信的IBinder。而popupWindow他的parentWindow取的是View的getWindowToken()是null,所以就不会adjustLayoutParamsForSubWindow了,他的token依旧是null。

WindowManagerGlobal:
   public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        ...
        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
        if (parentWindow != null) {
            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;
            }
        }
        ...
    }

Window:
    void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
        ...
        } else {
            if (wp.token == null) {
                wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
            }
            if ((curTitle == null || curTitle.length() == 0)
                    && mAppName != null) {
                wp.setTitle(mAppName);
            }
        }
        ...
    }

    

首先通过createPopupLayoutParams(token)把token传给p,再在invokePopup(p)里调用WindowManager.addView()

    private void invokePopup(WindowManager.LayoutParams p) {
        if (mContext != null) {
            p.packageName = mContext.getPackageName();
        }

        final PopupDecorView decorView = mDecorView;
        decorView.setFitsSystemWindows(mLayoutInsetDecor);

        setLayoutDirectionFromAnchor();

        mWindowManager.addView(decorView, p);

        if (mEnterTransition != null) {
            decorView.requestEnterTransition(mEnterTransition);
        }
    }

然后就调用到WindowManagerGlobal的addView()

WindowManagerImpl:
    @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }

WindowManagerGlobal:
    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        ...
        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {
            ...
            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 {
                root.setView(view, wparams, panelParentView);
            } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throw e;
            }
        }
    }

于是乎,我们的第一条结论Activity onCreate()里可以showDialog不可以show PopupWindow的原因就是这样的。

  • 分析原因No.2
    为什么View的post可以show PopupWindow 而Handler的post不行呢?
    先来看View.post源码
    public boolean post(Runnable action) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            // 如果当前View加入到了window中,直接调用UI线程的Handler发送消息
            return attachInfo.mHandler.post(action);
        }
        // Assume that post will succeed later
        // View未加入到window,放入ViewRootImpl的RunQueue中
        getRunQueue().post(action);
        return true;
    }

View的post时候分两种情况,当View已经attach到window,直接调用UI线程的Handler发送runnable。如果View还未attach到window(onCreate里面肯定没有attach到window的),将runnable放入一个类型为HandlerActionQueue的RunQueue中。当下一次performTraversals到来的时候就会把这个RunQueue拿出来执行

ViewRootImpl
    private void performTraversals() {
        ...
        // Execute enqueued actions on every traversal in case a detached view enqueued an action
        getRunQueue().executeActions(mAttachInfo.mHandler);
        ...
    }

这就是为什么用View的post而不用Handler的post。

本篇源码使用api-27。

    原文作者:十蛋stan
    原文地址: https://www.jianshu.com/p/0e4c58b0e012
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞