背景
在Activity
onCreate
中调用 popupWindow.showAtLocation
时发生了上述crash异常。
原因追溯
// parent – a parent view to get the View.getWindowToken() token from
public void showAtLocation(View parent, int gravity, int x, int y) {
mParentRootView = new WeakReference<>(parent.getRootView());
showAtLocation(parent.getWindowToken(), gravity, x, y);
}
该方法需要传入四个参数,后面3个用来确定位置暂且不管,重点看第一个参数parent
,用来获取windowToken
的父view。这里可以理解为windowToken是个必要条件。
/**
* Retrieve a unique token identifying the window this view is attached to.
* @return Return the window's token for use in
* {@link WindowManager.LayoutParams#token WindowManager.LayoutParams.token}.
*/
public IBinder getWindowToken() {
return mAttachInfo != null ? mAttachInfo.mWindowToken : null;
}
继续查看源码发现其实所谓的 Token
竟然是个 IBinder
估计是用来和WindowManager这种系统服务IPC用的,这里暂不深究。
从方法实现可以看出,最终获取的是 AttachInfo
的Token,这个报错信息应该也就是因为.mAttachInfo
为null导致的。
/**
* @param info the {@link android.view.View.AttachInfo} to associated with
* this view
*/
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;
......
}
接下来跟了一下 mAttachInfo
的赋值时间,发现在 dispatchAttachedToWindow
时进行的赋值,本来想继续跟下去但是发现可能涉及到系统源码进不去了,因为时间原因大概分析了一下。
从方法名上可以判断出在view关联到窗口的时候会被调用,因此在 Activity.onCreate
的时候去调用的话不能保证此时view已经关联成功。
解决方案
根据上面的分析,是因为当前view还没有关联到窗口所以才导致的错误,所以只要在确定view初始化完成并且关联到窗口之后再调用就好了。
实现的方案应该还挺多的,这里提供一个思路
/**
* <p>Causes the Runnable to be added to the message queue.
* The runnable will be run on the user interface thread.</p>
*
* @param action The Runnable that will be executed.
*
* @return Returns true if the Runnable was successfully placed in to the
* message queue. Returns false on failure, usually because the
* looper processing the message queue is exiting.
*
* @see #postDelayed
* @see #removeCallbacks
*/
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
// Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
getRunQueue().post(action);
return true;
}
View.post
:从源码可以看出,这个方法正好也是以 AttachInfo
作为判断依据,确保在该成员变量被初始化后执行 Runnable
。
因此直接将 Popupwinodw.showAtLocation 方法放在该view的post中即可解决。
parentView.post{
popupWindow.showAtLocation(parentView, gravity, x, y)
}
后续
最近在复习AWS、WMS相关知识,突然回想起了这个问题,这里补充一下。
dispatchAttachedToWindow
方法往上追溯发现以下调用链:
ViewGroup.dispatchAttachedToWindow -> View.dispatchAttachedToWindow
而我们知道Android中根View是DecorView,那么DecorView在什么时候执行的呢?
private void performTraversals() {
// cache mView since it is used so much below...
final View host = mView;
if (mFirst) {
// deocrView 调用dispatchAttachedToWindow 并依次传递下去
host.dispatchAttachedToWindow(mAttachInfo, 0);
mAttachInfo.mTreeObserver.dispatchOnWindowAttachedChange(true);
dispatchApplyInsets(host);
}
}
熟悉Android View 绘制机制的朋友看到这个方法应该就非常熟悉了,这其实是 ViewRootImpl
中调用的,因此完整的调用链应该是:
RootViewImpl.schduleTraversals -> RootViewImpl.doTraversal -> RootViewImpl.performTraversals -> DecorView.dispatchOnWindowAttachedChange -> childView.dispatchOnWindowAttachedChange
因为view的绘制周期并不在onCreate,因此肯定是没有赋值的,那为什么view.post就可以了呢?
上面解决方案中提到 post 以 AttachInfo 作为判断依据,确保在该成员变量被初始化后执行 Runnable ,这里打脸了,确实是理解错了,而且明明还贴了源码出来,只能说当时还是头脑不太清醒。
重新梳理解决思路
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
// Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
getRunQueue().post(action);
return true;
}
重新看这段代码,由于此时attachInfo肯定为null,因此会执行 getRunQueue().post(action);
/**
* Returns the queue of runnable for this view.
*
* @return the queue of runnables for this view
*/
private HandlerActionQueue getRunQueue() {
if (mRunQueue == null) {
mRunQueue = new HandlerActionQueue();
}
return mRunQueue;
}
可以发现,其实是将 action post 给了View自己的一个消息队列中,这个消息队列比较简单,和MessageQueue不一样的是他底层是由数组实现的,这里主要关心他的消费方法 executeActions
在哪里调用。
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;
// Transfer all pending runnables.
if (mRunQueue != null) {
mRunQueue.executeActions(info.mHandler);
mRunQueue = null;
}
}
这个方法就非常熟悉了,正是开始分析的源头,也就是 mAttachInfo 赋值的源头,在 mAttachInfo 进行赋值时View会将在绘制之前的所有消息在此时统一实现,因此这时候我们去调用 popupWindow.showAtLocation
自然就没有问题了。
知识拓展
那么在view已经绘制完成后,去调用 View.post
会怎么执行呢?
这时候会执行 attachInfo.mHandler.post(action)
,这里考察的就是Handler机制了,长话短说,这里的Handler使用的时主线程的Looper,因此和我们在主线程中创建的 Handler.post
效果是一致的。
总结
经过前后两次的分析,短短的一个方法调用却涉及到非常多的应用生命周期知识,可见理解AMS和WMS对我们开发中遇到的一些实际问题的根源排查确实能起到很大的作用,因此有时间确实还是应该多读系统源码。