一、 摘要
介紹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()都會直接把消息加入到主線程消息隊列中等待執行。