常见的PopupWindow Crash:xxx not attached to window manager 问题分析到修复之路

摘要

随着项目新版本发出去,又开始频繁出现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 < 0true导致抛出异常,那index < 0的原因是view无法在mViews中找到,我们搜索WindowManagerGlobal源码,查看一下mViews.add()方法在哪,很明显在WindowManagerGlobal#addView()中,所以这个异常应该是PopupWindow收到onScrollChanged回调后调用update方法时,mDecorView还没有有被add到刚刚的mViews集合中。

1.顺着addView方法查调用

我们在PopupWindow中搜索addView(),在PopupWindow#invokePopup()中找到,调用invokePopup()的地方分布在showAtLocation()showAsDropDown()的最后一行。

2.顺着mOnScrollChangedListener查添加时机

我们在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=nullattrs是调用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()中进行DecorViewdispatchAttachedToWindow()调用(因为DecorView没有复写该方法,所以调用的是ViewGroup#dispatchAttachedToWindow()),在ViewGroup#dispatchAttachedToWindow()中进行子viewdispatchAttachedToWindow()通知。

新版本发布做了什么?

在之前版本,我们是在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() 不靠谱的地方你知道吗?这篇文章。