本篇文章已授權(quán)微信公眾號 stormjun94 (Android 技術(shù)人員)獨家發(fā)布
前言
原來一直以為View.post()就是簡單的利用Handler發(fā)送出去,沒有什么特殊的地方梭稚,沒有必要糾結(jié)具體的實現(xiàn)砸抛,但是最近遇到一個問題,發(fā)現(xiàn)一個ListView中的一個View無法點擊捆等,特別奇怪顯示正常降铸,卻無法點擊住拭,我跟蹤源碼最后發(fā)現(xiàn)一直執(zhí)行到了ACTION_UP,也就是說這個事件是被這個View消費了柱嫌,但是最后執(zhí)行performClick()
方法的時候锋恬,居然沒有回調(diào)onClick的監(jiān)聽回調(diào)。
switch (action) {
case MotionEvent.ACTION_UP:
{
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
//源碼執(zhí)行到了這個地方编丘,但是沒有回調(diào)onClick()
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
}
通過上面的源碼与学,可以看到源碼執(zhí)行到了performClick的地方,但是最終沒有回調(diào)onClick(onClickListener是存在的)嘉抓,這就讓我不能理解了索守,唯一疑惑的地方就是這里,使用了post()方法抑片,將performClick這個runnable發(fā)送了出去卵佛,所以讓我糾結(jié)于是不是可以在View.post()里面研究一下。(本篇文章主要分析View.post的底層源碼,關(guān)于這個問題截汪,最后查源碼疾牲,解決了,是ListView的itemView和viewType沒有正確對應(yīng)上的原因衙解,但是能夠正常顯示阳柔,卻不能點擊,這里具體就不解釋蚓峦,后面如果有時間可以專門分析一下)
源碼解析
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;
}
可以看到這里枫匾,是存在兩種情況的,一種attachInfo不為null的情況拟淮,一種是attachInfo為null的情況干茉。這里就有幾個問題了:
1.attachInfo是什么?
2.這兩種情況有什么區(qū)別很泊?
3.第二種沒有直接通過Handler發(fā)出去角虫,怎么執(zhí)行的?
我上面的問題委造,是最后的查明原因的關(guān)鍵點戳鹅,就是一個在ListView正常顯示的View,但是它的attachInfo==null
。那么就來一個問題一個問題解決吧昏兆。
1.AttachInfo是什么枫虏?
既然AttachInfo==null,那么我們肯定要追問爬虱,attachInfo什么時候賦值的隶债,什么時候置空的,所以在View.java中全局搜索mAttachInfo =
注意后面要帶上一個空格跑筝,這也算一個看源碼的小技巧吧死讹,這樣可以過濾很多無用的代碼。
可以看到整個代碼里面一共就兩處曲梗,而且對應(yīng)的正好就是一處賦值赞警,一處置空。
/**
* @param info the {@link android.view.View.AttachInfo} to associated with
* this view
*/
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
mAttachInfo = info;
...
}
void dispatchDetachedFromWindow() {
...
mAttachInfo = null;
...
}
可以看到方法名也很對稱虏两,一個對應(yīng)于分發(fā)綁定愧旦,一個對應(yīng)于分發(fā)解綁。所以看多了源碼的應(yīng)該能意識到這是Google常用的一種向下分發(fā)的機(jī)制碘举,那么我們就要找到源頭忘瓦。
全局搜索dispatchAttachedToWindow
,這里直接ctrl點擊是沒辦法查看引用,所以這里還是要利用我們IDE的另一個功能全局查找耕皮。
可以看到這里除了一些特殊的像RecyclerView這種特殊組件境蜕,一眼就可以看到一個很特殊的類ViewRootImpl.java,這不就是我們所有布局的最上層布局嗎,那肯定就是它了凌停。
private void performTraversals() {
host.dispatchAttachedToWindow(mAttachInfo, 0);
}
這里就很清楚了粱年,看到了我們很熟悉的一個方法,performTraversals()
,這不就是我們頁面繪制的起點嗎罚拟,所以這里可以得出一條結(jié)論
在頁面繪制的起點的時候台诗,會通過分發(fā)的方式,將頂層的mAttachInfo分發(fā)給子View赐俗。而這個mAttachInfo是在ViewRootImpl初始化的時候構(gòu)造函數(shù)中new出來的拉队。
public ViewRootImpl(Context context, Display display) {
...
mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this,
context);
...
}
可以看到,這里里面保存了很多基礎(chǔ)信息阻逮,包括后面要使用的Handler對象粱快。所以到這里我們第一個問題解決了。
2.這兩種情況有什么區(qū)別叔扼?
通過上面的分析我們知道了事哭,一個View在繪制到頁面上后,都會被attach和當(dāng)前頁面綁定瓜富,對應(yīng)的綁定的信息里面mAttachInfo有Handler對象(主線程Handler)鳍咱,還有其他對象,這也是為什么我們在自線程可以利用View.post執(zhí)行UI操作的原因与柑,因為要執(zhí)行的操作會通過View內(nèi)部的主線程Handler發(fā)到主線程執(zhí)行谤辜。讓我們再來看一下兩種不同的場景。
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;
}
場景一
如果一個正常的已經(jīng)繪制到頁面上的View仅胞,對應(yīng)的mAttachInfo不會為null每辟,所以當(dāng)我們調(diào)用View.post的時候,會通過View內(nèi)部的Handler對象干旧,將runnable發(fā)送到主線程消息隊列中執(zhí)行渠欺。
場景二
對于場景二可能我們會很疑惑,沒有見到執(zhí)行的操作椎眯, 具體的場景這里舉個例子挠将,我們都知道,在Activity的onCreate
方法中编整,我們可以通過View.post()拿到我們View的寬高舔稀,這是為什么呢?其實就是這個這里的場景二有關(guān)掌测。
首先我們知道内贮,在onCreate
方法中,View還沒有執(zhí)行頁面繪制的三大操作的渐裸,這也是我們?yōu)槭裁床荒茉趏nCreate拿到寬高的原因骑素,因為頁面的繪制流程的起點performTraversals()
是在Activity的onResume
方法之后執(zhí)行的叉橱,所以這時候糟把,當(dāng)我們在onCreate
方法中,執(zhí)行View.post方法被因,根據(jù)前面的分析贡翘,沒有執(zhí)行performTraversals()
著角,所以沒有分發(fā)attach事富,所以mAttachInfo為null技俐,這時候,就會執(zhí)行我們的場景二统台。
private HandlerActionQueue getRunQueue() {
if (mRunQueue == null) {
mRunQueue = new HandlerActionQueue();
}
return mRunQueue;
}
public class HandlerActionQueue {
//數(shù)組
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];
}
//數(shù)組追加的工具類
mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);
mCount++;
}
}
}
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);
}
}
我們這里可以看到雕擂,其實很簡單,就是將我們要執(zhí)行的runnable利用一個數(shù)組保存了起來贱勃,也就是說當(dāng)我們在onCreate中執(zhí)行View.post的時候捂刺,并沒有立即執(zhí)行我們要執(zhí)行的方法,而是被保存了起來募寨。那么這里場景二和場景一的區(qū)別其實也是很明顯了,那么就到了最后一個問題森缠。
3.第二種沒有直接通過Handler發(fā)出去拔鹰,怎么執(zhí)行的?
通過上面我們知道贵涵,我們沒執(zhí)行的Runnable被保存了起來列肢,在上面提到的HandlerActionQueue類中,我們找尋相關(guān)的方法宾茂,可以看到瓷马,一個關(guān)鍵方法(其實類很短,很好找跨晴,而且方法名也很直接)
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;
}
}
可以看到這里有個很明顯的執(zhí)行的方法欧聘,通過傳入的Handler對象,遍歷保存的數(shù)組端盆,然后再將保存的runnable再通過handler發(fā)出去怀骤,傳入到Handler對應(yīng)的消息隊列中。
這次先按住ctrl焕妙,看一下使用的類蒋伦,發(fā)現(xiàn)有調(diào)用,一共有兩處焚鹊,一處和ViewRootIml有關(guān)一處和View有關(guān)痕届。
起初我沒有多想,直接去看ViewRootIml的源碼直接以為在這里就處理了,后來感謝@神天圣地的提醒研叫,這里應(yīng)該是在View的
dispatchAttachedToWindow
分發(fā)的時候锤窑,才處理的,因為HandlerActionQueue對象是不一樣的蓝撇。
private void performTraversals() {
...
// Execute enqueued actions on every traversal in case a detached view enqueued an action
getRunQueue().executeActions(mAttachInfo.mHandler);
...
//執(zhí)行繪制三大步驟
}
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
// Transfer all pending runnables.
if (mRunQueue != null) {
mRunQueue.executeActions(info.mHandler);
mRunQueue = null;
}
}
可以看到果复,這里又一次將mAttachInfo
中的Handler傳入,然后便會把我們通過View.post保存的runnable再發(fā)送到主線程的消息隊列中渤昌,等待執(zhí)行虽抄,由于后面里面會執(zhí)行第一次到繪制步驟,所有独柑,當(dāng)執(zhí)行到我們的runnable的時候迈窟,肯定就可以拿到View的寬高了。
特別注意
通過View.post拿到的寬高一定是真實的嗎忌栅?
這個不一定车酣,上面也提到了,這里只是第一次繪制的步驟索绪,如果像RelativeLayout湖员,或者其他特殊的View,再某些特殊情況下瑞驱,會執(zhí)行多次繪制娘摔,如果我們的runnable在第一次繪制結(jié)束后就里面執(zhí)行,那么就拿到的只是第一次繪制結(jié)束后的寬高唤反。當(dāng)然凳寺,絕大部分拿到的是真實的寬高。
View的attach和detach一定只有ViewRootImpl執(zhí)行嗎彤侍?
不一定肠缨,例如一些特殊的存在組件復(fù)用的RecyclerView,都存在自己定制的attach和detach操作盏阶,具體可以看我寫的關(guān)于RecyclerView的系列博客晒奕,可以讓你深層次的了解RecyclerView。
總結(jié)
通過這次的分析名斟,看似簡單的View.post的分析過程其實涉及到了Handler機(jī)制吴汪,View的繪制流程等很多重要的知識點,現(xiàn)在看來還是很值得我們閱讀這個的源碼的蒸眠。