序
之前的文章里寫(xiě)到過(guò),我們?cè)?onCreate() 和 onResume() 方法中無(wú)法獲取 View 的寬高信息,但在平時(shí)開(kāi)發(fā)中,我們經(jīng)常會(huì)用到 View#post 來(lái)進(jìn)行 View 寬高信息的獲取。
那么問(wèn)題就來(lái)了,為什么 View#post 就可以獲取到寬高信息?里邊那個(gè) run() 是在什么時(shí)候執(zhí)行的?具體實(shí)現(xiàn)原理又是什么?
帶著這些疑問(wèn),我最近研究了一下 View#post 的源碼。本來(lái)以為挺簡(jiǎn)單的一個(gè)東西,但是沒(méi)想到坑越挖越深,最過(guò)分的是,不同的版本源碼還不相同,實(shí)現(xiàn)原理也有細(xì)微的差別。集中攻克了一個(gè)周末以后,感覺(jué)大概理解了,索性寫(xiě)下篇博客進(jìn)行記錄備忘。
文章大概分為以下幾個(gè)方面:
- View#post 基本使用
- post() 執(zhí)行過(guò)程以及源碼分析
- post() 中 Runnable#run 執(zhí)行的時(shí)機(jī)
- View#post 整體流程的簡(jiǎn)單總結(jié)
- Android 7.0 里 View#post 的變動(dòng)以及原因
- 致謝
View#post 基本使用
具體代碼如下:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
View view = findViewById(R.id.test);
view.post(new Runnable() {
@Override
public void run() {
// 可以正常獲取到 View 的寬高信息
Log.e("Test", "view.post ---- > " + view.getHeight());
}
});
}
這里我們以 API 26 為例,來(lái)嘗試解答一下這個(gè)問(wèn)題。
實(shí)際上,Android 系統(tǒng)以 API 24 為界,之前之后的版本,對(duì)此處的實(shí)現(xiàn)有細(xì)微的差別,具體的改動(dòng)以及原因在后文會(huì)一一給出分析。
post() 執(zhí)行過(guò)程以及源碼分析
1. View#post 入口
先來(lái)看 View#post 源碼,重點(diǎn)注意注釋:
/**
* Causes the Runnable to be added to the message queue.
* The runnable will be run on the user interface thread.
* 將 Runnable 添加到執(zhí)行隊(duì)列中,其最終會(huì)在 UI 線程中執(zhí)行
*/
public boolean post(Runnable action) {
// AttachInfo 是 View 的內(nèi)部類,用來(lái)存儲(chǔ)一些基本信息
// 此處可以暫時(shí)認(rèn)為 mAttachInfo 為 null
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
// attachInfo 不為空時(shí),轉(zhuǎn)而使用其內(nèi)部的 Handler 對(duì)象操作
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.
// 在我們確定當(dāng)前 Runnable 的目標(biāo)運(yùn)行線程之前,先將其推遲執(zhí)行
// 假設(shè)在 attach 完成之后,此 Runnable 對(duì)象會(huì)被成功的「placed」(暫且翻譯成「放置」)
// 好好理解一下這個(gè)注釋,我們繼續(xù)往下走
getRunQueue().post(action);
return true;
}
首先,明確一點(diǎn):Runnable 會(huì)在 UI 線程中執(zhí)行;
然后,我們來(lái)看一下這個(gè)看上去很重要的 mAttachInfo 是在哪里賦值的:
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;
// Transfer all pending runnables. 轉(zhuǎn)移所有待辦任務(wù)
if (mRunQueue != null) {
mRunQueue.executeActions(info.mHandler);
mRunQueue = null;
}
// 回調(diào)方法
onAttachedToWindow();
}
先不在意除了賦值以外的其他操作,我們繼續(xù)追蹤 dispatchAttachedToWindow 方法,發(fā)現(xiàn)其最初調(diào)用是在 ViewRootImpl#performTraversals 方法。好了,記住這個(gè)結(jié)論,我們先把它放在一旁。
接下來(lái),我們來(lái)看一看這個(gè) getRunQueue().post() 又做了什么:
/**
* 獲取一個(gè) RunQueue 對(duì)象,用來(lái)進(jìn)行 post 操作
* Returns the queue of runnable for this view.
* 注釋是:為當(dāng)前 View 對(duì)象返回一個(gè)執(zhí)行隊(duì)列,記住這個(gè)「當(dāng)前 View 對(duì)象」
*/
private HandlerActionQueue getRunQueue() {
if (mRunQueue == null) {
mRunQueue = new HandlerActionQueue();
}
return mRunQueue;
}
2. HandlerActionQueue 又是個(gè)啥
很明顯,執(zhí)行 post 方法的是 HandlerActionQueue 對(duì)象,那這又是個(gè)什么東西:
/**
* Class used to enqueue pending work from Views when no Handler is attached.
* 此類用于在當(dāng)前 View 沒(méi)有 Handler 依附的時(shí)候,將其待完成的任務(wù)入隊(duì)
*/
public class HandlerActionQueue {
private HandlerAction[] mActions;
private int mCount;
// 這個(gè)就是我們?cè)谕膺呎{(diào)用的 post 方法,最終會(huì)調(diào)用到 postDelayed 方法
public void post(Runnable action) {
postDelayed(action, 0);
}
// 將傳入的 Runnable 對(duì)象存入數(shù)組中,等待調(diào)用
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++;
}
}
// 這里才是真的執(zhí)行方法
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;
}
}
}
通過(guò)查看 HandlerActionQueue 的源碼,我們發(fā)現(xiàn)了一個(gè)問(wèn)題:不同于在 onCreate() 直接獲取 View 的寬高,我們調(diào)用 post 方法,其中的 run 方法并沒(méi)有被馬上執(zhí)行。
這樣就不難解釋為什么用這種方式可以獲取到寬高了。那我們可以猜測(cè)一下,這種情況下,一定是 View 完成測(cè)量后才執(zhí)行了這個(gè)方法,所以我們才可以拿到寬高信息。
事實(shí)上也正是這樣的,那么這個(gè)方法到底是在什么時(shí)候執(zhí)行的呢?很明顯,HandlerActionQueue#executeActions 才是真正完成調(diào)用的方法,那這個(gè)方法又做了些什么工作呢?
根據(jù)代碼可知,該方法接收一個(gè) Handler,然后使用這個(gè) Handler 對(duì)當(dāng)前隊(duì)列中的所有 Runnable 進(jìn)行處理,即 post 到該 Handler 的線程中,按照優(yōu)先級(jí)對(duì)這些 Runnable 依次進(jìn)行處理。
簡(jiǎn)單來(lái)說(shuō),就是傳入的 Handler 決定著這些 Runnable 的執(zhí)行線程。
接下來(lái),我們來(lái)追蹤這個(gè)方法的調(diào)用情況。

