刨根究底之在onCreate()方法里显示PopupWindow
可以我们都遇到这样一个bug,在Activity的onCreate()里调用PopupWindow的showAsDropDown或showAtLocation就会报异常
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.anysoft.tyyd/com.anysoft.tyyd.activities.PlayerControlActivity}: android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?
解决方案就是找一个View去post一个Runnable,或者把显示popupwindow的逻辑放在onWindowFocusChanged()方法里。
在Runnable的run方法里执行显示PopupWindow的逻辑伪代码:
Activity
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
mView.post(new Runnable{ @Override public void run(){ showPopupWindow() }})
}
下面就从源码的角度分析这个bug。
这段异常的源码在ViewRootImpl里面:
ViewRootImpl
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
...
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?");
...
}
}
}
原因便是在ViewRootImpl的setView时用过Session调用addToDisplay()返回码是WindowManagerGlobal.ADD_BAD_APP_TOKEN。
在看问题之前先看几个经我测试过的结论:
- 同样是在onCreate()去show,Dialog就不会报错,而PopupWindow却会报错。
- 用View的post方法可以showPopupWindow,而用Handler的post却不行。
我们一步一步来看吧。
- 分析原因No.1
既然res是WindowManagerGlobal.ADD_BAD_APP_TOKEN,有人会问为什么不是WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN?别着急,我会给大家讲清楚的。
我们进入到 mWindowSession.addToDisplay()
Session:
@Override
public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets,
Rect outOutsets, InputChannel outInputChannel) {
return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
outContentInsets, outStableInsets, outOutsets, outInputChannel);
}
这里的mService就是WindowManagerService。这里return了mService.addWindow()
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;
//tag1 tag1 tag1
if (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW) {
parentWindow = windowForClientLocked(null, attrs.token, false);
if (parentWindow == 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 (parentWindow.mAttrs.type >= FIRST_SUB_WINDOW
&& parentWindow.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;
}
}
...
final int rootType = hasParent ? parentWindow.mAttrs.type : type;
if (token == null) {
if (rootType >= FIRST_APPLICATION_WINDOW && rootType <= LAST_APPLICATION_WINDOW) {
Slog.w(TAG_WM, "Attempted to add application window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
} else if(){...}
...
}
...
}
这里我仅列出了可能出现的逻辑。先来看是不是WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN。
如果type>=FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW就会返回WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;。这个type是哪里传过来的呢?其实这个type就是WindowManager.LayoutParam()生成时默认的,没有其他地方给他赋值,为WindowManager.LayoutParam.TYPE_APPLICATION。
WindowManager:
public static class LayoutParams extends ViewGroup.LayoutParams implements Parcelable {
...
public LayoutParams() {
super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
type = TYPE_APPLICATION;//值为2
format = PixelFormat.OPAQUE;
}
...
}
TYPE_APPLICATION的值为2而FIRST_SUB_WINDOW为1000,所以就不会返回WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN了。
也就是说在addWindow()方法中返回的只可能是WindowManagerGlobal.ADD_BAD_APP_TOKEN了。那么我们来看,这里的rootType就是原来的type,当token是null时他就肯定返回WindowManagerGlobal.ADD_BAD_APP_TOKEN了。
这个token是什么呢?
WindowToken token = displayContent.getWindowToken(
hasParent ? parentWindow.mAttrs.token : attrs.token);
再来看
DisplayContent:
WindowToken getWindowToken(IBinder binder) {
return mTokenMap.get(binder);
}
这里的mToken经过我层层查找其实就是调用PopupWindow的showAtLocation时传进来的View锚点的getWindowToken()
PopupWindow:
public void showAtLocation(View parent, int gravity, int x, int y) {
mParentRootView = new WeakReference<>(parent.getRootView());
showAtLocation(parent.getWindowToken(), gravity, x, y);
}
public void showAtLocation(IBinder token, int gravity, int x, int y) {
if (isShowing() || mContentView == null) {
return;
}
TransitionManager.endTransitions(mDecorView);
detachFromAnchor();
mIsShowing = true;
mIsDropdown = false;
mGravity = gravity;
final WindowManager.LayoutParams p = createPopupLayoutParams(token);
preparePopup(p);
p.x = x;
p.y = y;
invokePopup(p);
}
我们知道在Activity onCreate()的时候,这时候的View都是没有灵魂的View,他们没有根(ViewRootImpl)。这个时候View.getWindowToken()一定是null的所以会报错,而Dialog show的时候他在调用WindowManagerGlobal.addView()时会调用parentWindow. adjustLayoutParamsForSubWindow(wparams)给wparams传递mAppToken。首先这个parentWindow就是宿主Activity对应的PhoneWindow,而他的mAppToken就是Activity用于进程间通信的IBinder。而popupWindow他的parentWindow取的是View的getWindowToken()是null,所以就不会adjustLayoutParamsForSubWindow了,他的token依旧是null。
WindowManagerGlobal:
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
...
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
if (parentWindow != null) {
parentWindow.adjustLayoutParamsForSubWindow(wparams);
} else {
// If there's no parent, then hardware acceleration for this view is
// set from the application's hardware acceleration setting.
final Context context = view.getContext();
if (context != null && (context.getApplicationInfo().flags & ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
}
}
...
}
Window:
void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
...
} else {
if (wp.token == null) {
wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
}
if ((curTitle == null || curTitle.length() == 0)
&& mAppName != null) {
wp.setTitle(mAppName);
}
}
...
}
首先通过createPopupLayoutParams(token)把token传给p,再在invokePopup(p)里调用WindowManager.addView()
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);
}
}
然后就调用到WindowManagerGlobal的addView()
WindowManagerImpl:
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
WindowManagerGlobal:
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
...
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
...
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
// do this last because it fires off messages to start doing things
try {
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}
于是乎,我们的第一条结论Activity onCreate()里可以showDialog不可以show PopupWindow的原因就是这样的。
- 分析原因No.2
为什么View的post可以show PopupWindow 而Handler的post不行呢?
先来看View.post源码
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
// 如果当前View加入到了window中,直接调用UI线程的Handler发送消息
return attachInfo.mHandler.post(action);
}
// Assume that post will succeed later
// View未加入到window,放入ViewRootImpl的RunQueue中
getRunQueue().post(action);
return true;
}
View的post时候分两种情况,当View已经attach到window,直接调用UI线程的Handler发送runnable。如果View还未attach到window(onCreate里面肯定没有attach到window的),将runnable放入一个类型为HandlerActionQueue的RunQueue中。当下一次performTraversals到来的时候就会把这个RunQueue拿出来执行
ViewRootImpl
private void performTraversals() {
...
// Execute enqueued actions on every traversal in case a detached view enqueued an action
getRunQueue().executeActions(mAttachInfo.mHandler);
...
}
这就是为什么用View的post而不用Handler的post。
本篇源码使用api-27。