PopupWindow 的 BadTokenException

[toc]

PopupWindow 的 BadTokenException

2.0.0 版本出现这个崩溃

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Fatal Exception: android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?
at android.view.ViewRootImpl.setView(ViewRootImpl.java:890)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:337)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:109)
at android.widget.PopupWindow.invokePopup(PopupWindow.java:1333)
at android.widget.PopupWindow.showAsDropDown(PopupWindow.java:1156)
at androidx.core.widget.PopupWindowCompat.showAsDropDown(PopupWindowCompat.java:69)
at com.dianyun.pcgo.gift.api.RelativePopupWindow.showOnAnchor(RelativePopupWindow.java:196)
at com.dianyun.pcgo.gift.api.RelativePopupWindow.showOnAnchor(RelativePopupWindow.java:133)
at com.dianyun.pcgo.gift.api.RelativePopupWindow.showOnAnchor(RelativePopupWindow.java:108)
at com.dianyun.pcgo.home.explore.discover.ui.HomeExploreTopRightView.showReceiveGiftTips(HomeExploreTopRightView.java:112)
at com.dianyun.pcgo.home.explore.discover.ui.HomeExploreTopRightView.access$showReceiveGiftTips(HomeExploreTopRightView.java:42)
at com.dianyun.pcgo.home.explore.discover.ui.HomeExploreTopRightView$startObserver$$inlined$apply$lambda$1.onChanged(HomeExploreTopRightView.java:81)
at com.dianyun.pcgo.home.explore.discover.ui.HomeExploreTopRightView$startObserver$$inlined$apply$lambda$1.onChanged(HomeExploreTopRightView.java:42)
at androidx.lifecycle.LiveData.considerNotify(LiveData.java:133)
at androidx.lifecycle.LiveData.dispatchingValue(LiveData.java:151)
at androidx.lifecycle.LiveData.setValue(LiveData.java:309)
at androidx.lifecycle.MutableLiveData.setValue(MutableLiveData.java:50)
at com.dianyun.pcgo.home.explore.HomeExploreMainViewModel$queryGiftObtainStatus$1.invokeSuspend(HomeExploreMainViewModel.java:91)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(BaseContinuationImpl.java:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.java:106)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:148)

1 初步分析

原始的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private fun showReceiveGiftTips(tips: String) {
L.info(TAG, "showReceiveGiftTips tips $tips")
val activity = getContextToActivity()
if ((tips.isNotEmpty() && mHomeReceiveGiftTipsPopupWindow == null
&& activity?.isFinishing == false && !activity.isDestroyed) && mVisibleToUser) {
mHomeReceiveGiftTipsPopupWindow = HomeReceiveGiftTipsPopupWindow(context!!)
mHomeReceiveGiftTipsPopupWindow!!.setData(tips)
mHomeReceiveGiftTipsPopupWindow!!.showOnAnchor(
fl_gift,
RelativePopupWindow.VerticalPosition.BELOW,
RelativePopupWindow.HorizontalPosition.CENTER
)
}
}

我们现在找到抛出异常的地方

ViewRootImpl#setView

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
51
52
53
54
55
56
57
58
59
60
61
62
 /**
* We have one child
*/
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
mAttachInfo.mDisplayState = mDisplay.getState();
mDisplayManager.registerDisplayListener(mDisplayListener, mHandler);

mViewLayoutDirectionInitial = mView.getRawLayoutDirection();
mFallbackEventHandler.setView(view);
mWindowAttributes.copyFrom(attrs);
if (mWindowAttributes.packageName == null) {
mWindowAttributes.packageName = mBasePackageName;
}
attrs = mWindowAttributes;


requestLayout(); <--注意这里

int res; /* = WindowManagerImpl.ADD_OKAY; */

...

try {
mOrigWindowType = mWindowAttributes.type;
mAttachInfo.mRecomputeGlobalAttributes = true;
collectViewAttributes();
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
mTempInsets);
setFrame(mTmpFrame);
} catch (RemoteException e) {
..
}


if (res < WindowManagerGlobal.ADD_OKAY) {
mAttachInfo.mRootView = null;
mAdded = false;
mFallbackEventHandler.setView(null);
unscheduleTraversals();
setAccessibilityFocus(null, null);
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?");

...

...

}

}

}

2 初步解决

看到这个问题,一般会认为是 Activity 已经 finished 或者应 destroyed 了,所以我们在前面加判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