我們注意到,對(duì)于該方法出現(xiàn)了兩次調(diào)用,一次在 View#dispatchAttachToWindow(就是我們最開(kāi)始找到的那個(gè)方法),另一次是在 ViewRootImpl#performTraversals。
3. 萬(wàn)惡之源 performTraversals()
很明顯,所有的證據(jù)都指向了 performTraversals ,那么下面我們就來(lái)重點(diǎn)分析一下這個(gè)方法。
如果你了解過(guò) View 的測(cè)繪流程,那你對(duì)這個(gè)方法一定不會(huì)陌生,因?yàn)檫@個(gè)方法就是 View 繪制流程的起點(diǎn)。
private void performTraversals() {
// 此處的 host 是根布局 DecorView,用遞歸的方式一層一層的調(diào)用 dispatchAttachedToWindow
// mAttachInfo 是不是很眼熟,就是最開(kāi)始 View#post 的第一層判斷
// 這個(gè) mAttachInfo 在 ViewRootImpl 的構(gòu)造器中初始化的,其持有 ViewRootImpl 的 Handler 對(duì)象
host.dispatchAttachedToWindow(mAttachInfo, 0);
getRunQueue().executeActions(mAttachInfo.mHandler);
// 繪制流程就從這里開(kāi)始
performMeasure();
performLayout();
performDraw();
}
我們先從 dispatchAttachedToWindow 開(kāi)始,我們之前已經(jīng)看過(guò)這個(gè)方法的源碼了:
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;
// Transfer all pending runnables. 轉(zhuǎn)移所有待辦任務(wù)
if (mRunQueue != null) {
mRunQueue.executeActions(info.mHandler);
mRunQueue = null;
}
// 回調(diào)方法
onAttachedToWindow();
}
現(xiàn)在來(lái)進(jìn)行分析:
- 我們已經(jīng)知道了此方法是從根視圖開(kāi)始遞歸向下調(diào)用的,那么遞歸到最深處,就會(huì)輪到最開(kāi)始我們調(diào)用 post 方法的 View 對(duì)象來(lái)執(zhí)行該方法,也就是該方法內(nèi)的所有屬性,都是我們 findViewById 獲得的那個(gè) View 對(duì)象的屬性;
- 而且我們也知道,第一個(gè)參數(shù) AttachInfo 就是 ViewRootImpl 中初始化的 AttachInfo,它持有當(dāng)前 ViewRootImpl 的 Handler 對(duì)象引用,并將該引用傳給了 executeActions()。此時(shí),我們?cè)賮?lái)回顧一下 executeActions() 方法的作用,傳入的 Handler 決定著隊(duì)列里這些 Runnable 的執(zhí)行線程。
很明顯,此處的 mRunQueue 就是我們最開(kāi)始調(diào)用 post() 時(shí),調(diào)用 View#getRunQueue 返回的那個(gè)對(duì)象,這個(gè)對(duì)象中有準(zhǔn)備獲取View高度的 Runnable 對(duì)象,也就是說(shuō) mRunQueue 通過(guò)調(diào)用 executeActions() 將當(dāng)前 View 的所有 Runnable ,都會(huì)轉(zhuǎn)由 ViewRootImpl 的 Handler 來(lái)處理!而在完成這個(gè)工作之后,當(dāng)前 View 也顯示地將 mRunQueue 置空,因?yàn)樗械拇k任務(wù)都已經(jīng)交給 ViewRootImpl 去處理了。
現(xiàn)在再回過(guò)頭看代碼的注釋,就差不多可以理解了:
// Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
// 所有的 Runnable 都會(huì)在 attach 之后被正確的放到其應(yīng)該運(yùn)行的線程上去
getRunQueue().post(action);
// Transfer all pending runnables.
// 轉(zhuǎn)移所有待辦任務(wù)(到 ViewRootImpl 中進(jìn)行處理)
if (mRunQueue != null) {
mRunQueue.executeActions(info.mHandler);
mRunQueue = null;
}
dispatch 方法執(zhí)行完了,我們繼續(xù)回來(lái)走 performTraversals() ,接下來(lái)一句是:
// 有之前的經(jīng)驗(yàn),我們知道這句話的意思是
// 使用 mAttachInfo.mHandler 來(lái)處理 getRunQueue() 中的 Runnable 任務(wù)
getRunQueue().executeActions(mAttachInfo.mHandler);
要明確的一點(diǎn)是,此時(shí)我們處在 ViewRootImpl 類中,此處的 getRunQueue() 方法有別于 View#post:
// ViewRootImpl#getRunQueue
// 使用 ThreadLocal 來(lái)存儲(chǔ)每個(gè)線程自身的執(zhí)行隊(duì)列 HandlerActionQueue
static HandlerActionQueue getRunQueue() {
// sRunQueues 是 ThreadLocal<HandlerActionQueue> 對(duì)象
HandlerActionQueue rq = sRunQueues.get();
if (rq != null) {
return rq;
}
rq = new HandlerActionQueue();
sRunQueues.set(rq);
return rq;
}
// View#post
// 為當(dāng)前 View 返回一個(gè)執(zhí)行隊(duì)列,但是在 dispatchAttachToWindow 時(shí)轉(zhuǎn)到 UI 線程去
private HandlerActionQueue getRunQueue() {
if (mRunQueue == null) {
mRunQueue = new HandlerActionQueue();
}
return mRunQueue;
}
說(shuō)回 performTraversals() ,很明顯 getRunQueue() 是 UI 線程執(zhí)行隊(duì)列的第一次初始化,也就是說(shuō)當(dāng)前這個(gè)任務(wù)隊(duì)列里并沒(méi)有待執(zhí)行任務(wù)!
但是需要注意的是,當(dāng)前沒(méi)有執(zhí)行任務(wù)(HandlerActionQueue),不代表 Handler 消息隊(duì)列中沒(méi)有消息,這是兩個(gè)概念,需要注意區(qū)分開(kāi)。
總結(jié)一下:
- View#post 方法調(diào)用時(shí),會(huì)為當(dāng)前 View 對(duì)象初始化一個(gè) HandlerActionQueue ,并將 Runnable 入隊(duì)存儲(chǔ);
- 等在 ViewRootImpl#performTraversals 中遞歸調(diào)用到 View#dispatchAttachedToWindow 時(shí),會(huì)將 ViewRootImpl 的 Handler 對(duì)象傳下來(lái),然后通過(guò)這個(gè) Handler 將最初的 Runnable 發(fā)送到 UI 線程(消息隊(duì)列中)等待執(zhí)行,并將 View 的 HandlerActionQueue 對(duì)象置空,方便回收;
- ViewRootImpl#performTraversals 繼續(xù)執(zhí)行,才會(huì)為 UI 線程首次初始化 HandlerActionQueue 對(duì)象,并通過(guò) ThreadLocal 進(jìn)行存儲(chǔ),方便之后的復(fù)用,但需要注意的是,此處初始化的隊(duì)列中是沒(méi)有任何 Runnable 對(duì)象的;
- 然后 ViewRootImpl#performTraversals 繼續(xù)執(zhí)行,開(kāi)始 View 的測(cè)量流程。
View#post 中 Runnable#run 執(zhí)行的時(shí)機(jī)
但現(xiàn)在的問(wèn)題是,無(wú)論怎么說(shuō),HandlerActionQueue#executeActions 都是先于 View 測(cè)繪流程的,為什么在還沒(méi)有完成測(cè)量的時(shí)候,就可以拿到寬高信息?
我們都知道,Android 系統(tǒng)是基于消息機(jī)制運(yùn)行的,所有的事件、行為,都是基于 Handler 消息機(jī)制在運(yùn)行的。所以,當(dāng) ViewRootImpl#performTraversals 在執(zhí)行的時(shí)候,也一定是基于某個(gè)消息的。而且,HandlerActionQueue#executeActions 執(zhí)行的時(shí)候,也只是通過(guò) Handler 將 Runnable post 到了 UI 線程等待執(zhí)行(還記得 View#post 的注釋嗎?)。
不出意外的話,此時(shí) UI 線程正忙著執(zhí)行 ViewRootImpl#performTraversal ,等該方法執(zhí)行完畢,View 已經(jīng)完成了測(cè)量流程,此時(shí)再去執(zhí)行 Runnable#run ,也就自然可以獲取到 View 的寬高信息了。
下面用具體的實(shí)例佐證一下我們的猜想。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final ViewGroup viewGroup = (ViewGroup) getWindow().getDecorView();
// 等待 Add 到父布局中
view = new View(this) {
@Override
protected void onLayout( ... ... ) {
super.onLayout(changed, left, top, right, bottom);
Log.e("Test", "執(zhí)行了onLayout()");
}
};
// 自己聲明的 Handler
mHandler.post(new Runnable() {
@Override
public void run() {
Log.e("Test", "mHandler.post ---- > " + view.getHeight());
}
});
// onCreate() 中 mAttachInfo 還未被賦值,這里會(huì)交給 ViewRootImpl 的 Handler 來(lái)處理
// 即加入消息隊(duì)列,等待執(zhí)行
view.post(new Runnable() {
@Override
public void run() {
Log.e("Test", "view.post ---- > " + view.getHeight());
}
});
viewGroup.addView(view);
}
最終打印日志如下:

