在做页面首次使用引导需求时使用 PopupWindow 来实现,没想到测试过程中一直遇到这两种异常而崩溃:
android.view.WindowManager$BadTokenException
Unable to add window – token android.os.BinderProxy@74dec62 is not valid; is your activity running?
java.lang.IllegalArgumentException
View=android.widget.PopupWindow$PopupDecorView{78549e8 V.E…… ……I. 0,0-0,0} not attached to window manager
这种崩溃让人百思不得其解,因为 PopupWindow 使用还是比较简单的,出错代码如下:
public class TestActivity extends Activity {
private TextView mTextView;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = (TextView) findViewById(R.id.text_view);
loadApiFromNet(new Callback() { // 服务器接口调用
@Override
public void onSuccess(String str) {
// 其他处理忽略
showPopup();
}
});
}
private void showPopup() {
View popupView = LayoutInflater.from(this).inflate(R.layout.popup, null);
((TextView) popupView.findViewById(R.id.tv_text)).setText("测试测试");
PopupWindow popup = new PopupWindow(popupView, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, true);
popup.setTouchable(false);
if (null != mTextView) {
popup.showAsDropDown(mTextView);
}
}
}
原因是 showAsDropDown() 方法中以来的 View 类型实例还没有依附到绘制 Window 中,因此将 PopupWindow 的显示逻辑延时即可解决问题:
if (null != view.getWindowToken() ) {
popupWindow.showAsDropDown(view, 0, 0);
} else {
view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
if (null != v.getWindowToken()) {
popupWindow.showAsDropDown(v, 0, 0);
v.removeOnAttachStateChangeListener(this);
}
}
@Override
public void onViewDetachedFromWindow(View v) {
}
});
}
问题解决了,但是这就结束了?
当然不行
知其然,更要知其所以然。怎么能放过这么个了解 Android 的机会呢?
那就让我们从源码的角度出发,探究造成异常的根源吧!
PopupWindow 的调用栈顺序为:
/**
* Displays the content view in a popup window anchored to the corner of
* another view. The window is positioned according to the specified
* gravity and offset by the specified x and y coordinates.
*
* If there is not enough room on screen to show the popup in its entirety,
* this method tries to find a parent scroll view to scroll. If no parent
* view can be scrolled, the specified vertical gravity will be ignored and
* the popup will anchor itself such that it is visible.
*
* If the view later scrolls to move anchor
to a different
* location, the popup will be moved correspondingly.
*
* @param anchor the view on which to pin the popup window
* @param xoff A horizontal offset from the anchor in pixels
* @param yoff A vertical offset from the anchor in pixels
* @param gravity Alignment of the popup relative to the anchor
*
* @see #dismiss()
*/
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);
}
没有看到 Popup 的显示流程,继续深入到 invokePopup() 方法中:
/**
* Invoke the popup window by adding the content view to the window
* manager.
*
* The content view must be non-null when this method is invoked.
*
* @param p the layout parameters of the popup's content view
*/
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);
}
}
很显然,在 invokePopup() 方法中找到了操作 View 的相关代码 mWindowManager.addView(decorView, p)。
从 PopupWindow 代码中容易发现,mWindowManager 是一个 WindowManger 类型变量,在构造方法中赋值:
public PopupWindow(View contentView, int width, int height, boolean focusable) {
if (contentView != null) {
mContext = contentView.getContext();
mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
}
setContentView(contentView);
setWidth(width);
setHeight(height);
setFocusable(focusable);
}
通过查看源码,最终在 SystemServiceRegistry 类的静态块中找到了 mWindowManager 的具体实例类型为 WindowManagerImpl:
Context.getSystemService(String)
->
ContextImpl.getSystemService(String)
->
SystemServiceRegistry.getSystemService(ContextImpl, String)
->
final class SystemServiceRegistry {
static {
// 省略。。。
registerService(Context.WINDOW_SERVICE, WindowManager.class,
new CachedServiceFetcher() {
@Override
public WindowManager createService(ContextImpl ctx) {
return new WindowManagerImpl(ctx);
}});
// 省略。。。
}
}
因此, mWindowManager.addView() 方法为
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
看到,这里面有使用了一层代理 WindowManagerGlobal,不过胜利就在眼前了,继续深入 WindowManagerGlobal.addView() 方法 :
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (display == null) {
throw new IllegalArgumentException("display must not be null");
}
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}
// 省略
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
// Start watching for system property changes.
if (mSystemPropertyUpdater == null) {
mSystemPropertyUpdater = new Runnable() {
@Override public void run() {
synchronized (mLock) {
for (int i = mRoots.size() - 1; i >= 0; --i) {
mRoots.get(i).loadSystemProperties();
}
}
}
};
SystemProperties.addChangeCallback(mSystemPropertyUpdater);
}
int index = findViewLocked(view, false);
if (index >= 0) {
if (mDyingViews.contains(view)) {
// Don't wait for MSG_DIE to make it's way through root's queue.
mRoots.get(index).doDie();
} else {
throw new IllegalStateException("View " + view
+ " has already been added to the window manager.");
}
// The previous removeView() had not completed executing. Now it has.
}
// If this is a panel window, then find the window it is being
// attached to for future reference.
if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
final int count = mViews.size();
for (int i = 0; i < count; i++) {
if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
panelParentView = mViews.get(i);
}
}
}
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.
synchronized (mLock) {
final int index = findViewLocked(view, false);
if (index >= 0) {
removeViewLocked(index, true);
}
}
throw e; //关注这里
}
}
终于,找到了抛出异常的代码块了,离成功还差一小点了。继续研究 root.setView() 方法,找到了 ViewRootImpt.java:
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;
// ** 省略 **
try {
mOrigWindowType = mWindowAttributes.type;
mAttachInfo.mRecomputeGlobalAttributes = true;
collectViewAttributes();
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);
} catch (RemoteException e) {
mAdded = false;
mView = null;
mAttachInfo.mRootView = null;
mInputChannel = null;
mFallbackEventHandler.setView(null);
unscheduleTraversals();
setAccessibilityFocus(null, null);
throw new RuntimeException("Adding window failed", e);
} finally {
if (restore) {
attrs.restore();
}
}
// ** 省略 **
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?");
case WindowManagerGlobal.ADD_NOT_APP_TOKEN:
throw new WindowManager.BadTokenException(
"Unable to add window -- token " + attrs.token
+ " is not for an application");
case WindowManagerGlobal.ADD_APP_EXITING:
throw new WindowManager.BadTokenException(
"Unable to add window -- app for token " + attrs.token
+ " is exiting");
case WindowManagerGlobal.ADD_DUPLICATE_ADD:
throw new WindowManager.BadTokenException(
"Unable to add window -- window " + mWindow
+ " has already been added");
case WindowManagerGlobal.ADD_STARTING_NOT_NEEDED:
// Silently ignore -- we would have just removed it
// right away, anyway.
return;
case WindowManagerGlobal.ADD_MULTIPLE_SINGLETON:
throw new WindowManager.BadTokenException("Unable to add window "
+ mWindow + " -- another window of type "
+ mWindowAttributes.type + " already exists");
case WindowManagerGlobal.ADD_PERMISSION_DENIED:
throw new WindowManager.BadTokenException("Unable to add window "
+ mWindow + " -- permission denied for window type "
+ mWindowAttributes.type);
case WindowManagerGlobal.ADD_INVALID_DISPLAY:
throw new WindowManager.InvalidDisplayException("Unable to add window "
+ mWindow + " -- the specified display can not be found");
case WindowManagerGlobal.ADD_INVALID_TYPE:
throw new WindowManager.InvalidDisplayException("Unable to add window "
+ mWindow + " -- the specified window type "
+ mWindowAttributes.type + " is not valid");
}
throw new RuntimeException(
"Unable to add window -- unknown error code " + res);
}
// ** 省略 **
}
}
}
这里发现了可能会抛出之前 BadToken 的异常,显然和
mWindowSession.addToDisplay()
方法相关。
在 WindowManagerGlobal 类中,发现通过 AIDL 最终调用到了
WindowManagerService.addWindow()
方法。AIDL 可以参考 Google 官方文档,查看 Android 源码可以借助AndroidXRef.com
触发了其中的一段校验导致了问题发生:
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;
}
}
在 windowForClientLocked() 方法中最终找到了出错原因:
final WindowState windowForClientLocked(Session session, IBinder client, boolean throwOnError) {
WindowState win = mWindowMap.get(client);
if (localLOGV) {
Slog.v(
TAG_WM, "Looking up client " + client + ": " + win);
}
if (win == null) {
RuntimeException ex = new IllegalArgumentException(
"Requested window " + client + " does not exist");
if (throwOnError) {
throw ex;
}
Slog.w(TAG_WM, "Failed looking up window", ex);
return null;
}
if (session != null && win.mSession != session) {
RuntimeException ex = new IllegalArgumentException(
"Requested window " + client + " is in session " +
win.mSession + ", not " + session);
if (throwOnError) {
throw ex;
}
Slog.w(TAG_WM, "Failed looking up window", ex);
return null;
}
return win;
}
可见异常的直接原因就是 IBinder 实例为 null。通过上述流程,在 PopupWIndow 中找到 IBinder 实例为
showAsDropDown(View anchor, int xoff, int yoff, int gravity)
方法中 anchor.getWindowToken() 的返回值。
// 未完待续
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;
}