private fun showReceiveGiftTips(tips: String?) {
L.info(TAG, "showReceiveGiftTips tips $tips")
if (!mVisibleToUser) {
return
}
if (tips.isNullOrEmpty()) {
return
}
if (mHomeReceiveGiftTipsPopupWindow != null) {
return
}
val activity = getContextToActivity() // 这里加了 activity 的判断
if (ActivityUtils.activityIsDestroyed(activity)) {
return
}
mHomeReceiveGiftTipsPopupWindow = HomeReceiveGiftTipsPopupWindow(context!!)
mHomeReceiveGiftTipsPopupWindow!!.setData(tips)
mHomeReceiveGiftTipsPopupWindow!!.showOnAnchor(
fl_gift,
RelativePopupWindow.VerticalPosition.BELOW,
RelativePopupWindow.HorizontalPosition.CENTER
)
}

我们开心的修复,在 2.0.1 跟了出去

然而,还是有问题

3 分析过程

第一次解决尝试失败,我们在重新审视这个崩溃。

下面是平时我们遇到的

1
2
3
4
5
6
7
8
9
android.view.WindowManager$BadTokenException: Unable to add window — token android.os.BinderProxy@447a6748 is not valid; is your activity running?
at android.view.ViewRoot.setView(ViewRoot.java:468)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:177)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:91)
at android.view.Window$LocalWindowManager.addView(Window.java:424)
at android.app.Dialog.show(Dialog.java:239)
at android.app.Activity.showDialog(Activity.java:2488)

at android.os.Handler.dispatchMessage(Handler.java:99)

结合抛出的异常信息,我们发现 chikii 抛出的这个 BadTokenException token 的值为空。它和我们平时遇到的 BadTokenException 有点不一样.

1
WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?

这个 token 是 null 就是突破口

我们先看看这个 token 是从哪里来的

3.1 PopupWindow#show 的流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@startuml

RelativePopupWindow -> RelativePopupWindow: showOnAnchor(anchor)
RelativePopupWindow -> PopupWindow: showAtLocation(parent) ①

PopupWindow -> PopupWindow: showAtLocation(token) ②

PopupWindow -> PopupWindow: createPopupLayoutParams(token): WindowManager.LayoutParams③

PopupWindow -> PopupWindow: invokePopup(WindowManager.LayoutParams) ④

PopupWindow -> WindowManagerImpl:addView(View, iewGroup.LayoutParams)

WindowManagerImpl -> WindowManagerGlobal:addView(View,ViewGroup.LayoutParams ...) ⑤

WindowManagerGlobal -> ViewRootImpl:setView(view,WindowManager.LayoutParams,panelParentView) ⑥

@enduml

说明:

PopupWindow#showAtLocation
在这个方法拿到 anchor 锚点 View 的 windowToken,然后一路传着下去
仍然会

1
2
3
4
public void showAtLocation(View parent, int gravity, int x, int y) {
mParentRootView = new WeakReference<>(parent.getRootView());
showAtLocation(parent.getWindowToken(), gravity, x, y);
}

②③
创建 WindowManager.LayoutParams

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
// PopupWindow#showAtLocation
public void showAtLocation(IBinder token, int gravity, int x, int y) {

...

final WindowManager.LayoutParams p = createPopupLayoutParams(token);
preparePopup(p);

...

invokePopup(p);
}


protected final WindowManager.LayoutParams createPopupLayoutParams(IBinder token) {
final WindowManager.LayoutParams p = new WindowManager.LayoutParams();

...

p.token = token;

...

return p;
}


PopupWindow#invokePopup
将 DecorView 添加到 WindowManager 中

1
2
3
4
5
6
7
8
9
10
11
12
13

private void invokePopup(WindowManager.LayoutParams p) {

...

final PopupDecorView decorView = mDecorView;

...

mWindowManager.addView(decorView, p);

...
}


WindowManagerGlobal#addView 里面创建 ViewRootImpl 并调用 ViewRootImpl 的 addView 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// WindowManagerGlobal#addView 
public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow) {

....

ViewRootImpl root;

root = new ViewRootImpl(view.getContext(), display);

view.setLayoutParams(wparams);

...

root.setView(view, wparams, panelParentView);

...

}


调用 WindowSession.addToDisplay 显示 PopuWindow

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
//ViewRootImpl#setView
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;

...

attrs = mWindowAttributes;

