源碼詳解Android中View.post()用法-創(chuàng)新互聯(lián)

emmm,大伙都知道,子線程是不能進行 UI 操作的,或者很多場景下,一些操作需要延遲執(zhí)行,這些都可以通過 Handler 來解決。但說實話,實在是太懶了,總感覺寫 Handler 太麻煩了,一不小心又很容易寫出內(nèi)存泄漏的代碼來,所以為了偷懶,我就經(jīng)常用 View.post() or View.postDelay() 來代替 Handler 使用。

創(chuàng)新互聯(lián)建站專業(yè)為企業(yè)提供安福網(wǎng)站建設(shè)、安福做網(wǎng)站、安福網(wǎng)站設(shè)計、安福網(wǎng)站制作等企業(yè)網(wǎng)站建設(shè)、網(wǎng)頁設(shè)計與制作、安福企業(yè)網(wǎng)站模板建站服務,十年安福做網(wǎng)站經(jīng)驗,不只是建網(wǎng)站,更提供有價值的思路和整體網(wǎng)絡服務。

但用多了,總有點心虛,View.post() 會不會有什么隱藏的問題?所以趁有點空余時間,這段時間就來梳理一下,View.post() 原理到底是什么,內(nèi)部都做了啥事。

提問

開始看源碼前,先提幾個問題,帶著問題去看源碼應該會比較有效率,防止閱讀源碼過程中,陷得太深,跟得太偏了。

Q1: 為什么 View.post() 的操作是可以對 UI 進行操作的呢,即使是在子線程中調(diào)用 View.post()?

Q2:網(wǎng)上都說 View.post() 中的操作執(zhí)行時,View 的寬高已經(jīng)計算完畢,所以經(jīng)??匆娫?Activity 的 onCreate() 里調(diào)用 View.post() 來解決獲取 View 寬高為0的問題,為什么可以這樣做呢?

Q3:用 View.postDelay() 有可能導致內(nèi)存泄漏么?

ps:本篇分析的源碼基于 andoird-25 版本,版本不一樣源碼可能有些區(qū)別,大伙自己過源碼時可以注意一下。另,下面分析過程有點長,慢慢看哈。

源碼分析

好了,就帶著這幾個問題來跟著源碼走吧。其實,這些問題大伙心里應該都有數(shù)了,看源碼也就是為了驗證心里的想法。第一個問題,之所以可以對 UI 進行操作,那內(nèi)部肯定也是通過 Handler 來實現(xiàn)了,所以看源碼的時候就可以看看內(nèi)部是如何對 Handler 進行封裝的。而至于剩下的問題,那就在看源碼過程中順帶看看能否找到答案吧。

View.post()

View.post() 方法很簡單,代碼很少。那我們就一行行的來看。

如果 mAttachInfo 不為空,那就調(diào)用 mAttachInfo.mHanlder.post() 方法,如果為空,則調(diào)用 getRunQueue().post() 方法。

那就找一下,mAttachInfo 是什么時候賦值的,可以借助 AS 的 Ctrl + F 查找功能,過濾一下 mAttachInfo =,注意 = 號后面還有一個空格,否則你查找的時候會發(fā)現(xiàn)全文有兩百多處匹配到。我們只關(guān)注它是什么時候賦值的,使用的場景就不管了,所以過濾條件可以細一點。這樣一來,全文就只有兩處匹配:

源碼詳解Android中View.post()用法

源碼詳解Android中View.post()用法

一處賦值,一處置空,剛好又是在對應的一個生命周期里:

dispatchAttachedToWindow() 下文簡稱 attachedToWindow


dispatchDetachedFromWindow() 下文簡稱 detachedFromWindow。

所以,如果 mAttachInfo 不為空的時候,走的就是 Handler 的 post(),也就是 View.post() 在這種場景下,實際上就是調(diào)用的 Handler.post(),接下去就是搞清楚一點,這個 Handler 是哪里的 Handler,在哪里初始化等等,但這點可以先暫時放一邊,因為 mAttachInfo 是在 attachedToWindow 時才賦值的,所以接下去關(guān)鍵的一點是搞懂 attachedToWindow 到 detachedFromWindow 這個生命周期分別在什么時候在哪里被調(diào)用了。

