刨根究底之在onCreate()方法里顯示PopupWindow的正確姿勢

可以我們都遇到這樣一個bug,在Activity的onCreate()里調(diào)用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方法里執(zhí)行顯示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調(diào)用addToDisplay()返回碼是WindowManagerGlobal.ADD_BAD_APP_TOKEN。
在看問題之前先看幾個經(jīng)我測試過的結(jié)論:

  1. 同樣是在onCreate()去show,Dialog就不會報錯,而PopupWindow卻會報錯。
  2. 用View的post方法可以showPopupWindow,而用Handler的post卻不行。
    我們一步一步來看吧。
  • 分析原因No.1
    既然res是WindowManagerGlobal.ADD_BAD_APP_TOKEN,有人會問為什么不是WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN?別著急,我會給大家講清楚的。
    我們進(jìn)入到 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(){...}
                ...
            }
            ...
}

這里我僅列出了可能出現(xiàn)的邏輯。先來看是不是WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN。
如果type>=FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW就會返回WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;。這個type是哪里傳過來的呢?其實(shí)這個type就是WindowManager.LayoutParam()生成時默認(rèn)的,沒有其他地方給他賦值,為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,當(dāng)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經(jīng)過我層層查找其實(shí)就是調(diào)用PopupWindow的showAtLocation時傳進(jìn)來的View錨點(diǎn)的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的時候他在調(diào)用WindowManagerGlobal.addView()時會調(diào)用parentWindow. adjustLayoutParamsForSubWindow(wparams)給wparams傳遞mAppToken。首先這個parentWindow就是宿主Activity對應(yīng)的PhoneWindow,而他的mAppToken就是Activity用于進(jìn)程間通信的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)里調(diào)用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);
        }
    }

然后就調(diào)用到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;
            }
        }
    }

于是乎,我們的第一條結(jié)論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) {
            // 如果當(dāng)前View加入到了window中,直接調(diào)用UI線程的Handler發(fā)送消息
            return attachInfo.mHandler.post(action);
        }
        // Assume that post will succeed later
        // View未加入到window,放入ViewRootImpl的RunQueue中
        getRunQueue().post(action);
        return true;
    }

View的post時候分兩種情況,當(dāng)View已經(jīng)attach到window,直接調(diào)用UI線程的Handler發(fā)送runnable。如果View還未attach到window(onCreate里面肯定沒有attach到window的),將runnable放入一個類型為HandlerActionQueue的RunQueue中。當(dāng)下一次performTraversals到來的時候就會把這個RunQueue拿出來執(zhí)行

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。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容