Android中View的異步消息

一、 摘要

介紹Android中View的異步消息,以及消息傳遞流程。


二、 分析View.post()

說起View的異步消息,也就是View.post()這個方法,它常用於在主線程更新UI,我們要搞清楚它的具體實現,先來看看post()的源碼(postDelayed()原理相同,因此不再單獨講解):

/**
 * <p>Causes the Runnable to be added to the message queue.
 * The runnable will be run on the user interface thread.</p>
 *
 * @param action The Runnable that will be executed.
 *
 * @return Returns true if the Runnable was successfully placed in to the
 *         message queue.  Returns false on failure, usually because the
 *         looper processing the message queue is exiting.
 *
 * @see #postDelayed
 * @see #removeCallbacks
 */
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;
}

這部分告訴我們以下幾點信息:

  • Runnable會被加入到消息隊列。
  • Runnable將在主線程執行。
  • 當mAttachInfo爲空時,消息會被髮送到RunQueue當中。

因此,我們分別討論這兩種情況:(1)mAttachInfo不爲空:AttachInfo的實例化;(2)mAttachInfo爲空:RunQueue機制。

1. AttachInfo的實例化

先追蹤mAttachInfo的賦值過程:

然後我們來看dispatchAttachedToWindow()的調用:

根據上一節《Android中View的繪製流程》所學內容,我們知道了ViewRootImpl中的這個View對象是一個DecorView實例,而DecorView又繼承ViewGroup,因此通過ViewGroup的dispatchAttachedToWindow(),可以從ViewRootImpl到DecorView再到子View進行遞歸調用,將同一個AttachInfo對象傳給它們。所以我們直接看ViewRootImpl中的這處調用,並且它位於上一節中分析過的performTraversals()中:

private void performTraversals() {
    final View host = mView;
    // ...
    if (mFirst) {
        host.dispatchAttachedToWindow(mAttachInfo, 0);
        // ...
    }
    // ...
    performMeasure();
    // ...
    performLayout();
    // ...
    performDraw();
    // ...
}
  • 第一次初始化時綁定mAttachInfo。
  • 綁定mAttachInfo在measure過程之前。

假如我們在綁定mAttachInfo之後,performMeasure()之前,調用View.post()並在其中獲取View的寬高,那麼能正確獲得寬高嗎?答案是,可以!這涉及到Activity的消息輪詢機制——MessageQueue,爲了不偏離主題,此處直接以結論形式表述,具體分析將在單獨一篇文章中介紹,Activity位於ActivityThread中,ActivityThread有一個Looper成員,這就是主線程的輪詢,它裏面包含了一個MessageQueue,我們的主線程異步消息最終都是被加入到這個MessageQueue中,當Looper輪詢到一個Message時,便執行Message,並且會調用ViewRootImpl.performTraversals(),那麼,當我們在綁定mAttachInfo之後,performMeasure()之前,調用View.post(),此時會創建一個新的Message到消息隊列中,只有當下一次輪詢到該Message時,纔會去執行裏面的獲取寬高操作,而那時,View早已完成了測量工作,並且有具體的寬高值了。

說完了dispatchAttachedToWindow(),我們接着追蹤mAttachInfo的實例化過程:

在ViewRootImpl實例化時進行mAttachInfo的實例化,並且我們可以看到傳入了一個mHandler,這也就是View.post()中使用的Handler對象,我們再來看看這個mHandler的實例化:

final class ViewRootHandler extends Handler {...}

final ViewRootHandler mHandler = new ViewRootHandler();

在聲明時便實例化,Handler的無參構造表示使用當前Looper,ViewRootImpl又是運行於主線程,因此最終到View.post()中,也是由主線程的Looper執行,這也解釋了無論我們的View對象是在子線程還是主線程調用post(),最終都能在主線程執行。

補充說明一點,如果我們使用View.postDelayed(),並在其中使用外部Activity的引用,就存在內存泄露的風險,這涉及到“非靜態內部類持有外部引用導致內存泄漏”這個問題,假如此時我們的Activity對象已經銷燬,但是主線程中延遲執行該消息,其中涉及到對Activity實例的操作,那就會引發很嚴重的問題,解決方法是使用弱引用去創建外部Activity的引用。感興趣的讀者可以參考我的另一篇文章《Java四種引用方式


2. RunQueue機制

由View.post()中的getRunQueue()方法開始入手:

/**
 * Returns the queue of runnable for this view.
 *
 * @return the queue of runnables for this view
 */
private HandlerActionQueue getRunQueue() {
    if (mRunQueue == null) {
        mRunQueue = new HandlerActionQueue();
    }
    return mRunQueue;
}

返回當前View對象的mRunQueue,如果爲空,則實例化。

再看HandlerActionQueue:

/**
 * Class used to enqueue pending work from Views when no Handler is attached.
 *
 * @hide Exposed for test framework only.
 */
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++;
        }
    }

    // ...

    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 HandlerAction(Runnable action, long delay) {
            this.action = action;
            this.delay = delay;
        }
        // ...
    }
}
  • 這個類專用於當View中還未綁定Handler時,將待執行任務加入隊列。
  • post()只做了入列的事,並沒有去執行任務。
  • executeActions()使用傳入的Handler對象去執行隊列中的任務。

那麼我們就去看什麼時候調用的executeActions():

/**
 * @param info the {@link android.view.View.AttachInfo} to associated with
 *        this view
 */
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    mAttachInfo = info;
    // ...
    // Transfer all pending runnables.
    if (mRunQueue != null) {
        mRunQueue.executeActions(info.mHandler);
        mRunQueue = null;
    }
    // ...
}

只有這一處調用,而這一處又剛好是上一節講過的dispatchAttachedToWindow(),由此我們明白了,當ViewRootImpl中進行AttachInfo實例的傳遞時,View中會去遍歷RunQueue,並通過ViewRootHandler對象將消息發送到主線程的消息隊列中,等待輪詢執行。


三、 總結

View.post()內部分爲兩種執行方式:當View中的AttachInfo對象還未初始化時,post()將消息加入一個臨時隊列RunQueue中;當AttachInfo對象初始化之後,會一次性把臨時隊列中的消息全部加入主線程消息隊列中等待執行,並且之後調用post()都會直接把消息加入到主線程消息隊列中等待執行。


四、 參考文獻

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章