衆所周知,Activity中onCreate、onResume中無法直接獲取到View的寬高,原因是在這些生命週期中,View還沒有經過measure流程,爲什麼通過View.post方法能在回調中獲取到View的寬高呢?
接下來我們就扒一扒View.post的源碼,看看爲什麼在View.post中能正常打印View的寬高。
一、 Runnable去哪了?
先跟蹤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的結果驗證這個推斷。
二、 給Runnable排排隊
跟蹤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呢?
上面分析到執行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<OnAttachStateChangeListener> 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()有什麼明確的時序關係。慚愧呀慚愧。
四、dispatchAttachedToWindow執行時機
有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);
}