開發(fā)的過程中,我們有時候需要在Activity啟動以后第一時間獲取某個View的寬高,并作相應處理,當我們在onCreate中通過View.getWidth(或getMeasuredWidth)和View.getHeight(或getMeasuredHeight)方法獲取的時候,你會發(fā)現(xiàn)它們都返回0。我們猜測是因為,這個時候View還沒有布局完成。解決的辦法有很多種,比如在View的OnGlobalLayoutListener中獲取,在onLayout中獲取等等,具體請參考StackOverflow:
https://stackoverflow.com/questions/18861585/get-content-view-size-in-oncreate
有另外一個方法,有些人應該也知道,在onCreate中通過View.post,在其中的Runnable中就能獲取到正確的寬高。那么,這里的原理是什么,很多人認為這里就是一個時間差,等View的measure和layout完了就有寬和高了,有些人甚至使用postDelay延遲幾秒來確保萬無一失。那么這里面到底會不會有不穩(wěn)定的因素呢?這篇文章告訴你答案。
本篇文章分析的是Android 6.0系統(tǒng)的源碼。閱讀之前,請先了解Android中Handler,Looper和MessageQueue等概念,推薦文章:http://blog.csdn.net/lmj623565791/article/details/38377229/
詳細分析:
請看View.post源碼,
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
// Assume that post will succeed later
ViewRootImpl.getRunQueue().post(action);
return true;
}
在onCreate的方法中執(zhí)行的時候,attachInfo==null(為什么?請自行驗證),所以post方法執(zhí)行到了
ViewRootImpl.getRunQueue().post(action)。
這個方法是將action先保存到ViewRootImpl中的一個靜態(tài)隊列中,保存起來什么時候用呢?
在ViewRootImpl.perfromTravesals方法中可以看到這個隊列調(diào)用的代碼,
// Execute enqueued actions on every traversal in case a detached view enqueued an action
getRunQueue().executeActions(mAttachInfo.mHandler);
...
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
performLayout(lp, mWidth, mHeight);
...
performDraw();
大家都知道performTraversals是布局遍歷的方法,所以我們知道了,post的Runnable是在布局遍歷的時候執(zhí)行的。然而,它是在performMeasure、performLayout之前執(zhí)行的。也就是說,這個Runnable是在View的測量和布局之前執(zhí)行。View測量和布局完成之前是獲取不到寬高的,那我們的Runnable是怎么獲取到寬高的呢。
事實上,getRunQueue().executeActions(mAttachInfo.mHandler) 也不是直接執(zhí)行這些Runnable,而是往主線程消息隊列添加對應的消息進去,那么我們的這個消息執(zhí)行的時機是怎么保證在View布局完成之后呢?研究過performTraversals源碼的話,應該知道,performTraversals這個方法也是被附到一個消息上添加到消息隊列等待執(zhí)行的。下面看分析。
首先從View布局的終極方法performTraversals開始分析,這個方法到底是怎么執(zhí)行的,在哪執(zhí)行的。一路追過去,
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
perfromTraversals由doTraversal調(diào)用,
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
而doTraversal方法在TraversalRunnable調(diào)用,那么這個Runnable的對象在哪執(zhí)行的呢?
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
可以看到,mTraversalRunnable出現(xiàn)在這個方法scheduleTraversals里:
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
這里面mTraversalRunnable也是被post了,這個post方法叫做postCallback,猜測是不是也是加到消息隊列去了?
繼續(xù)看mChoreographer.postCallback方法,這個方法最終會到下面這個方法中:
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
if (DEBUG_FRAMES) {
Log.d(TAG, "PostCallback: type=" + callbackType
+ ", action=" + action + ", token=" + token
+ ", delayMillis=" + delayMillis);
}
synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
if (dueTime <= now) {
scheduleFrameLocked(now);
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}
注意到熟悉的一句代碼:mHandler.sendMessageAtTime,這個方法就是將消息加入到消息隊列,而這個Handler對應的消息隊列是什么?
我們從 mHandler這個對象著手,看看它是怎么來的。
private Choreographer(Looper looper) {
mLooper = looper;
mHandler = new FrameHandler(looper);
mDisplayEventReceiver = USE_VSYNC ? new FrameDisplayEventReceiver(looper) : null;
mLastFrameTimeNanos = Long.MIN_VALUE;
mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());
mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
for (int i = 0; i <= CALLBACK_LAST; i++) {
mCallbackQueues[i] = new CallbackQueue();
}
}
在這個構(gòu)造函數(shù)中,可以看到mHandler構(gòu)造的時候傳入一個Looper對象,那就要看下這個Looper是從哪里來的。
// Thread local storage for the choreographer.
private static final ThreadLocal<Choreographer> sThreadInstance =
new ThreadLocal<Choreographer>() {
@Override
protected Choreographer initialValue() {
Looper looper = Looper.myLooper();
if (looper == null) {
throw new IllegalStateException("The current thread must have a looper!");
}
return new Choreographer(looper);
}
};
這里又看到熟悉的代碼了Looper looper = Looper.myLooper(); 獲取了當前線程(主線程)的Looper對象。
所以,performTraversals方法實際上也是被加入到消息隊列中去執(zhí)行的,而我們最初post的Runnable,是在performTraversals方法中將其附加到一個消息上并加入到消息隊列里。所以,我們post的Runnable的消息,肯定是在performTraversals的消息之后執(zhí)行的,也就是我們post的Runnable肯定是在performTraversals之后執(zhí)行,而performTraversals之后,View的寬和高便計算出來了。
需要注意的是, 在onCreate(以及onStart, onResume)方法中必須保證是在主線程調(diào)用View.post方法,因為ViewRootImpl.getRunQueue()在不同線程獲取到的是不同的對象(為什么?請研究getRunQueue()的類型),所以在onCreate方法內(nèi)新建線程并在里面post的時候,這個post是成功不了的。在Android 7.0中修復了這個問題,也就說7.0系統(tǒng)中,在onCreate方法中新建線程并在里面post,這個Runnable是可以被執(zhí)行到的。感興趣的看源碼。
一句話總結(jié):
在onCreate中調(diào)用View.post的時候,post的Runnable被保存起來,在下一次遍歷布局的時候重新加到消息隊列里(Android 7.0是在attach window的時候重新加到消息隊列里),而且布局遍歷本身也是加到消息隊列執(zhí)行的,所以我們post的Runnable所在的消息肯定是在布局遍歷之后,所以在Runnable里必定可以獲取到正確的寬高。