雖然我們現(xiàn)在還不清楚,attachedToWindow 到底是什么時候被調(diào)用的,但看到這里我們至少清楚一點,在 Activity 的 onCreate() 期間,這個 View 的 attachedToWindow 應該是還沒有被調(diào)用,也就是 mAttachInfo 這時候還是為空,但我們在 onCreate() 里執(zhí)行 View.post() 里的操作仍然可以保證是在 View 寬高計算完畢的,也就是開頭的問題 Q2,那么這點的原理顯然就是在另一個 return 那邊的方法里了:getRunQueue().post()。

那么,我們就先解決 Q2 吧,為什么 View.post() 可以保證操作是在 View 寬高計算完畢之后呢?跟進 getRunQueue() 看看:

getRunQueue().post()

所以調(diào)用的其實是 HandlerActionQueue.post() 方法,那么我們再繼續(xù)跟進去看看:

源碼詳解Android中View.post()用法

post(Runnable) 方法內(nèi)部調(diào)用了 postDelayed(Runnable, long),postDelayed() 內(nèi)部則是將 Runnable 和 long 作為參數(shù)創(chuàng)建一個 HandlerAction 對象,然后添加到 mActions 數(shù)組里。下面先看看 HandlerAction:

源碼詳解Android中View.post()用法

很簡單的數(shù)據(jù)結(jié)構(gòu),就一個 Runnable 成員變量和一個 long 成員變量。這個類作用可以理解為用于包裝 View.post(Runnable) 傳入的 Runnable 操作的,當然因為還有 View.postDelay() ,所以就還需要一個 long 類型的變量來保存延遲的時間了,這樣一來這個數(shù)據(jù)結(jié)構(gòu)就不難理解了吧。

所以,我們調(diào)用 View.post(Runnable) 傳進去的 Runnable 操作,在傳到 HandlerActionQueue 里會先經(jīng)過 HandlerAction 包裝一下,然后再緩存起來。至于緩存的原理,HandlerActionQueue 是通過一個默認大小為4的數(shù)組保存這些 Runnable 操作的,當然,如果數(shù)組不夠用時,就會通過 GrowingArrayUtils 來擴充數(shù)組,具體算法就不繼續(xù)看下去了,不然越來越偏。

到這里,我們先來梳理下:

當我們在 Activity 的 onCreate() 里執(zhí)行 View.post(Runnable) 時,因為這時候 View 還沒有 attachedToWindow,所以這些 Runnable 操作其實并沒有被執(zhí)行,而是先通過 HandlerActionQueue 緩存起來。

那么到什么時候這些 Runnable 才會被執(zhí)行呢?我們可以看看 HandlerActionQueue 這個類,它的代碼不多,里面有個 executeActions() 方法,看命名就知道,這方法是用來執(zhí)行這些被緩存起來的 Runnable 操作的:

源碼詳解Android中View.post()用法

哇,看到重量級的人物了:Handler。看來被緩存起來沒有執(zhí)行的 Runnable 最后也還是通過 Hnadler 來執(zhí)行的。那么,這個 Handler 又是哪里的呢?看來關(guān)鍵點還是這個方法在哪里被調(diào)用了,那就找找看:

借助 AS 的 Ctrl + Alt + F7 快捷鍵,可以查找 SDK 里的某個方法在哪些地方被調(diào)用了。

源碼詳解Android中View.post()用法

很好,找到了,而且只找到這個地方。其實,這個快捷鍵有時并沒有辦法找到一些方法被調(diào)用的地方,這也是源碼閱讀過程中令人頭疼的一點,因為沒法找到這些方法到底在哪些地方被調(diào)用了,所以很難把流程梳理下來。如果方法是私有的,那很好辦,就用 Ctrl + F 在這個類里找一下就可以,如果匹配結(jié)果太多,那就像開頭那樣把過濾條件詳細一點。如果方法不是私有的,那真的就很難辦了,這也是一開始找到 dispatchAttachedToWindow() 后為什么不繼續(xù)跟蹤下去轉(zhuǎn)而來分析Q2:getRunQueue() 的原因,因為用 AS 找不到 dispatchAttachedToWindow() 到底在哪些地方被誰調(diào)用了。哇,好像又扯遠了,回歸正題回歸正題。

