razerdp/BasePopup

Activity onCreate 中showPopupWindow方法无法显示问题

xchengDroid opened this issue · 13 comments

提issue前请去WIKI#常见问题找找相关问题,避免重复提问

提issue前请务必参考以下格式填写,否则该问题优先级将会降低

  • 系统版本:android 6.0
  • 库版本:'com.github.razerdp:BasePopup_Candy:2.2.2.200214'
  • 问题描述/重现步骤:Activity onCreate中调用弹出Pop的方法
  • 问题代码/截图:PopWindow.showPopupWindow();
  • 报错信息:
    02-17 02:55:33.130 20124-20124/com.cosmap.onemap E/BasePopupWindow: onShowError:
    android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?
    at android.view.ViewRootImpl.setView(ViewRootImpl.java:567)
    at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:310)
    at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:85)
    at razerdp.basepopup.WindowManagerProxy.addView(WindowManagerProxy.java:63)
    at android.widget.PopupWindow.invokePopup(PopupWindow.java:1258)
    at android.widget.PopupWindow.showAtLocation(PopupWindow.java:1032)
    at android.widget.PopupWindow.showAtLocation(PopupWindow.java:995)
    at razerdp.basepopup.PopupWindowProxy.showAtLocation(PopupWindowProxy.java:119)
    at razerdp.basepopup.BasePopupWindow.tryToShowPopup(BasePopupWindow.java:757)
    at razerdp.basepopup.BasePopupWindow.showPopupWindow(BasePopupWindow.java:573)
    xxxxx 忽略
    at android.os.Handler.handleCallback(Handler.java:739)
    at android.os.Handler.dispatchMessage(Handler.java:95)
    at android.os.Looper.loop(Looper.java:148)
    at android.app.ActivityThread.main(ActivityThread.java:5417)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)

大佬 你这把原来没有的问题都给改出来了啊!有点尴尬

image

没有你说的问题。

@razerdp 感谢大佬认真负责的态度,首先我觉得我们模拟器不一样,我用的Genymotion,不知触发时机是否可能不一样。
我debug看了下 token是为null的
tokenError2x

这个else 里面decorView.getWindowToken()是否可能为null 我不确定,望大佬看看! 我在9.0的真机上没遇到这个问题

请用真机测试。。。或者官方模拟器。

主要是之前的2.2.1 release版本是好的
我怀疑可能是通过isWindowTokenReady = window.isActive(); 判断不准确导致的

private void checkWindowTokenReady() {
        if (isWindowTokenReady) return;
        Activity act = getContext();
        Window window = act == null ? null : act.getWindow();
        if (window == null) return;
        isWindowTokenReady = window.isActive();
    }

在这个版本之前的版本,做法非常粗暴,就是show不成功,在catch中进行postDelay,尝试三次。如果activity在这个时候destroy了,因为postdelay的原因,还是持有着引用的。

在这次的版本中,重构了这方面的操作,对windowToken进行判断后,如果没准备好,就hook掉window.callback,在windowFocusChange中进行show,如果准备好就直接进行show,这样的处理比之前的处理好了很多很多,而且也是更加的合理。

最后对你的疑问进行解释:
window.isActive()在activity中时序是在onResume()之后,在onPostResume()之中,此时window是准备就绪的,也就是windowToken是存在的,如果这时候非active的话,那也根本无法弹出PopupWindow。

因为手上没有Genymotion,现在上Genymotion官网也没法下载,所以暂时来说我没法去进行测试了,不过在AS的模拟器上,5.0~10.0测试皆为正常,真机上我的测试机是红米k20和小米平板以及华为mate10,测试皆是正常。

如果您还有疑惑,可以尝试对tryToShowPopup()方法进行单步调试,并提供更多的信息。

