[未完待续] 跳出 PopupWindow 使用过程中的坑

跳出 PopupWindow 使用过程中的坑

遇到异常 Exception

在做页面首次使用引导需求时使用 PopupWindow 来实现,没想到测试过程中一直遇到这两种异常而崩溃:

  • android.view.WindowManager$BadTokenException
    Unable to add window – token android.os.BinderProxy@74dec62 is not valid; is your activity running?

  • java.lang.IllegalArgumentException
    View=android.widget.PopupWindow$PopupDecorView{78549e8 V.E…… ……I. 0,0-0,0} not attached to window manager

这种崩溃让人百思不得其解,因为 PopupWindow 使用还是比较简单的,出错代码如下:

public class TestActivity extends Activity {

    private TextView mTextView;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTextView = (TextView) findViewById(R.id.text_view);

        loadApiFromNet(new Callback() {     // 服务器接口调用
            @Override
            public void onSuccess(String str) {
                // 其他处理忽略
                showPopup();
            }
        });
    }

    private void showPopup() {
        View popupView = LayoutInflater.from(this).inflate(R.layout.popup, null);
        ((TextView) popupView.findViewById(R.id.tv_text)).setText("测试测试");
        PopupWindow popup = new PopupWindow(popupView, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, true);
        popup.setTouchable(false);
        if (null != mTextView) {
            popup.showAsDropDown(mTextView);
        }
    }
}

解决方案

原因是 showAsDropDown() 方法中以来的 View 类型实例还没有依附到绘制 Window 中,因此将 PopupWindow 的显示逻辑延时即可解决问题:

if (null != view.getWindowToken() ) {
            popupWindow.showAsDropDown(view, 0, 0);
        } else {
            view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
                @Override
                public void onViewAttachedToWindow(View v) {
                    if (null != v.getWindowToken()) {
                        popupWindow.showAsDropDown(v, 0, 0);
                        v.removeOnAttachStateChangeListener(this);
                    }
                }

                @Override
                public void onViewDetachedFromWindow(View v) {

                }
            });
        }

寻找真相

问题解决了,但是这就结束了?
当然不行
知其然,更要知其所以然。怎么能放过这么个了解 Android 的机会呢?
那就让我们从源码的角度出发,探究造成异常的根源吧!

PopupWindow 的调用栈顺序为:

/**
     * Displays the content view in a popup window anchored to the corner of
     * another view. The window is positioned according to the specified
     * gravity and offset by the specified x and y coordinates.
     * 

* If there is not enough room on screen to show the popup in its entirety, * this method tries to find a parent scroll view to scroll. If no parent * view can be scrolled, the specified vertical gravity will be ignored and * the popup will anchor itself such that it is visible. *

* If the view later scrolls to move anchor to a different * location, the popup will be moved correspondingly. * * @param anchor the view on which to pin the popup window * @param xoff A horizontal offset from the anchor in pixels * @param yoff A vertical offset from the anchor in pixels * @param gravity Alignment of the popup relative to the anchor * * @see #dismiss() */ public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) { if (isShowing() || mContentView == null) { return; } TransitionManager.endTransitions(mDecorView); attachToAnchor(anchor, xoff, yoff, gravity); mIsShowing = true; mIsDropdown = true; final WindowManager.LayoutParams p = createPopupLayoutParams(anchor.getWindowToken()); preparePopup(p); final boolean aboveAnchor = findDropDownPosition(anchor, p, xoff, yoff, p.width, p.height, gravity); updateAboveAnchor(aboveAnchor); p.accessibilityIdOfAnchor = (anchor != null) ? anchor.getAccessibilityViewId() : -1; invokePopup(p); }

没有看到 Popup 的显示流程,继续深入到 invokePopup() 方法中:

/**
     * 

Invoke the popup window by adding the content view to the window * manager.

* *

The content view must be non-null when this method is invoked.

* * @param p the layout parameters of the popup's content view */
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); } }

很显然,在 invokePopup() 方法中找到了操作 View 的相关代码 mWindowManager.addView(decorView, p)

从 PopupWindow 代码中容易发现,mWindowManager 是一个 WindowManger 类型变量,在构造方法中赋值:

public PopupWindow(View contentView, int width, int height, boolean focusable) {
        if (contentView != null) {
            mContext = contentView.getContext();
            mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
        }

        setContentView(contentView);
        setWidth(width);
        setHeight(height);
        setFocusable(focusable);
    }

通过查看源码,最终在 SystemServiceRegistry 类的静态块中找到了 mWindowManager 的具体实例类型为 WindowManagerImpl

Context.getSystemService(String)  
->
ContextImpl.getSystemService(String)
->
SystemServiceRegistry.getSystemService(ContextImpl, String)
->
final class SystemServiceRegistry {
    static {
        // 省略。。。
        registerService(Context.WINDOW_SERVICE, WindowManager.class,
                    new CachedServiceFetcher() {
                @Override
                public WindowManager createService(ContextImpl ctx) {
                    return new WindowManagerImpl(ctx);
                }});
        // 省略。。。
    }
}

因此, mWindowManager.addView() 方法为

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

看到,这里面有使用了一层代理 WindowManagerGlobal,不过胜利就在眼前了,继续深入 WindowManagerGlobal.addView() 方法 :

