開發的過程中,我們有時候需要在Activity
啟動以后第一時間獲取某個View的寬高,并作相應處理,當我們在onCreate
中通過View.getWidth
(或getMeasuredWidth
)和View.getHeight
(或getMeasuredHeight
)方法獲取的時候,你會發現它們都返回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
延遲幾秒來確保萬無一失。那么這里面到底會不會有不穩定的因素呢?這篇文章告訴你答案。
本篇文章分析的是Android 6.0系統的源碼。閱讀之前,請先了解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
的方法中執行的時候,attachInfo==null
(為什么?請自行驗證),所以post方法執行到了
ViewRootImpl.getRunQueue().post(action)。
這個方法是將action
先保存到ViewRootImpl
中的一個靜態隊列中,保存起來什么時候用呢?
在ViewRootImpl.perfromTravesals
方法中可以看到這個隊列調用的代碼,
// 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
是在布局遍歷的時候執行的。然而,它是在performMeasure、performLayout之前執行的。也就是說,這個Runnable
是在View的測量和布局之前執行。View測量和布局完成之前是獲取不到寬高的,那我們的Runnable
是怎么獲取到寬高的呢。
事實上,getRunQueue().executeActions(mAttachInfo.mHandler)
也不是直接執行這些Runnable
,而是往主線程消息隊列添加對應的消息進去,那么我們的這個消息執行的時機是怎么保證在View布局完成之后呢?研究過performTraversals
源碼的話,應該知道,performTraversals
這個方法也是被附到一個消息上添加到消息隊列等待執行的。下面看分析。
首先從View布局的終極方法performTraversals
開始分析,這個方法到底是怎么執行的,在哪執行的。一路追過去,
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
調用,
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
而doTraversal
方法在TraversalRunnable
調用,那么這個Runnable
的對象在哪執行的呢?
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
可以看到,mTraversalRunnable
出現在這個方法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
,猜測是不是也是加到消息隊列去了?
繼續看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();
}
}
在這個構造函數中,可以看到mHandler
構造的時候傳入一個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
方法實際上也是被加入到消息隊列中去執行的,而我們最初post的Runnable
,是在performTraversals
方法中將其附加到一個消息上并加入到消息隊列里。所以,我們post的Runnable
的消息,肯定是在performTraversals
的消息之后執行的,也就是我們post的Runnable
肯定是在performTraversals
之后執行,而performTraversals
之后,View的寬和高便計算出來了。
需要注意的是, 在onCreate
(以及onStart
, onResume
)方法中必須保證是在主線程調用View.post
方法,因為ViewRootImpl.getRunQueue()
在不同線程獲取到的是不同的對象(為什么?請研究getRunQueue()
的類型),所以在onCreate
方法內新建線程并在里面post的時候,這個post是成功不了的。在Android 7.0中修復了這個問題,也就說7.0系統中,在onCreate
方法中新建線程并在里面post,這個Runnable
是可以被執行到的。感興趣的看源碼。
一句話總結:
在onCreate
中調用View.post
的時候,post的Runnable
被保存起來,在下一次遍歷布局的時候重新加到消息隊列里(Android 7.0是在attach window的時候重新加到消息隊列里),而且布局遍歷本身也是加到消息隊列執行的,所以我們post的Runnable
所在的消息肯定是在布局遍歷之后,所以在Runnable
里必定可以獲取到正確的寬高。