美文网首页Android开发Android开发经验谈Android进阶之路
刨根究底之在onCreate()方法里显示PopupWindow

刨根究底之在onCreate()方法里显示PopupWindow

作者: 十蛋stan | 来源:发表于2018-11-09 11:16 被阅读19次

    可以我们都遇到这样一个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。

    相关文章

      网友评论

      本文标题:刨根究底之在onCreate()方法里显示PopupWindow

      本文链接:https://www.haomeiwen.com/subject/fvtvxqtx.html