刨根究底之在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。