public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        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");
        }

        // 省略

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

            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.
            synchronized (mLock) {
                final int index = findViewLocked(view, false);
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
            }
            throw e;    //关注这里
        }
    }

终于,找到了抛出异常的代码块了,离成功还差一小点了。继续研究 root.setView() 方法,找到了 ViewRootImpt.java:

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            if (mView == null) {
                mView = view;

                mAttachInfo.mDisplayState = mDisplay.getState();
                mDisplayManager.registerDisplayListener(mDisplayListener, mHandler);

                mViewLayoutDirectionInitial = mView.getRawLayoutDirection();
                mFallbackEventHandler.setView(view);
                mWindowAttributes.copyFrom(attrs);
                if (mWindowAttributes.packageName == null) {
                    mWindowAttributes.packageName = mBasePackageName;
                }
                attrs = mWindowAttributes;

                // ** 省略 **

                try {
                    mOrigWindowType = mWindowAttributes.type;
                    mAttachInfo.mRecomputeGlobalAttributes = true;
                    collectViewAttributes();
                    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(),
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mInputChannel);
                } catch (RemoteException e) {
                    mAdded = false;
                    mView = null;
                    mAttachInfo.mRootView = null;
                    mInputChannel = null;
                    mFallbackEventHandler.setView(null);
                    unscheduleTraversals();
                    setAccessibilityFocus(null, null);
                    throw new RuntimeException("Adding window failed", e);
                } finally {
                    if (restore) {
                        attrs.restore();
                    }
                }

                // ** 省略 **

                if (res < WindowManagerGlobal.ADD_OKAY) {
                    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");
                        case WindowManagerGlobal.ADD_DUPLICATE_ADD:
                            throw new WindowManager.BadTokenException(
                                    "Unable to add window -- window " + mWindow
                                    + " has already been added");
                        case WindowManagerGlobal.ADD_STARTING_NOT_NEEDED:
                            // Silently ignore -- we would have just removed it
                            // right away, anyway.
                            return;
                        case WindowManagerGlobal.ADD_MULTIPLE_SINGLETON:
                            throw new WindowManager.BadTokenException("Unable to add window "
                                    + mWindow + " -- another window of type "
                                    + mWindowAttributes.type + " already exists");
                        case WindowManagerGlobal.ADD_PERMISSION_DENIED:
                            throw new WindowManager.BadTokenException("Unable to add window "
                                    + mWindow + " -- permission denied for window type "
                                    + mWindowAttributes.type);
                        case WindowManagerGlobal.ADD_INVALID_DISPLAY:
                            throw new WindowManager.InvalidDisplayException("Unable to add window "
                                    + mWindow + " -- the specified display can not be found");
                        case WindowManagerGlobal.ADD_INVALID_TYPE:
                            throw new WindowManager.InvalidDisplayException("Unable to add window "
                                    + mWindow + " -- the specified window type "
                                    + mWindowAttributes.type + " is not valid");
                    }
                    throw new RuntimeException(
                            "Unable to add window -- unknown error code " + res);
                }
                // ** 省略 **
            }
        }
    }

这里发现了可能会抛出之前 BadToken 的异常,显然和

mWindowSession.addToDisplay()

方法相关。

在 WindowManagerGlobal 类中,发现通过 AIDL 最终调用到了

WindowManagerService.addWindow()

方法。AIDL 可以参考 Google 官方文档,查看 Android 源码可以借助AndroidXRef.com
触发了其中的一段校验导致了问题发生:

if (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW) {
            attachedWindow = windowForClientLocked(null, attrs.token, false);
            if (attachedWindow == 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 (attachedWindow.mAttrs.type >= FIRST_SUB_WINDOW
                    && attachedWindow.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;
            }
        }

在 windowForClientLocked() 方法中最终找到了出错原因:

final WindowState windowForClientLocked(Session session, IBinder client, boolean throwOnError) {
        WindowState win = mWindowMap.get(client);
        if (localLOGV) {
            Slog.v(
                    TAG_WM, "Looking up client " + client + ": " + win);
        }
        if (win == null) {
            RuntimeException ex = new IllegalArgumentException(
                    "Requested window " + client + " does not exist");
            if (throwOnError) {
                throw ex;
            }
            Slog.w(TAG_WM, "Failed looking up window", ex);
            return null;
        }
        if (session != null && win.mSession != session) {
            RuntimeException ex = new IllegalArgumentException(
                    "Requested window " + client + " is in session " +
                            win.mSession + ", not " + session);
            if (throwOnError) {
                throw ex;
            }
            Slog.w(TAG_WM, "Failed looking up window", ex);
            return null;
        }

        return win;
    }

可见异常的直接原因就是 IBinder 实例为 null。通过上述流程,在 PopupWIndow 中找到 IBinder 实例为

showAsDropDown(View anchor, int xoff, int yoff, int gravity)

方法中 anchor.getWindowToken() 的返回值。

// 未完待续

先看 findViewLocked() 方法

private int findViewLocked(View view, boolean required) {
        final int index = mViews.indexOf(view);
        if (required && index < 0) {
            throw new IllegalArgumentException("View=" + view + " not attached to window manager");
        }
        return index;
    }

你可能感兴趣的:(Android,popupwindow,Exception)