我看了2.2.1的代码 ,您在最新版的处理意图是取消了Handler.post的方式延迟初始化显示pop的操作(因为您觉得这样不是很严谨),而采用Window.Callback window.setCallback(mWindowCallbackWrapper); 的方式处理! 我发表一下自己的观点,您看是否正确!
1、 window.setCallback 方法只能设置一次,如果此处调用了setCallback 会不会影响开发者调用此方法!你设置setCallback的被覆盖了或者其他开发者被您设置setCallback的覆盖了!
2、onWindowFocusChanged 是否确定和token关联 !官方的方法注释中没有明确的确定关联
3、这是View.getWindowToken()的源码,mWindowToken是从mAttachInfo 里面获取的

 public IBinder getWindowToken() {
        return mAttachInfo != null ? mAttachInfo.mWindowToken : null;
    }

mAttachInfo 又是从哪获取的呢 !查看View源码可以看到是在dispatchAttachedToWindow第一行赋值的,

 void dispatchAttachedToWindow(AttachInfo info, int visibility) {
        mAttachInfo = info;
       忽略其他...
     if (listeners != null && listeners.size() > 0) {         
            for (OnAttachStateChangeListener listener : listeners) {
                listener.onViewAttachedToWindow(this);
            }
        }
}

我们是否可以通过View.addOnAttachStateChangeListener 方法添加监听,在onViewAttachedToWindow方法里处理pop的show操作,而且还可以直接在tryToShowPopup 设置 v 或者decorView 此监听,不需要cacheShow(View v, boolean positionMode) 操作。

 public interface OnAttachStateChangeListener {
        /**
         * Called when the view is attached to a window.
         * @param v The view that was attached
         */
        public void onViewAttachedToWindow(View v);
        /**
         * Called when the view is detached from a window.
         * @param v The view that was detached
         */
        public void onViewDetachedFromWindow(View v);
    }

以上是我的一点小想法!望到大佬采纳

感谢您的关注以及建议哈

首先提出我的结论:我觉得addOnAttachStateChangeListener 值得一试,很不错的建议。

不过值得注意的是,在之前的某个版本中,其实我也尝试过利用decorView.post()进行onCreate()时的弹出,因为view.post()的执行一定是在onAttachToWindow()执行之后,但不知道为什么在模拟器上却无法弹出,没有任何的log,windowToken也是有的,但就是无法弹出(这个问题至今我都没法测出来)。

正是因此,我才放弃了AttachToWindow相关的方式,如今你提起AttachStateChangeListener,我又想起了view.post()在api24前后的变化,毕竟view.post()还真有可能不执行,但attchstatechangelistener应该不会,毕竟都得往WindowManagerGlobal去addView(),所以我觉得可以尝试一下。

下面是回复你提出来的问题:

之所以需要用到window.callback,确实是为了获取focusChange的监听,那么为什么要得到这个监听呢,是因为当view获取焦点的时候,它必定是在视图上有显示,那么此刻windowToken一定是存在的,也证明了activity在onResume之后,其实是变相监听activity#onResume()。

而你所说的覆盖问题,在开发的时候我就留意到setCallback()的覆盖问题了,也因此我的windowCallback是一个wrapper,他只是一个代理(或者说装饰者),我们仅仅处理其中的windowFocus,其余的还是交给原来的callback。

因此,如果开发者/系统在使用basepopup之前就已经设置过callback,我们仍然是正常回调给原来的callback的,这不会影响原来的使用。

但如果是在new popup()之后设置的callback呢,说实话,确实会有可能存在这个问题,会把我们的callbackWrapper给顶掉,具体可能是在binder通信期间,或者说下一个vsync间隔期间给替换了,这里其实我报有了侥幸心理,因为很多开发者都不会使用window.setCallback()这个东东,所以我这里就侥幸使用了,如今看来,不怕一万就怕万一,确实也不是非常的好哈哈哈哈,,这也是我也打算采取attachStateChangeListener的原因。

最后是关于cacheShow()的问题,首先其实我们需要做的是尽可能的避开监听中来显示的问题,因为官方popupwindow/dialog的设计初衷并不兼容onCreate()中弹出这种*操作(因为他是依附于parent window下的)

