爲什麼View.post中能獲取到View寬高

衆所周知,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);
   }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章