摘要
随着项目新版本发出去,又开始频繁出现PopupWindow$PopupDecorView not attached to window manager
问题,通过firebase
上我们自己上传的日志看到大部分crash
在商品详情页,具体堆栈信息如下:
1 2 3 4 5 6 7 8 9
| Fatal Exception: java.lang.IllegalArgumentException View=android.widget.PopupWindow$PopupDecorView{cc692ff V.E...... ......I. 0,0-0,0} not attached to window manager android.view.WindowManagerGlobal.findViewLocked (WindowManagerGlobal.java:424) android.view.WindowManagerGlobal.updateViewLayout (WindowManagerGlobal.java:336) android.view.WindowManagerImpl.updateViewLayout (WindowManagerImpl.java:93) android.widget.PopupWindow.update (PopupWindow.java:1816) android.widget.PopupWindow$1.onScrollChanged (PopupWindow.java:174) android.view.ViewTreeObserver.dispatchOnScrollChanged (ViewTreeObserver.java:1016) ...
|
分析堆栈
看源码WindowManagerGlobal#findViewLocked()
方法:
1 2 3 4 5 6 7
| 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; }
|
应该是required && index < 0
为true
导致抛出异常,那index < 0
的原因是view
无法在mViews
中找到,我们搜索WindowManagerGlobal
源码,查看一下mViews.add()
方法在哪,很明显在WindowManagerGlobal#addView()
中,所以这个异常应该是PopupWindow
收到onScrollChanged
回调后调用update
方法时,mDecorView
还没有有被add
到刚刚的mViews
集合中。
1.顺着addView
方法查调用
我们在PopupWindow
中搜索addView()
,在PopupWindow#invokePopup()
中找到,调用invokePopup()
的地方分布在showAtLocation()
和showAsDropDown()
的最后一行。
我们在PopupWindow
中搜索mOnScrollChangedListener
,在PopupWindow#attachToAnchor()
中找到,调用时机在update()
和showAsDropDown()
中。
3.结论
根据以上两点,可以大胆猜测应该是调用PopupWindow#showAsDropDown()
出现异常,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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); }
|
在执行完attachToAnchor()
后,执行invokePopup()
前产生异常。如此会导致WindowManagerGlobal#addView()
未执行,onScrollChanged()
却被回调,最终走到WindowManagerGlobal#findViewLocked()
抛出异常。
分析代码
在商品详情页中我们使用了PopupWindow
,但都是使用经过封装的BaseWhiteBgPopupWindow
,我们复写了showAtLocation()
、showAsDropDown()
、dismiss()
几个重要方法,加上了判定activity是否存活的通用方式,并且做了try-catch保护,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| @Override public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) { try { ... if (BasePopupWindowUtils.canHandleSafely(mContext, contentView)) { super.showAsDropDown(anchor, xoff, yoff, gravity); } ... } catch (Throwable e) { ExceptionUtils.uploadCatchedException(e); } } public static boolean canHandleSafely(Context context, View contentView) { if (contentView == null) { return false; } try { final Context mContext = context != null ? context : contentView.getContext(); if (mContext != null) { if (mContext instanceof Lifeful) { Lifeful lifeful = (Lifeful) mContext; if (lifeful.isAlive()) { return true; } } else if (mContext instanceof Activity) { if (ActivityUtils.activityIsAlive(mContext)) { return true; } } else { return true; } } } catch (Exception e) { ExceptionUtils.printExceptionTrace(e); } return false; } public static boolean activityIsAlive(Context currentActivity) { if (currentActivity == null || !(currentActivity instanceof Activity)) { return false; } Activity activity = (Activity) currentActivity; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { return !(activity.isDestroyed() || activity.isFinishing()); } else { return !activity.isFinishing(); } }
|
所以我们通过firebase
查看被catch后上报的日志:
1 2 3 4
| Non-fatal Exception: android.view.WindowManager$BadTokenException Unable to add window -- token null is not valid; is your activity running? android.view.ViewRootImpl.setView (ViewRootImpl.java:884) android.widget.PopupWindow.showAsDropDown (PopupWindow.java:1312)
|
熟悉的BadTokenException
,再次分析堆栈
1.搜索异常信息,寻找来源
搜索代码可知道,异常代码在ViewRootImpl
中,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| // 该变量决定WindowManagerService#addWindow()中的typs=TYPE_APPLICATION=2 final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams(); public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) { synchronized (this) { if (mView == null) { mView = view; } ... 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?"); ... } ... } }
|
追踪res
的来源,在Session
中,
1 2 3 4 5 6 7 8
| @Override public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs, int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets, Rect outOutsets, InputChannel outInputChannel) { // 通过WindowManagerService完成Window的添加 return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId, outContentInsets, outStableInsets, outOutsets, outInputChannel); }
|
继续查看WindowManagerService
代码,列出可能走到的代码部分,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| 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; 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; } } ... WindowToken token = mTokenMap.get(attrs.token); if (token == null) { if (type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) { Slog.w(TAG_WM, "Attempted to add application window with unknown token " + attrs.token + ". Aborting."); return WindowManagerGlobal.ADD_BAD_APP_TOKEN; } ... } ... }
|
前面注释中已经说明了typs=TYPE_APPLICATION=2
,说明走不到if (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW)
的条件,也就是说导致异常的原因是token=null
,因为符合type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW
条件,返回WindowManagerGlobal.ADD_BAD_APP_TOKEN
。
此处attrs.token=null
,所以mTokenMap
中获取到的token=null
。
2.分析attrs.token
为空的原因
回到捕获的异常信息,指向attrs.token=null
,attrs
是调用ViewRootImpl#setView()
时传进来的,此处调用顺序简单描述为WindowManager#addView()->WindowManagerImpl#addView()->WindowManagerGlobal#addView()->ViewRootImpl#setView()
,前面分析过PopupWindow
中的addView()
调用在invokePopup()
中,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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); } }
|
此处传入参数p
即为抛异常处的attrs
,我们看看p
的产生,
1
| final WindowManager.LayoutParams p = createPopupLayoutParams(anchor.getWindowToken());
|
传入的参数正是token
,也就是说anchor.getWindowToken()=null
导致showAsDropDown()
抛出异常被捕获。
3.分析View#getWindowToken()
返回空的原因
1 2 3 4 5 6 7 8
| public IBinder getWindowToken() { return mAttachInfo != null ? mAttachInfo.mWindowToken : null; } void dispatchAttachedToWindow(AttachInfo info, int visibility) { mAttachInfo = info; ... }
|
可以分析应该是dispatchAttachedToWindow()
还未调用,mAttachInfo=null
,导致View#getWindowToken()=null
。
dispatchAttachedToWindow()
的调用是在ViewRootImpl#performTraversals()
中进行DecorView
的dispatchAttachedToWindow()
调用(因为DecorView
没有复写该方法,所以调用的是ViewGroup#dispatchAttachedToWindow()
),在ViewGroup#dispatchAttachedToWindow()
中进行子view
的dispatchAttachedToWindow()
通知。
新版本发布做了什么?
在之前版本,我们是在Activity#onCreate()
中执行页面数据请求,等待数据回来后进行绑定操作,此刻再执行showAsDropDown()
,可能因为请求造成的一点延迟showAsDropDown()
,问题数量比较少没有被重视;
随着页面改版,存在AB两个版本,并且AB的结果从请求数据下发,我们使用Activity
进行页面数据请求,等待数据请求成功后,创建对应的Fragment
承载页面展示,我们在Fragment#onViewCreated()
中调用的showAsDropDown()
,这个改动将大大提高了错误出现的可能。
解决方案
根据之前的分析知道,当View#mAttachInfo != null
时,我们可以直接执行showAsDropDown()
操作;当View#mAttachInfo == null
时,我们就等待dispatchAttachedToWindow()
执行给mAttachInfo
赋值后,再调用showAsDropDown()
。
完美契合以上条件的方式,系统已经提供给我们,那就是View#post()
操作,感兴趣的可以自己研究下源码,但我们也要注意View#post()
在不同版本之间的差异,具体可以查看View.post() 不靠谱的地方你知道吗?这篇文章。