也就是說(shuō):
- Handler#post 首先執(zhí)行,其 post 的時(shí)間點(diǎn)在 onCreate() 方法內(nèi),在消息隊(duì)列中的位置一定比 performTraversals() 靠前;
- ViewRootImpl#performTraversal 執(zhí)行,過(guò)程中執(zhí)行了 View#dispatchAttachedToWindow 方法,將最初的 Runnable 入隊(duì)后進(jìn)行測(cè)量流程,完成了 layout 過(guò)程;
- 之后才執(zhí)行了最初的 View#post 方法,也就說(shuō)明了,在 View#dispatchAttachedToWindow 中使用 ViewRootImpl 的 Handler postDelay 的 Runnable 對(duì)象,在主線程消息隊(duì)列中,確實(shí)是排在 ViewRootImpl#performTraversal 之后的
View#post 整體流程的簡(jiǎn)單總結(jié)
最后大概總結(jié)一下:
當(dāng)我們使用 View#post 時(shí),會(huì)有兩種情況:
- 在當(dāng)前 View attach 到 Window 之前,會(huì)自己先維護(hù)一個(gè) HandlerActionQueue 對(duì)象,用來(lái)存儲(chǔ)當(dāng)前的 Runnable 對(duì)象,然后等到 Attach 到 Window 的時(shí)候 (也就是 ViewRootImpl 執(zhí)行到 performTraversal 方法時(shí)) ,會(huì)統(tǒng)一將 Runnable 轉(zhuǎn)交給 ViewRootImpl 處理;
- 而在 View#dispatchAttachedToWindow 時(shí),也會(huì)為當(dāng)前 View 初始化一個(gè) AttachInfo 對(duì)象,該對(duì)象持有 ViewRootImpl 的引用,當(dāng) View 有此對(duì)象后,后續(xù)的所有 Runnable 都將直接交給 ViewRootImpl 處理;
- 而 ViewRootImpl 也會(huì)在執(zhí)行 performTraversal 方法,也會(huì)調(diào)用 ViewRootImpl#getRunQueue ,利用 ThreadLocal 來(lái)為主線程維護(hù)一個(gè) HandlerActionQueue 對(duì)象,至此,ViewRootImpl 內(nèi)部都將使用該隊(duì)列來(lái)進(jìn)行 Runnable 任務(wù)的短期維護(hù);
- 但需要注意的是,各個(gè) View 調(diào)用的 post 方法,仍然是由各自的 HandlerActionQueue 對(duì)象來(lái)入隊(duì)任務(wù)的,然后在 View#dispatchAttachedToWindow 的時(shí)候轉(zhuǎn)移給 ViewRootImpl 去處理。
Android 7.0 里 View#post 的變動(dòng)以及原因
View#post 說(shuō)到這里大概就差不多了,文章開(kāi)篇的時(shí)候說(shuō)到:
Android 系統(tǒng)以 API 24 為界,之前之后的版本,對(duì)此處的實(shí)現(xiàn)有細(xì)微的差別
下面來(lái)簡(jiǎn)單對(duì)比一下具體的差別,順便分析一下具體為什么要這樣改動(dòng)。
實(shí)際上這個(gè)方法的改動(dòng)主要是為了解決一個(gè) bug,這個(gè) bug 就是:在 View 被 attach 到 window 之前,從子線程調(diào)用的 View#post ,永遠(yuǎn)無(wú)法得到執(zhí)行。
具體原因,我們來(lái)看一下 API23 版本的 View#post,就大概都明白了:
// Android API23 View#post
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
return attachInfo.mHandler.post(action);
}
// Assume that post will succeed later
// 注意此處,不同于我們之前介紹的,這里是直接使用 ViewRootImpl#getRunQueue 來(lái)入隊(duì)任務(wù)的
ViewRootImpl.getRunQueue().post(action);
return true;
}
我們可以看到,不同于我們之前介紹的,API23 版本中,View#post 在沒(méi)有 attach 到 window 之前,也就是 mAttachInfo 是 null 的時(shí)候,不是自己維護(hù)任務(wù)隊(duì)列,而是直接使用 ViewRootImpl#getRunQueue 來(lái)入隊(duì)任務(wù)的。
再來(lái)看一下 ViewRootImpl#getRunQueue 方法,我們就會(huì)發(fā)現(xiàn)問(wèn)題出在哪里了:
static final ThreadLocal<RunQueue> sRunQueues = new ThreadLocal<RunQueue>();
static RunQueue getRunQueue() {
RunQueue rq = sRunQueues.get();
if (rq != null) {
return rq;
}
rq = new RunQueue();
sRunQueues.set(rq);
return rq;
}
沒(méi)錯(cuò),這個(gè)隊(duì)列的保存與獲取,是通過(guò)以線程為 key 值來(lái)存取對(duì)象 ThreadLocal 來(lái)維護(hù)的。而在這個(gè)版本的源碼中,executeActions() 方法的執(zhí)行,只有一次調(diào)用,那就是 ViewRootImpl#performTraversal 中(感興趣的可以去 23 版本的源碼中查看,這里就不貼圖了),與此同時(shí),該方法肯定是執(zhí)行在主線程中的。
現(xiàn)在的問(wèn)題就變成了:我在子線程中 post 了一個(gè) runnable,并且系統(tǒng)以該子線程為 key 將隊(duì)列存了起來(lái)等待執(zhí)行;但是在具體執(zhí)行的時(shí)候,系統(tǒng)卻是去主線程中尋找待執(zhí)行的 Runnable,那么當(dāng)然是永遠(yuǎn)都得不到執(zhí)行的了。
而在具體 attach 到 window 之后,View 的 mAttachInfo 持有 ViewRootImpl 引用,會(huì)直接將所有的 Runnable 轉(zhuǎn)交給 ViewRootImpl 的 Handler 處理,也就都能得到妥善處理,就與線程無(wú)關(guān)了。
除此以外,ViewRootImpl 使用 ThreadLocal 來(lái)存儲(chǔ)隊(duì)列信息,在某些情境下,還會(huì)導(dǎo)致內(nèi)存泄漏。詳細(xì)信息可以參考:https://blog.csdn.net/a740169405/article/details/69668957
所以,Google 工程師為了解決這兩個(gè)問(wèn)題(內(nèi)存泄漏的問(wèn)題更嚴(yán)重一些),就在 View#post 方法中使用 View 對(duì)象來(lái)進(jìn)行隊(duì)列的存儲(chǔ),然后在 attach 到 window 的時(shí)候,通過(guò)持有 ViewRootImpl 引用的 AttachInfo 對(duì)象直接將 View 對(duì)象的 Runnable 處理掉,就完美解決了這些問(wèn)題。
致謝
下邊是自己研究的時(shí)候具體參考過(guò)的文章,給各位前輩加個(gè)雞腿: