1. 程式人生 > >View.post獲取控制元件寬高原理探索

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團隊的這個機制設計的太巧妙了。