View.post獲取控制元件寬高原理探索
大家都遇到過在android開發時,在Activity中的onCreate方法中通過控制元件的getMeasureHeight/getHeight或者getMeasureWidth/getWidth方法獲取到的寬高大小都是0,我相信大家遇到這種問題時首先會想到開啟度娘然後一搜,常見的二種解決方案就出來了。
1.通過監聽Draw/Layout事件:ViewTreeObserver
1 view.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
2 @Override
3 public void onGlobalLayout() {
4 mScrollView.post(new Runnable() {
5 public void run() {
6 view.getHeight(); //height is ready
7 }
8 });
9 }
10 });
當我們註冊這個監聽時,控制元件經過 onMeasure->onLayout->onDraw一系列方法渲染完成後會去回撥這個註冊的監聽,我們自然能拿到控制元件的寬高。
2.第二種方法我比較喜歡,只要用View.post()一個runnable就可以了
1 ...
2 view.post(new Runnable() {
3 @Override
4 public void run() {
5 view.getHeight(); //height is ready
6 }
7 });
8 ...
我們一般這麼用總能拿到控制元件的寬高大小,方法是非常好用,但是本著十萬個為什麼態度,我決定把這個方法的原理整理一遍。
我們先回憶下,android中每個介面是個Activity,每個Activity最頂層是一個DecorView,它包裹我們自定義的佈局.
好接下來我們看下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;
}
這裡面判斷mAttachInfo是不是為空,如果不為null時,直接取出mAttachInfo中存放的Handler物件去post 我們的Runnable任務,如果為null的話我們看看getRunQueue()方法會做什麼
private HandlerActionQueue getRunQueue() {
if (mRunQueue == null) {
mRunQueue = new HandlerActionQueue();
}
return mRunQueue;
}
它會去建立一次HandlerActionQueue物件然後把這個物件返回,好我們點進這個物件看一下
public class HandlerActionQueue {
private HandlerAction[] mActions;
private int mCount;
public void post(Runnable action) {
postDelayed(action, 0);
}
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++;
}
}
....
我們之前程式碼可以看到如果mAttachInfo為null的話會去呼叫HandlerActionQueue物件中的post方法傳遞我們Runnable任務,接著會呼叫postDelayed方法,這個方法會把我們的Runnable任務和需要延遲的時間都封裝到HandlerAction物件中然後加入到下面的HandlerAction[]陣列中,點進去也會發現HandlerAction 就是一個簡單的封裝類。
private static class HandlerAction {
final Runnable action;
final long delay;
public HandlerAction(Runnable action, long delay) {
this.action = action;
this.delay = delay;
}
public boolean matches(Runnable otherAction) {
return otherAction == null && action == null
|| action != null && action.equals(otherAction);
}
}
現在我們回過頭思考一下就會有疑惑了,情景回到post方法中,我們根據mAttachInfo這個值判斷是直接post傳送任務還是把任務放入佇列,那麼這個值是什麼時候被賦值的呢?
答案就在ViewRootImpl類中,在ViewRootImpl構造中有這麼端程式碼建立了AttachInfo物件。
mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this);
然後在performTraversals()方法中會去呼叫這麼段程式碼 ,這段程式碼執行在大家很熟悉的 performMeasure、performLayout、performDraw方法之前。
host.dispatchAttachedToWindow(mAttachInfo, 0);
host就是DecorView,它是一個ViewGroup,所以我們先看看ViewGroup中的dispatchAttachedToWindow方法
@Override
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mGroupFlags |= FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;
super.dispatchAttachedToWindow(info, visibility);
mGroupFlags &= ~FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW;
final int count = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < count; i++) {
final View child = children[i];
child.dispatchAttachedToWindow(info,
combineVisibility(visibility, child.getVisibility()));
}
final int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size();
for (int i = 0; i < transientCount; ++i) {
View view = mTransientViews.get(i);
view.dispatchAttachedToWindow(info,
combineVisibility(visibility, view.getVisibility()));
}
}
可以看到主要這個方法中會呼叫自己和所有child父類也就是View中的dispatchAttachedToWindow方法,那我們看看View中的dispatchAttachedToWindow方法究竟幹了些什麼事情吧?
/**
* @param info the {@link android.view.View.AttachInfo} to associated with
* this view
*/
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;
if (mOverlay != null) {
mOverlay.getOverlayView().dispatchAttachedToWindow(info, visibility);
}
mWindowAttachCount++;
// We will need to evaluate the drawable state at least once.
mPrivateFlags |= PFLAG_DRAWABLE_STATE_DIRTY;
if (mFloatingTreeObserver != null) {
info.mTreeObserver.merge(mFloatingTreeObserver);
mFloatingTreeObserver = null;
}
registerPendingFrameMetricsObservers();
if ((mPrivateFlags&PFLAG_SCROLL_CONTAINER) != 0) {
mAttachInfo.mScrollContainers.add(this);
mPrivateFlags |= PFLAG_SCROLL_CONTAINER_ADDED;
}
// 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) {
// NOTE: because of the use of CopyOnWriteArrayList, we *must* use an iterator to
// perform the dispatching. The iterator is a safe guard against listeners that
// could mutate the list by calling the various add/remove methods. This prevents
// the array from being modified while we iterate it.
for (OnAttachStateChangeListener listener : listeners) {
listener.onViewAttachedToWindow(this);
}
}
int vis = info.mWindowVisibility;
if (vis != GONE) {
onWindowVisibilityChanged(vis);
if (isShown()) {
// Calling onVisibilityChanged directly here since the subtree will also
// receive dispatchAttachedToWindow and this same call
onVisibilityAggregated(vis == VISIBLE);
}
}
// Send onVisibilityChanged directly instead of dispatchVisibilityChanged.
// As all views in the subtree will already receive dispatchAttachedToWindow
// traversing the subtree again here is not desired.
onVisibilityChanged(this, visibility);
if ((mPrivateFlags&PFLAG_DRAWABLE_STATE_DIRTY) != 0) {
// If nobody has evaluated the drawable state yet, then do it now.
refreshDrawableState();
}
needGlobalAttributesUpdate(false);
}
這段程式碼比較長但是我們一眼就可以看到mAttachInfo 正是在這裡被賦值的,而被賦值呼叫的地方正是在上面ViewRootImpl中的performTraversals()方法中!
然後我們接著看這個方法
mRunQueue.executeActions(info.mHandler);
呼叫了這句程式碼,執行了我們前面提到的HandlerAction類中的executeActions方法,我們看下這個方法做了些什麼事情
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;
}
}
ok,一切思路都那麼清晰了,這個方法會用我們傳遞進來的mAttachInfo中的Handler去遍歷我們View.post儲存的所有Runnable任務。
至此貌似所有的流程都分析完畢了,但是如果有細心的同學會發現前面的分析有漏洞,哪裡呢?就是在執行ViewRootImpl中的performTraversals()方法的時候,
host.dispatchAttachedToWindow(mAttachInfo, 0);
呼叫這個方法明明執行在測量,佈局,繪製三個方法之前,也就是說呼叫這個方法後就會拿到我們傳遞mAttachInfo中Handler去執行View.post中的Runnable,然後才會去呼叫測量,佈局,繪製三個方法,那理論上還是拿不到寬高值啊,這個時候還沒執行測量啊,可是為什麼結果可以拿到呢???
沒關係,我一開始也是這麼認為的並且陷入了很長一段時間的困惑當中,這是由於不瞭解android訊息機制導致的,Android的執行其實是一個訊息驅動模式,也就是說在Android主執行緒中預設是建立了一個Handler的,並且這個主執行緒中建立了一個Looper去迴圈遍歷執行佇列中的Message,這個執行是同步的,也就是說執行完一個Message後才會去繼續執行下一個,呼叫performTraversals()這個方法是通過主執行緒的Looper遍歷執行的,這個方法還沒執行結束,然後我們在這個方法中通過mAttachInfo中Handler去執行View.post中的Runnable,mAttachInfo中Handler也是建立在主執行緒,所以它會在上一個訊息執行結束後才會被執行,也就是說會在測量,佈局,繪製執行後才執行,這樣自然而然能拿到控制元件的寬和高啦。
我不禁敬佩,Android團隊的這個機制設計的太巧妙了。