emmm,看來這里也繞回來了,dispatchAttachedToWindow() 看來是個關(guān)鍵的節(jié)點。

那到這里,我們再次來梳理一下:

我們使用 View.post() 時,其實內(nèi)部它自己分了兩種情況處理,當 View 還沒有 attachedToWindow 時,通過 View.post(Runnable) 傳進來的 Runnable 操作都先被緩存在 HandlerActionQueue,然后等 View 的 dispatchAttachedToWindow() 被調(diào)用時,就通過 mAttachInfo.mHandler 來執(zhí)行這些被緩存起來的 Runnable 操作。從這以后到 View 被 detachedFromWindow 這段期間,如果再次調(diào)用 View.post(Runnable) 的話,那么這些 Runnable 不用再緩存了,而是直接交給 mAttachInfo.mHanlder 來執(zhí)行。

以上,就是到目前我們所能得知的信息。這樣一來,Q2 是不是漸漸有一些頭緒了:View.post(Runnable) 的操作之所以可以保證肯定是在 View 寬高計算完畢之后才執(zhí)行的,是因為這些 Runnable 操作只有在 View 的 attachedToWindow 到 detachedFromWiondow 這期間才會被執(zhí)行。

那么,接下去就還剩兩個關(guān)鍵點需要搞清楚了:

dispatchAttachedToWindow() 是什么時候被調(diào)用的? mAttachInfo 是在哪里初始化的? dispatchAttachedToWindow() & mAttachInfo

只借助 AS 的話,很難找到 dispatchAttachedToWindow() 到底在哪些地方被調(diào)用。所以,到這里,我又借助了 Source Insight 軟件。


很棒!找到了四個被調(diào)用的地方,三個在 ViewGroup 里,一個在 ViewRootImpl.performTraversals() 里。找到了就好,接下去繼續(xù)用 AS 來分析吧,Source Insight 用不習慣,不過分析源碼時確實可以結(jié)合這兩個軟件。

源碼詳解Android中View.post()用法

哇,懵逼,完全懵逼。我就想看個 View.post(),結(jié)果跟著跟著,跟到這里來了。ViewRootImpl 我在分析Android KeyEvent 點擊事件分發(fā)處理流程時短暫接觸過,但這次顯然比上次還需要更深入去接觸,哎,力不從心啊。

我只能跟大伙肯定的是,mView 是 Activity 的 DecorView。咦~,等等,這樣看來 ViewRootImpl 是調(diào)用的 DecorView 的 dispatchAttachedToWindow() ,但我們在使用 View.post() 時,這個 View 可以是任意 View,并不是非得用 DecorView 吧。哈哈哈,這是不是代表著我們找錯地方了?不管了,我們就去其他三個被調(diào)用的地方: ViewGroup 里看看吧:

源碼詳解Android中View.post()用法

addViewInner() 是 ViewGroup 在添加子 View 時的內(nèi)部邏輯,也就是說當 ViewGroup addView() 時,如果 mAttachInfo 不為空,就都會去調(diào)用子 View 的 dispatchAttachedToWindow(),并將自己的 mAttachInfo 傳進去。還記得 View 的 dispatchAttachedToWindow() 這個方法么:

源碼詳解Android中View.post()用法

mAttachInfo 唯一被賦值的地方也就是在這里,那么也就是說,子 View 的 mAttachInfo 其實跟父控件 ViewGroup 里的 mAttachInfo 是同一個的。那么,關(guān)鍵點還是這個 mAttachInfo 什么時候才不為空,也就是說 ViewGroup 在 addViewInner() 時,傳進去的 mAttachInfo 是在哪被賦值的呢?我們來找找看:

咦,利用 AS 的 Ctrl + 左鍵 怎么找不到 mAttachInfo 被定義的地方呢,不管了,那我們用 Ctrl + F 搜索一下在 ViewGroup 類里 mAttachInfo 被賦值的地方好了:

源碼詳解Android中View.post()用法

咦,怎么一個地方也沒有。難道說,這個 mAttachInfo 是父類 View 定義的變量么,既然 AS 找不到,我們換 Source Insight 試試:

源碼詳解Android中View.post()用法

還真的是,ViewGroup 是繼承的 View,并且處于同一個包里,所以可以直接使用該變量,那這樣一來,我們豈不是又繞回來了。前面說過,dispatchAttachedToWindow() 在 ViewGroup 里有三處調(diào)用的地方,既然 addViewInner() 這里的看不出什么,那去另外兩個地方看看:

源碼詳解Android中View.post()用法

剩下的兩個地方就都是在 ViewGroup 重寫的 dispatchAttachedToWindow() 方法里了,這代碼也很好理解,在該方法被調(diào)用的時候,先執(zhí)行 super 也就是 View 的 dispatchAttachedToWindow() 方法,還沒忘記吧,mAttachInfo 就是在這里被賦值的。然后再遍歷子 View,分別調(diào)用子 View 的 dispatchAttachedToWindow() 方法,并將 mAttachInfo 作為參數(shù)傳遞進去,這樣一來,子 View 的 mAttachInfo 也都被賦值了。

但這樣一來,我們就繞進死胡同了。

我們還是先來梳理一下吧:

目前,我們知道,View.post(Runnable) 的這些 Runnable 操作,在 View 被 attachedToWindow 之前會先緩存下來,然后在 dispatchAttachedToWindow() 被調(diào)用時,就將這些緩存下來的 Runnable 通過 mAttachInfo 的 mHandler 來執(zhí)行。在這之后再調(diào)用 View.post(Runnable) 的話,這些 Runnable 操作就不用再被緩存了,而是直接交由 mAttachInfo 的 mHandler 來執(zhí)行。

所以,我們得搞清楚 dispatchAttachedToWindow() 在什么時候被調(diào)用,以及 mAttachInfo 是在哪被初始化的,因為需要知道它的變量如 mHandler 都是些什么以及驗證 mHandler 執(zhí)行這些 Runnable 操作是在 measure 之后的,這樣才能保證此時的寬高不為0。

然后,我們在跟蹤 dispatchAttachedToWindow() 被調(diào)用的地方時,跟到了 ViewGroup 的 addViewInner() 里。在這里我們得到的信息是如果 mAttachInfo 不為空時,會直接調(diào)用子 View 的 dispatchAttachedToWindow(),這樣新 add 進來的子 View 的 mAttachInfo 就會被賦值了。但 ViewGroup 的 mAttachInfo 是父類 View 的變量,所以為不為空的關(guān)鍵還是回到了 dispatchAttachedToWindow() 被調(diào)用的時機。

我們還跟到了 ViewGroup 重寫的 dispatchAttachedToWindow() 方法里,但顯然,ViewGroup 重寫這個方法只是為了將 attachedToWindow 這個事件通知給它所有的子 View。

所以,最后,我們能得到的結(jié)論就是,我們還得再回去 ViewRootImpl 里,dispatchAttachedToWindow() 被調(diào)用的地方,除了 ViewRootImpl,我們都分析過了,得不到什么信息,只剩最后 ViewRootImpl 這里了,所以關(guān)鍵點肯定在這里。看來這次,不行也得上了。

ViewRootImpl.performTraversals()

源碼詳解Android中View.post()用法

這方法代碼有八百多行??!不過,我們只關(guān)注我們需要的點就行,這樣一省略無關(guān)代碼來看,是不是感覺代碼就簡單得多了。

mFirst 初始化為 true,全文只有一處賦值,所以 if(mFirst) 塊里的代碼只會執(zhí)行一次。我對 ViewRootImpl 不是很懂,performTraversals() 這個方法應該是通知 Activity 的 View 樹開始測量、布局、繪制。而 DevorView 是 Activity 視圖的根布局、View 樹的起點,它繼承 FrameLayout,所以也是個 ViewGroup,而我們之前對 ViewGroup 的 dispatchAttachedToWindow() 分析過了吧,在這個方法里會將 mAttachInfo 傳給所有子 View。也就是說,在 Activity 首次進行 View 樹的遍歷繪制時,ViewRootImpl 會將自己的 mAttachInfo 通過根布局 DecorView 傳遞給所有的子 View 。

