可以我們都遇到這樣一個bug,在Activity的onCreate()里調(diào)用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方法里執(zhí)行顯示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調(diào)用addToDisplay()返回碼是WindowManagerGlobal.ADD_BAD_APP_TOKEN。
在看問題之前先看幾個經(jīng)我測試過的結(jié)論:
- 同樣是在onCreate()去show,Dialog就不會報錯,而PopupWindow卻會報錯。
- 用View的post方法可以showPopupWindow,而用Handler的post卻不行。
我們一步一步來看吧。
- 分析原因No.1
既然res是WindowManagerGlobal.ADD_BAD_APP_TOKEN,有人會問為什么不是WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN?別著急,我會給大家講清楚的。
我們進(jìn)入到 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(){...}
...
}
...
}
這里我僅列出了可能出現(xiàn)的邏輯。先來看是不是WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN。
如果type>=FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW就會返回WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;。這個type是哪里傳過來的呢?其實(shí)這個type就是WindowManager.LayoutParam()生成時默認(rèn)的,沒有其他地方給他賦值,為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,當(dāng)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經(jīng)過我層層查找其實(shí)就是調(diào)用PopupWindow的showAtLocation時傳進(jìn)來的View錨點(diǎn)的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的時候他在調(diào)用WindowManagerGlobal.addView()時會調(diào)用parentWindow. adjustLayoutParamsForSubWindow(wparams)給wparams傳遞mAppToken。首先這個parentWindow就是宿主Activity對應(yīng)的PhoneWindow,而他的mAppToken就是Activity用于進(jìn)程間通信的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)里調(diào)用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);
}
}
然后就調(diào)用到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;
}
}
}
于是乎,我們的第一條結(jié)論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) {
// 如果當(dāng)前View加入到了window中,直接調(diào)用UI線程的Handler發(fā)送消息
return attachInfo.mHandler.post(action);
}
// Assume that post will succeed later
// View未加入到window,放入ViewRootImpl的RunQueue中
getRunQueue().post(action);
return true;
}
View的post時候分兩種情況,當(dāng)View已經(jīng)attach到window,直接調(diào)用UI線程的Handler發(fā)送runnable。如果View還未attach到window(onCreate里面肯定沒有attach到window的),將runnable放入一個類型為HandlerActionQueue的RunQueue中。當(dāng)下一次performTraversals到來的時候就會把這個RunQueue拿出來執(zhí)行
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。