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)
大佬 你这把原来没有的问题都给改出来了啊!有点尴尬
@razerdp 感谢大佬认真负责的态度,首先我觉得我们模拟器不一样,我用的Genymotion,不知触发时机是否可能不一样。
我debug看了下 token是为null的
这个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是否正常弹出。