那么,我們就來看看 ViewRootImpl 的 mAttachInfo 什么時候初始化的吧:

源碼詳解Android中View.post()用法

在構(gòu)造函數(shù)里對 mAttachInfo 進行初始化,傳入了很多參數(shù),我們關(guān)注的應該是 mHandler 這個變量,所以看看這個變量定義:

源碼詳解Android中View.post()用法

終于找到 new Handler() 的地方了,至于這個自定義的 Handler 類做了啥,我們不關(guān)心,反正通過 post() 方式執(zhí)行的操作跟它自定義的東西也沒有多大關(guān)系。我們關(guān)心的是在哪 new 了這個 Handler。因為每個 Handler 在 new 的時候都會綁定一個 Looper,這里 new 的時候是無參構(gòu)造函數(shù),那默認綁定的就是當前線程的 Looper,而這句 new 代碼是在主線程中執(zhí)行的,所以這個 Handler 綁定的也就是主線程的 Looper。至于這些的原理,就涉及到 Handler 的源碼和 ThreadLocal 的原理了,就不繼續(xù)跟進了,太偏了,大伙清楚結(jié)論這點就好。

這也就是為什么 View.post(Runnable) 的操作可以更新 UI 的原因,因為這些 Runnable 操作都通過 ViewRootImpl 的 mHandler 切到主線程來執(zhí)行了。

這樣 Q1 就搞定了,終于搞定了一個問題,不容易啊,本來以為很簡單的來著。

跟到 ViewRootImpl 這里應該就可以停住了。至于 ViewRootImpl 跟 Activity 有什么關(guān)系、什么時候被實例化的、跟 DecroView 如何綁定的就不跟進了,因為我也還不是很懂,感興趣的可以自己去看看,我在末尾會給一些參考博客。

至此,我們清楚了 mAttachInfo 的由來,也知道了 mAttachInfo.mHandler,還知道在 Activity 首次遍歷 View 樹進行測量、繪制時會通過 DecorView 的 dispatchAttachedToWindow() 將 ViewRootImpl 的 mAttachInfo 傳遞給所有子 View,并通知所有調(diào)用 View.post(Runnable) 被緩存起來的 Runnable 操作可以執(zhí)行了。

但不知道大伙會不會跟我一樣還有一點疑問:看網(wǎng)上對 ViewRootImpl.performTraversals() 的分析:遍歷 View 樹進行測量、布局、繪制操作的代碼顯然是在調(diào)用了 dispatchAttachedToWindow() 之后才執(zhí)行,那這樣一來是如何保證 View.post(Runnable) 的 Runnable 操作可以獲取到 View 的寬高呢?明明測量的代碼 performMeasure() 是在 dispatchAttachedToWindow() 后面才執(zhí)行。

源碼詳解Android中View.post()用法

我在這里卡了很久,一直沒想明白。我甚至以為是 PhoneWindow 在加載 layout 布局到 DecorView 時就進行了測量的操作,所以一直跟,跟到 LayoutInflater.inflate(),跟到了 ViewGroup.addView(),最后發(fā)現(xiàn)跟測量有關(guān)的操作最終都又繞回到 ViewRootImpl 中去了。

原來是自己火候不夠,對 Android 的消息機制還不大理解,這篇博客前前后后寫了一兩個禮拜,就是在不斷查缺補漏,學習、理解相關(guān)的知識點。

大概的來講,就是我們的 app 都是基于消息驅(qū)動機制來運行的,主線程的 Looper 會無限的循環(huán),不斷的從 MessageQueue 里取出 Message 來執(zhí)行,當一個 Message 執(zhí)行完后才會去取下一個 Message 來執(zhí)行。而 Handler 則是用于將 Message 發(fā)送到 MessageQueue 里,等輪到 Message 執(zhí)行時,又通過 Handler 發(fā)送到 Target 去執(zhí)行,等執(zhí)行完再取下一個 Message,如此循環(huán)下去。

