一次奇葩問題導(dǎo)致的徹底了解View.post()的底層原理

本篇文章已授權(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)在看來還是很值得我們閱讀這個的源碼的蒸眠。

相關(guān)博客推薦

1.【Android源碼解析】View.post()到底干了啥 - 請叫我大蘇 - 博客園

  1. 通過View.post()獲取View的寬高引發(fā)的兩個問題:1post的Runnable何時被執(zhí)行漾橙,2為何View需要layout兩次;以及發(fā)現(xiàn)Android的一個小bug
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末楞卡,一起剝皮案震驚了整個濱河市霜运,隨后出現(xiàn)的幾起案子脾歇,更是在濱河造成了極大的恐慌,老刑警劉巖淘捡,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件藕各,死亡現(xiàn)場離奇詭異,居然都是意外死亡焦除,警方通過查閱死者的電腦和手機(jī)激况,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來膘魄,“玉大人乌逐,你說我怎么就攤上這事〈雌希” “怎么了浙踢?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長灿渴。 經(jīng)常有香客問我洛波,道長,這世上最難降的妖魔是什么骚露? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任蹬挤,我火速辦了婚禮,結(jié)果婚禮上棘幸,老公的妹妹穿的比我還像新娘闻伶。我一直安慰自己,他們只是感情好够话,可當(dāng)我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著光绕,像睡著了一般女嘲。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上诞帐,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天欣尼,我揣著相機(jī)與錄音,去河邊找鬼停蕉。 笑死愕鼓,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的慧起。 我是一名探鬼主播菇晃,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蚓挤!你這毒婦竟也來了磺送?” 一聲冷哼從身側(cè)響起驻子,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎估灿,沒想到半個月后崇呵,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡馅袁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年域慷,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片汗销。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡犹褒,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出大溜,到底是詐尸還是另有隱情化漆,我是刑警寧澤,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布钦奋,位于F島的核電站座云,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏付材。R本人自食惡果不足惜朦拖,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望厌衔。 院中可真熱鬧璧帝,春花似錦、人聲如沸富寿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽页徐。三九已至苏潜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間变勇,已是汗流浹背恤左。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留搀绣,地道東北人飞袋。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓,卻偏偏與公主長得像链患,于是被迫代替她去往敵國和親巧鸭。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,675評論 2 359