只是考虑到这种蛋疼的需求非常常见,basepopup才打算去“强行兼容”(其实按照我的思路,这些蛋疼的问题理应由使用者去完成)

回到问题里,因为我们不希望时常进行监听,而只是需要在“必要的时候”进行选择性监听,所以判断tokenIsReady是必须要的。不过如果采取了监听onAttach的话,由于detach的存在,所以好像不能是“必要的时候”进行监听,不过目前来说我也接受这个问题,因为现在的destroy是利用了weakhashmap在单例中进行callback的,这是一个高风险操作,我一直想办法去替代它,现在机会来了!

其次,你说的cacheShow也确实可以放弃,但这可能会造成代码回调,不过应该不影响,予以采纳-V-

最后非常感谢你的支持哈~如果有思路或者好的想法可以多多交流-V-,也非常欢迎你加入我们的群共同探讨一些技术问题哈哈

由于我在网易工作,平时无法用QQ,所以只有一个微信群,如果你可以接受微信群聊的话,可以通过下面二维码加入(已删)

感谢大佬采纳意见
我看了代码 ,感觉不再需要 checkWindowTokenReady()方法了,感觉代码还是有点问题,我们需要依赖的的是v 或者decorView的windowToken, 针对他们独立的设置监听可能更合适点!我看最新的代码是设置decorView的监听!

下面是我fix的代码,能解决我genymotion 6.0不能弹出的问题

public abstract class FixedOnCreatePopWindow extends BasePopupWindow {
    private boolean isAddListener;

    public FixedOnCreatePopWindow(Context context) {
        super(context);
    }

    public FixedOnCreatePopWindow(Context context, int width, int height) {
        super(context, width, height);
    }

    @Override
    void tryToShowPopup(View v, boolean positionMode) {
        if (v == null) {
            //什么都没传递,取顶级view的id
            Activity act = getContext();
            if (act == null) {
                onShowError(new IllegalStateException("无法获取Activity,请确保您的Context是否为Activity"));
                return;
            }
            v = act.findViewById(android.R.id.content);
        }
        if (v.getWindowToken() != null) {
            super.tryToShowPopup(v, positionMode);
            return;
        }
        if (!isAddListener) {
            isAddListener = true;
            v.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
                @Override
                public void onViewAttachedToWindow(View v) {
                    FixedOnCreatePopWindow.super.tryToShowPopup(v, positionMode);
                    isAddListener = false;
                    //防止PopupWindow关闭了,但是View还是活跃状态即展会在屏幕中
                    //但是OnAttachStateChangeListener还一直隐式的引用PopupWindow导致内存溢出!
                    v.removeOnAttachStateChangeListener(this);
                }

                @Override
                public void onViewDetachedFromWindow(View v) {

                }
            });
        }
    }
}

针对windowToken!=null的判断应该放在tryToShowPopup的开始位置,只有windowToken存在的时候再去初始化调用其他代码,这样是不是更合理点

之所以针对decorView,是因为我需要监听activity的生命期。
如果是针对tryToShowPopup()参数中的view

AttachStateChangeListener的onViewDetachedFromWindow能告诉我decorView是否已经removeFromParent,对于activity来说,除了再次setContentView()之外,也就只有finish()的时候会告诉整个viewTree去detachFromWindow()。

在basepopup中,我需要在detach中进行资源的释放以防止内存泄漏,因此,AttachStateChangeListener是需要贯穿整个activity的生命期的。

至于checkWindowTokenReady(),确实是可以不要的,不过当时写这个的时候是为了预防一种情况:
在弹出前没有windoToken,但是在attach之前再次showPopup,此时有windowToken,但是attachToWindow还没回调到我们的popup,此时就可能会弹出2个popup了。

不过上面说的情况理论上其实不会出现的,因为都在主线程,,不过毕竟写库嘛,需要考虑多一点,所以就保留了。

暂时抛掉这个,新的candy版在昨晚就发布了,你看看新的candy是否正常弹出。