清楚了這點后,我們再回過頭來看看:

performTraversals() 會先執(zhí)行 dispatchAttachedToWindow(),這時候所有子 View 通過 View.post(Runnable) 緩存起來的 Runnable 操作就都會通過 mAttachInfo.mHandler 的 post() 方法將這些 Runnable 封裝到 Message 里發(fā)送到 MessageQueue 里。mHandler 我們上面也分析過了,綁定的是主線程的 Looper,所以這些 Runnable 其實都是發(fā)送到主線程的 MessageQueue 里排隊,等待執(zhí)行。然后 performTraversals() 繼續(xù)往下工作,相繼執(zhí)行 performMeasure(),performLayout() 等操作。等全部執(zhí)行完后,表示這個 Message 已經(jīng)處理完畢,所以 Looper 才會去取下一個 Message,這時候,才有可能輪到這些 Runnable 執(zhí)行。所以,這些 Runnable 操作也就肯定會在 performMeasure() 操作之后才執(zhí)行,寬高也就可以獲取到了。畫張圖,幫助理解一下:

源碼詳解Android中View.post()用法

哇,Q2的問題終于也搞定了,也不容易啊。本篇也算是結(jié)束了。

總結(jié)

分析了半天,最后我們來稍微小結(jié)一下:

View.post(Runnable) 內(nèi)部會自動分兩種情況處理,當 View 還沒 attachedToWindow 時,會先將這些 Runnable 操作緩存下來;否則就直接通過 mAttachInfo.mHandler 將這些 Runnable 操作 post 到主線程的 MessageQueue 中等待執(zhí)行。

如果 View.post(Runnable) 的 Runnable 操作被緩存下來了,那么這些操作將會在 dispatchAttachedToWindow() 被回調(diào)時,通過 mAttachInfo.mHandler.post() 發(fā)送到主線程的 MessageQueue 中等待執(zhí)行。

mAttachInfo 是 ViewRootImpl 的成員變量,在構(gòu)造函數(shù)中初始化,Activity View 樹里所有的子 View 中的 mAttachInfo 都是 ViewRootImpl.mAttachInfo 的引用。

mAttachInfo.mHandler 也是 ViewRootImpl 中的成員變量,在聲明時就初始化了,所以這個 mHandler 綁定的是主線程的 Looper,所以 View.post() 的操作都會發(fā)送到主線程中執(zhí)行,那么也就支持 UI 操作了。

dispatchAttachedToWindow() 被調(diào)用的時機是在 ViewRootImol 的 performTraversals() 中,該方法會進行 View 樹的測量、布局、繪制三大流程的操作。

Handler 消息機制通常情況下是一個 Message 執(zhí)行完后才去取下一個 Message 來執(zhí)行(異步 Message 還沒接觸),所以 View.post(Runnable) 中的 Runnable 操作肯定會在 performMeaure() 之后才執(zhí)行,所以此時可以獲取到 View 的寬高。

好了,就到這里了。至于開頭所提的問題,前兩個已經(jīng)在上面的分析過程以及總結(jié)里都解答了。而至于剩下的問題,這里就稍微提一下:

使用 View.post(),還是有可能會造成內(nèi)存泄漏的,Handler 會造成內(nèi)存泄漏的原因是由于內(nèi)部類持有外部的引用,如果任務是延遲的,就會造成外部類無法被回收。而根據(jù)我們的分析,mAttachInfo.mHandler 只是 ViewRootImpl 一個內(nèi)部類的實例,所以使用不當還是有可能會造成內(nèi)存泄漏的。

網(wǎng)站名稱:源碼詳解Android中View.post()用法-創(chuàng)新互聯(lián)
分享鏈接:http://muchs.cn/article16/deocdg.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站設(shè)計App開發(fā)、靜態(tài)網(wǎng)站、品牌網(wǎng)站建設(shè)、服務器托管、品牌網(wǎng)站設(shè)計

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)

外貿(mào)網(wǎng)站制作