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ù),十年安福做網(wǎng)站經(jīng)驗,不只是建網(wǎng)站,更提供有價值的思路和整體網(wǎng)絡(luò)服務(wù)。但用多了,總有點心虛,View.post() 會不會有什么隱藏的問題?所以趁有點空余時間,這段時間就來梳理一下,View.post() 原理到底是什么,內(nèi)部都做了啥事。
提問
開始看源碼前,先提幾個問題,帶著問題去看源碼應(yīng)該會比較有效率,防止閱讀源碼過程中,陷得太深,跟得太偏了。
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() 有可能導(dǎo)致內(nèi)存泄漏么?
ps:本篇分析的源碼基于 andoird-25 版本,版本不一樣源碼可能有些區(qū)別,大伙自己過源碼時可以注意一下。另,下面分析過程有點長,慢慢看哈。
源碼分析
好了,就帶著這幾個問題來跟著源碼走吧。其實,這些問題大伙心里應(yīng)該都有數(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)注它是什么時候賦值的,使用的場景就不管了,所以過濾條件可以細一點。這樣一來,全文就只有兩處匹配:
一處賦值,一處置空,剛好又是在對應(yīng)的一個生命周期里:
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 應(yīng)該是還沒有被調(diào)用,也就是 mAttachInfo 這時候還是為空,但我們在 onCreate() 里執(zhí)行 View.post() 里的操作仍然可以保證是在 View 寬高計算完畢的,也就是開頭的問題 Q2,那么這點的原理顯然就是在另一個 return 那邊的方法里了:getRunQueue().post()。
那么,我們就先解決 Q2 吧,為什么 View.post() 可以保證操作是在 View 寬高計算完畢之后呢?跟進 getRunQueue() 看看:
getRunQueue().post()
所以調(diào)用的其實是 HandlerActionQueue.post() 方法,那么我們再繼續(xù)跟進去看看:
post(Runnable) 方法內(nèi)部調(diào)用了 postDelayed(Runnable, long),postDelayed() 內(nèi)部則是將 Runnable 和 long 作為參數(shù)創(chuàng)建一個 HandlerAction 對象,然后添加到 mActions 數(shù)組里。下面先看看 HandlerAction:
很簡單的數(shù)據(jù)結(jié)構(gòu),就一個 Runnable 成員變量和一個 long 成員變量。這個類作用可以理解為用于包裝 View.post(Runnable) 傳入的 Runnable 操作的,當(dāng)然因為還有 View.postDelay() ,所以就還需要一個 long 類型的變量來保存延遲的時間了,這樣一來這個數(shù)據(jù)結(jié)構(gòu)就不難理解了吧。
所以,我們調(diào)用 View.post(Runnable) 傳進去的 Runnable 操作,在傳到 HandlerActionQueue 里會先經(jīng)過 HandlerAction 包裝一下,然后再緩存起來。至于緩存的原理,HandlerActionQueue 是通過一個默認大小為4的數(shù)組保存這些 Runnable 操作的,當(dāng)然,如果數(shù)組不夠用時,就會通過 GrowingArrayUtils 來擴充數(shù)組,具體算法就不繼續(xù)看下去了,不然越來越偏。
到這里,我們先來梳理下:
當(dāng)我們在 Activity 的 onCreate() 里執(zhí)行 View.post(Runnable) 時,因為這時候 View 還沒有 attachedToWindow,所以這些 Runnable 操作其實并沒有被執(zhí)行,而是先通過 HandlerActionQueue 緩存起來。
那么到什么時候這些 Runnable 才會被執(zhí)行呢?我們可以看看 HandlerActionQueue 這個類,它的代碼不多,里面有個 executeActions() 方法,看命名就知道,這方法是用來執(zhí)行這些被緩存起來的 Runnable 操作的:
哇,看到重量級的人物了:Handler。看來被緩存起來沒有執(zhí)行的 Runnable 最后也還是通過 Hnadler 來執(zhí)行的。那么,這個 Handler 又是哪里的呢?看來關(guān)鍵點還是這個方法在哪里被調(diào)用了,那就找找看:
借助 AS 的 Ctrl + Alt + F7
快捷鍵,可以查找 SDK 里的某個方法在哪些地方被調(diào)用了。
很好,找到了,而且只找到這個地方。其實,這個快捷鍵有時并沒有辦法找到一些方法被調(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)部它自己分了兩種情況處理,當(dāng) 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 用不習(xí)慣,不過分析源碼時確實可以結(jié)合這兩個軟件。
哇,懵逼,完全懵逼。我就想看個 View.post(),結(jié)果跟著跟著,跟到這里來了。ViewRootImpl 我在分析Android KeyEvent 點擊事件分發(fā)處理流程時短暫接觸過,但這次顯然比上次還需要更深入去接觸,哎,力不從心啊。
我只能跟大伙肯定的是,mView 是 Activity 的 DecorView。咦~,等等,這樣看來 ViewRootImpl 是調(diào)用的 DecorView 的 dispatchAttachedToWindow() ,但我們在使用 View.post() 時,這個 View 可以是任意 View,并不是非得用 DecorView 吧。哈哈哈,這是不是代表著我們找錯地方了?不管了,我們就去其他三個被調(diào)用的地方: ViewGroup 里看看吧:
addViewInner() 是 ViewGroup 在添加子 View 時的內(nèi)部邏輯,也就是說當(dāng) ViewGroup addView() 時,如果 mAttachInfo 不為空,就都會去調(diào)用子 View 的 dispatchAttachedToWindow(),并將自己的 mAttachInfo 傳進去。還記得 View 的 dispatchAttachedToWindow() 這個方法么:
mAttachInfo 唯一被賦值的地方也就是在這里,那么也就是說,子 View 的 mAttachInfo 其實跟父控件 ViewGroup 里的 mAttachInfo 是同一個的。那么,關(guān)鍵點還是這個 mAttachInfo 什么時候才不為空,也就是說 ViewGroup 在 addViewInner() 時,傳進去的 mAttachInfo 是在哪被賦值的呢?我們來找找看:
咦,利用 AS 的 Ctrl + 左鍵
怎么找不到 mAttachInfo 被定義的地方呢,不管了,那我們用 Ctrl + F
搜索一下在 ViewGroup 類里 mAttachInfo 被賦值的地方好了:
咦,怎么一個地方也沒有。難道說,這個 mAttachInfo 是父類 View 定義的變量么,既然 AS 找不到,我們換 Source Insight 試試:
還真的是,ViewGroup 是繼承的 View,并且處于同一個包里,所以可以直接使用該變量,那這樣一來,我們豈不是又繞回來了。前面說過,dispatchAttachedToWindow() 在 ViewGroup 里有三處調(diào)用的地方,既然 addViewInner() 這里的看不出什么,那去另外兩個地方看看:
剩下的兩個地方就都是在 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()
這方法代碼有八百多行!!不過,我們只關(guān)注我們需要的點就行,這樣一省略無關(guān)代碼來看,是不是感覺代碼就簡單得多了。
mFirst 初始化為 true,全文只有一處賦值,所以 if(mFirst) 塊里的代碼只會執(zhí)行一次。我對 ViewRootImpl 不是很懂,performTraversals() 這個方法應(yīng)該是通知 Activity 的 View 樹開始測量、布局、繪制。而 DevorView 是 Activity 視圖的根布局、View 樹的起點,它繼承 FrameLayout,所以也是個 ViewGroup,而我們之前對 ViewGroup 的 dispatchAttachedToWindow() 分析過了吧,在這個方法里會將 mAttachInfo 傳給所有子 View。也就是說,在 Activity 首次進行 View 樹的遍歷繪制時,ViewRootImpl 會將自己的 mAttachInfo 通過根布局 DecorView 傳遞給所有的子 View 。
那么,我們就來看看 ViewRootImpl 的 mAttachInfo 什么時候初始化的吧:
在構(gòu)造函數(shù)里對 mAttachInfo 進行初始化,傳入了很多參數(shù),我們關(guān)注的應(yīng)該是 mHandler 這個變量,所以看看這個變量定義:
終于找到 new Handler() 的地方了,至于這個自定義的 Handler 類做了啥,我們不關(guān)心,反正通過 post() 方式執(zhí)行的操作跟它自定義的東西也沒有多大關(guān)系。我們關(guān)心的是在哪 new 了這個 Handler。因為每個 Handler 在 new 的時候都會綁定一個 Looper,這里 new 的時候是無參構(gòu)造函數(shù),那默認綁定的就是當(dāng)前線程的 Looper,而這句 new 代碼是在主線程中執(zhí)行的,所以這個 Handler 綁定的也就是主線程的 Looper。至于這些的原理,就涉及到 Handler 的源碼和 ThreadLocal 的原理了,就不繼續(xù)跟進了,太偏了,大伙清楚結(jié)論這點就好。
這也就是為什么 View.post(Runnable) 的操作可以更新 UI 的原因,因為這些 Runnable 操作都通過 ViewRootImpl 的 mHandler 切到主線程來執(zhí)行了。
這樣 Q1 就搞定了,終于搞定了一個問題,不容易啊,本來以為很簡單的來著。
跟到 ViewRootImpl 這里應(yīng)該就可以停住了。至于 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í)行。
我在這里卡了很久,一直沒想明白。我甚至以為是 PhoneWindow 在加載 layout 布局到 DecorView 時就進行了測量的操作,所以一直跟,跟到 LayoutInflater.inflate(),跟到了 ViewGroup.addView(),最后發(fā)現(xiàn)跟測量有關(guān)的操作最終都又繞回到 ViewRootImpl 中去了。
原來是自己火候不夠,對 Android 的消息機制還不大理解,這篇博客前前后后寫了一兩個禮拜,就是在不斷查缺補漏,學(xué)習(xí)、理解相關(guān)的知識點。
大概的來講,就是我們的 app 都是基于消息驅(qū)動機制來運行的,主線程的 Looper 會無限的循環(huán),不斷的從 MessageQueue 里取出 Message 來執(zhí)行,當(dāng)一個 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í)行,寬高也就可以獲取到了。畫張圖,幫助理解一下:
哇,Q2的問題終于也搞定了,也不容易啊。本篇也算是結(jié)束了。
總結(jié)
分析了半天,最后我們來稍微小結(jié)一下:
View.post(Runnable) 內(nèi)部會自動分兩種情況處理,當(dāng) 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)部類持有外部的引用,如果任務(wù)是延遲的,就會造成外部類無法被回收。而根據(jù)我們的分析,mAttachInfo.mHandler 只是 ViewRootImpl 一個內(nèi)部類的實例,所以使用不當(dāng)還是有可能會造成內(nèi)存泄漏的。
網(wǎng)站名稱:源碼詳解Android中View.post()用法-創(chuàng)新互聯(lián)
分享鏈接:http://aaarwkj.com/article16/deocdg.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站設(shè)計、App開發(fā)、靜態(tài)網(wǎng)站、品牌網(wǎng)站建設(shè)、服務(wù)器托管、品牌網(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)
猜你還喜歡下面的內(nèi)容