众所周知,Activity中onCreate、onResume中无法直接获取到View的宽高,原因是在这些生命周期中,View还没有经过measure流程,为什么通过View.post方法能在回调中获取到View的宽高呢?
接下来我们就扒一扒View.post的源码,看看为什么在View.post中能正常打印View的宽高。
先跟踪View.post方法,看看这个post出去的Runnable去哪了。
// View.post
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构造方法
AttachInfo(IWindowSession session, IWindow window, Display display,
ViewRootImpl viewRootImpl, Handler handler, Callbacks effectPlayer,
Context context) {
mSession = session;
mWindow = window;
mWindowToken = window.asBinder();
mDisplay = display;
mViewRootImpl = viewRootImpl;
mHandler = handler;
mRootCallbacks = effectPlayer;
mTreeObserver = new ViewTreeObserver(context);
}
可以看到,View.post中会先尝试调用mAttachInfo的handler处理Runnable,如果mAttachInfo还没有初始化,会将其加入到一个队列里。
而AttachInfo的handler是一个普通的handler,如果调用它的post方法处理Runnable,是做不到确保View测绘完才执行Runnable的。由此可以断定Activity onCreate和onResume时 View的mAttachInfo没有完成初始化,post出去的Runnable是被加入了一个队列,当然也可以使用反射打印mAttachInfo或打印View.getViewRootImpl的结果验证这个推断。
跟踪getRunQueue()代码我们很容易得知,这个队列的类是HandlerActionQueue,代码如下(有删减)
public class HandlerActionQueue {
private HandlerAction[] mActions;
private int mCount;
public void postDelayed(Runnable action, long delayMillis) {
final HandlerAction handlerAction = new HandlerAction(action, delayMillis);
synchronized (this) {
if (mActions == null) {
mActions = new HandlerAction[4];
}
mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
mCount++;
}
}
public void executeActions(Handler handler) {
synchronized (this) {
final HandlerAction[] actions = mActions;
for (int i = 0, count = mCount; i < count; i++) {
final HandlerAction handlerAction = actions[i];
handler.postDelayed(handlerAction.action, handlerAction.delay);
}
mActions = null;
mCount = 0;
}
}
private static class HandlerAction {
final Runnable action;
final long delay;
public boolean matches(Runnable otherAction) {
return otherAction == null && action == null
|| action != null && action.equals(otherAction);
}
}
}
被post进来的Runnable会被封装为HandlerAction对象,并被维护进一个HandlerAction数组中,等调用executeActions(Hander handler)方法时,会依次将HandlerAction取出,将给传入的handler执行。
上面分析到执行Runnable的时机就是触发executeActions的时机,在View中搜索executeActions,只有一处调用,在View.dispatchAttachedToWindow()方法中。
View.dispatchAttachedToWindow
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;
...
// Transfer all pending runnables.
if (mRunQueue != null) {
mRunQueue.executeActions(info.mHandler);
mRunQueue = null;
}
performCollectViewAttributes(mAttachInfo, visibility);
onAttachedToWindow();
ListenerInfo li = mListenerInfo;
final CopyOnWriteArrayList listeners =
li != null ? li.mOnAttachStateChangeListeners : null;
if (listeners != null && listeners.size() > 0) {
for (OnAttachStateChangeListener listener : listeners) {
listener.onViewAttachedToWindow(this);
}
}
...
int vis = info.mWindowVisibility;
if (vis != GONE) {
onWindowVisibilityChanged(vis);
if (isShown()) {
onVisibilityAggregated(vis == VISIBLE);
}
}
...
onVisibilityChanged(this, visibility);
...
}
一看名字就知道这是一个很重要的方法,View在这里给mAttachInfo赋了值(mAttachInfo携带了很多跟Window、ViewRootImpl相关的信息,并且以后再调用View.post都会交给mAttachInfo处理),执行了mAttachInfo没赋值期间post进来的Runnable等等。
当然,找到了Runnable调用的地方才是刚开始。
接着我们需要知道View.dispatchAttachedToWindow()是什么时候调用的,跟View.measure()有什么明确的时序关系。惭愧呀惭愧。
有Android源码的同学可以在framework/base下grep一下,没有的可以上http://androidxref.com/搜索一下dispatchAttachedToWindow方法,很容易可以定位到这个方法是在ViewRootImpl.performTraversals()里执行的,performTraversals应该是大家比较眼熟的一个方法了,可以简化成下面的样子
performTraversals() {
if(isFirst) {
host.dispatchAttachedToWindow(mAttachInfo, 0);
}
performMeasure();
performLayout()
performDraw();
}
由(三)的分析可以知道执行dispatchAttachedToWindow的时候将post消息传递了出去,也就是说View.post的消息一定是在performTraversals执行完才执行的。
想起来在面试的时候回答了是因为Choreographer收到SF发来的Vsync信号触发绘制时会先postSyncBarrier,导致同步message被阻塞。。。真是牛头不对马嘴呀。。。
Choreographer触发绘制时确实会先postSyncBarrier,优先处理绘制消息,但在执行绘制消息的doTraversals方法里已经把Barrier移除了,而performTraversals正是在Barrier移除后执行的。
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
另外,在performTraversals中的这段代码解答了我对多次measure的疑惑,如果LayoutParam使用了weight,将会以精确模式再次触发measure流程。
if (lp.horizontalWeight > 0.0f) {
width += (int) ((mWidth - width) * lp.horizontalWeight);
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
MeasureSpec.EXACTLY);
measureAgain = true;
}
if (lp.verticalWeight > 0.0f) {
height += (int) ((mHeight - height) * lp.verticalWeight);
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
MeasureSpec.EXACTLY);
measureAgain = true;
}
if (measureAgain) {
if (DEBUG_LAYOUT) Log.v(mTag,
"And hey let's measure once more: width=" + width
+ " height=" + height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}