View的post(Runnable action)方法原理探索

首先寫這篇文章之前貼上二篇分析較好的博客,非常感謝博主的奉獻(xiàn)精神
http://blog.csdn.net/scnuxisan225/article/details/49815269
http://mp.weixin.qq.com/s/laR3_Xvr0Slb4oR8D1eQyQ

大家都遇到過在android開發(fā)時(shí),在Activity中的onCreate方法中通過控件的getMeasureHeight/getHeight或者getMeasureWidth/getWidth方法獲取到的寬高大小都是0,我相信大家遇到這種問題時(shí)首先會(huì)想到打開度娘然后一搜,常見的二種解決方案就出來了。

1.通過監(jiān)聽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 });

當(dāng)我們注冊(cè)這個(gè)監(jiān)聽時(shí),控件經(jīng)過 onMeasure->onLayout->onDraw一系列方法渲染完成后會(huì)去回調(diào)這個(gè)注冊(cè)的監(jiān)聽,我們自然能拿到控件的寬高。

2.第二種方法我比較喜歡,只要用View.post()一個(gè)runnable就可以了

1 ...
2       view.post(new Runnable() {
3             @Override
4             public void run() {
5                 view.getHeight(); //height is ready
6             }
7         });
8 ...

我們一般這么用總能拿到控件的寬高大小,方法是非常好用,但是本著十萬個(gè)為什么態(tài)度,我決定把這個(gè)方法的原理整理一遍。

我們先回憶下,android中每個(gè)界面是個(gè)Activity,每個(gè)Activity最頂層是一個(gè)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時(shí),直接取出mAttachInfo中存放的Handler對(duì)象去post 我們的Runnable任務(wù),如果為null的話我們看看getRunQueue()方法會(huì)做什么

    private HandlerActionQueue getRunQueue() {
        if (mRunQueue == null) {
            mRunQueue = new HandlerActionQueue();
        }
        return mRunQueue;
    }

它會(huì)去創(chuàng)建一次HandlerActionQueue對(duì)象然后把這個(gè)對(duì)象返回,好我們點(diǎn)進(jìn)這個(gè)對(duì)象看一下

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的話會(huì)去調(diào)用HandlerActionQueue對(duì)象中的post方法傳遞我們Runnable任務(wù),接著會(huì)調(diào)用postDelayed方法,這個(gè)方法會(huì)把我們的Runnable任務(wù)和需要延遲的時(shí)間都封裝到HandlerAction對(duì)象中然后加入到下面的HandlerAction[]數(shù)組中,點(diǎn)進(jìn)去也會(huì)發(fā)現(xiàn)HandlerAction 就是一個(gè)簡單的封裝類。

 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);
        }
    }

現(xiàn)在我們回過頭思考一下就會(huì)有疑惑了,情景回到post方法中,我們根據(jù)mAttachInfo這個(gè)值判斷是直接post發(fā)送任務(wù)還是把任務(wù)放入隊(duì)列,那么這個(gè)值是什么時(shí)候被賦值的呢?

答案就在ViewRootImpl類中,在ViewRootImpl構(gòu)造中有這么端代碼創(chuàng)建了AttachInfo對(duì)象。

 mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this);

然后在performTraversals()方法中會(huì)去調(diào)用這么段代碼 ,這段代碼執(zhí)行在大家很熟悉的 performMeasure、performLayout、performDraw方法之前。

host.dispatchAttachedToWindow(mAttachInfo, 0);

host就是DecorView,它是一個(gè)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()));
        }
    }

可以看到主要這個(gè)方法中會(huì)調(diào)用自己和所有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 正是在這里被賦值的,而被賦值調(diào)用的地方正是在上面ViewRootImpl中的performTraversals()方法中!
然后我們接著看這個(gè)方法

 mRunQueue.executeActions(info.mHandler);

調(diào)用了這句代碼,執(zhí)行了我們前面提到的HandlerAction類中的executeActions方法,我們看下這個(gè)方法做了些什么事情

    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,一切思路都那么清晰了,這個(gè)方法會(huì)用我們傳遞進(jìn)來的mAttachInfo中的Handler去遍歷我們View.post儲(chǔ)存的所有Runnable任務(wù)。

至此貌似所有的流程都分析完畢了,但是如果有細(xì)心的同學(xué)會(huì)發(fā)現(xiàn)前面的分析有漏洞,哪里呢?就是在執(zhí)行ViewRootImpl中的performTraversals()方法的時(shí)候,

host.dispatchAttachedToWindow(mAttachInfo, 0);

調(diào)用這個(gè)方法明明執(zhí)行在測(cè)量,布局,繪制三個(gè)方法之前,也就是說調(diào)用這個(gè)方法后就會(huì)拿到我們傳遞mAttachInfo中Handler去執(zhí)行View.post中的Runnable,然后才會(huì)去調(diào)用測(cè)量,布局,繪制三個(gè)方法,那理論上還是拿不到寬高值啊,這個(gè)時(shí)候還沒執(zhí)行測(cè)量啊,可是為什么結(jié)果可以拿到呢???

沒關(guān)系,我一開始也是這么認(rèn)為的并且陷入了很長一段時(shí)間的困惑當(dāng)中,這是由于不了解android消息機(jī)制導(dǎo)致的,Android的運(yùn)行其實(shí)是一個(gè)消息驅(qū)動(dòng)模式,也就是說在Android主線程中默認(rèn)是創(chuàng)建了一個(gè)Handler的,并且這個(gè)主線程中創(chuàng)建了一個(gè)Looper去循環(huán)遍歷執(zhí)行隊(duì)列中的Message,這個(gè)執(zhí)行是同步的,也就是說執(zhí)行完一個(gè)Message后才會(huì)去繼續(xù)執(zhí)行下一個(gè),調(diào)用performTraversals()這個(gè)方法是通過主線程的Looper遍歷執(zhí)行的,這個(gè)方法還沒執(zhí)行結(jié)束,然后我們?cè)谶@個(gè)方法中通過mAttachInfo中Handler去執(zhí)行View.post中的Runnable,mAttachInfo中Handler也是創(chuàng)建在主線程,所以它會(huì)在上一個(gè)消息執(zhí)行結(jié)束后才會(huì)被執(zhí)行,也就是說會(huì)在測(cè)量,布局,繪制執(zhí)行后才執(zhí)行,這樣自然而然能拿到控件的寬和高啦。

我不禁敬佩,Android團(tuán)隊(duì)的這個(gè)機(jī)制設(shè)計(jì)的太巧妙了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容