...

requestLayout();

...

try {

...
// 调用 WindowSession.addToDisplay 显示 PopupWindow
res = mWindowSession.addToDisplay(...);

} catch (RemoteException e) {

...

}

现在整个调用的 链路都非常清楚了,调用 HomeReceiveGiftTipsPopupWindow 的 锚点 View 的 token 为空,即
fl_gift 的token 为空

1
2
3
4
5
6
7
mHomeReceiveGiftTipsPopupWindow = HomeReceiveGiftTipsPopupWindow(context!!)
mHomeReceiveGiftTipsPopupWindow!!.setData(tips)
mHomeReceiveGiftTipsPopupWindow!!.showOnAnchor(
fl_gift, <---- 这个 View 的 token 为空
RelativePopupWindow.VerticalPosition.BELOW,
RelativePopupWindow.HorizontalPosition.CENTER
)

3.2 View 的 token 来源

1
2
3
4
// View# getWindowToken
public IBinder getWindowToken() {
return mAttachInfo != null ? mAttachInfo.mWindowToken : null;
}

View 的 Token 是从 mAttachInfo 拿的,因此要知道 mAttachInfo 是怎么来的

3.2.1 View$AttachInfo

AttachInfo 是 View 的一个内部类,里面包含信息主要有

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
// View$AttachInfo.java

/**
* A set of information given to a view when it is attached to its parent
* window.
*/
final static class AttachInfo {


final IWindowSession mSession;


final IWindow mWindow;

final IBinder mWindowToken; // 这个就是要拿的 token

Display mDisplay;

final Callbacks mRootCallbacks;

IWindowId mIWindowId;
WindowId mWindowId;

/**
* The top view of the hierarchy.
*/
View mRootView;

IBinder mPanelParentWindowToken;

boolean mHardwareAccelerated;
boolean mHardwareAccelerationRequested;
ThreadedRenderer mThreadedRenderer;
List<RenderNode> mPendingAnimatingRenderNodes;

....

}

AttachInfo 的创建在 ViewRootImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ViewRootImpl.java

public ViewRootImpl(Context context, Display display) {

...

mWindowSession = WindowManagerGlobal.getWindowSession();

mWindow = new W(this);

// 这里创建 mAttachInfo
mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
context);

...

}

3.2.2 AttachInfo 传递给 View 的过程

1
2
3
4
5
6
7
8
ViewRootImpl#setView
-> ViewRootImpl#requestLayout
-> ViewRootImpl#scheduleTraversals
-> Choreographer#postCallback
-> TraversalRunnable#run
-> ViewRootImpl#doTraversal
-> ViewRootImpl#performTraversals
-> View#dispatchAttachedToWindow(mAttachInfo, 0); // 这里创建好的 AttachInfo 赋值给 View

View#dispatchAttachedToWindow

1
2
3
4
5
6
7

void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;

...

}

可以看到 View 中的 token 是要经过 View#dispatchAttachedToWindow 方法才能有值的,这要求我们在显示 PopupWindow 的时候,要选择相应的时机。

4 最终解决

通过上面的 PopupWindow 的 show 流程和 View 的token 赋值流程的分析,
在去看看 为啥我们在第一步加了对 Activity 状态的判断还出现崩溃。

主要是因为锚点 View 的 token 为空导致的。

导致 token 为空,也是因为调用 show 的时机不对。

解决办法是在调用 PopupWindow 的 show 之前先判断一下锚点 View 的 applicationWindowToken ,并且调用的时机也要调整一下,不能在 View 一创建的时候就调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun showReceiveGiftTips(tips: String?) {
...

if (giftIcon.applicationWindowToken == null) { // 一 token 判断
L.error(TAG, "showReceiveGiftTips giftIcon.applicationWindowToken return")
return
}
val activity = getContextToActivity() // 二 activity 的判断
if (ActivityUtils.activityIsDestroyed(activity)) {
return
}
mHomeReceiveGiftTipsPopupWindow = HomeReceiveGiftTipsPopupWindow(context!!)
mHomeReceiveGiftTipsPopupWindow!!.setData(tips)
mHomeReceiveGiftTipsPopupWindow!!.showOnAnchor(fl_gift, ...
)
}

1. 后续在使用 PopupWindow 的时候要注意调用 show 的时机
2. show 之前最好判断一下锚点 View 的 token 和 Activity 的状态

yxhuang wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客