【Andorid原始碼解析】View.post() 到底幹了啥
本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家釋出
emmm,大夥都知道,子執行緒是不能進行 UI 操作的,或者很多場景下,一些操作需要延遲執行,這些都可以通過 Handler 來解決。但說實話,實在是太懶了,總感覺寫 Handler 太麻煩了,一不小心又很容易寫出記憶體洩漏的程式碼來,所以為了偷懶,我就經常用 View.post() or View.postDelay() 來代替 Handler 使用。
但用多了,總有點心虛,View.post() 會不會有什麼隱藏的問題?所以趁有點空餘時間,這段時間就來梳理一下,View.post() 原理到底是什麼,內部都做了啥事。
提問
開始看原始碼前,先提幾個問題,帶著問題去看原始碼應該會比較有效率,防止閱讀原始碼過程中,陷得太深,跟得太偏了。
Q1: 為什麼 View.post() 的操作是可以對 UI 進行操作的呢,即使是在子執行緒中呼叫 View.post()?
Q2:網上都說 View.post() 中的操作執行時,View 的寬高已經計算完畢,所以經常看見在 Activity 的 onCreate() 裡呼叫 View.post() 來解決獲取 View 寬高為0的問題,為什麼可以這樣做呢?
Q3:用 View.postDelay() 有可能導致記憶體洩漏麼?
ps:本篇分析的原始碼基於 andoird-25
原始碼分析
好了,就帶著這幾個問題來跟著原始碼走吧。其實,這些問題大夥心裡應該都有數了,看原始碼也就是為了驗證心裡的想法。第一個問題,之所以可以對 UI 進行操作,那內部肯定也是通過 Handler 來實現了,所以看原始碼的時候就可以看看內部是如何對 Handler 進行封裝的。而至於剩下的問題,那就在看原始碼過程中順帶看看能否找到答案吧。
View.post()
View.post() 方法很簡單,程式碼很少。那我們就一行行的來看。
如果 mAttachInfo
那就找一下,mAttachInfo 是什麼時候賦值的,可以藉助 AS 的 Ctrl + F
查詢功能,過濾一下 mAttachInfo =
,注意 =
號後面還有一個空格,否則你查詢的時候會發現全文有兩百多處匹配到。我們只關注它是什麼時候賦值的,使用的場景就不管了,所以過濾條件可以細一點。這樣一來,全文就只有兩處匹配:
一處賦值,一處置空,剛好又是在對應的一個生命週期裡:
- dispatchAttachedToWindow() 下文簡稱 attachedToWindow
- dispatchDetachedFromWindow() 下文簡稱 detachedFromWindow。
所以,如果 mAttachInfo 不為空的時候,走的就是 Handler 的 post(),也就是 View.post() 在這種場景下,實際上就是呼叫的 Handler.post(),接下去就是搞清楚一點,這個 Handler 是哪裡的 Handler,在哪裡初始化等等,但這點可以先暫時放一邊,因為 mAttachInfo 是在 attachedToWindow 時才賦值的,所以接下去關鍵的一點是搞懂 attachedToWindow 到 detachedFromWindow 這個生命週期分別在什麼時候在哪裡被呼叫了。
雖然我們現在還不清楚,attachedToWindow 到底是什麼時候被呼叫的,但看到這裡我們至少清楚一點,在 Activity 的 onCreate() 期間,這個 View 的 attachedToWindow 應該是還沒有被呼叫,也就是 mAttachInfo 這時候還是為空,但我們在 onCreate() 裡執行 View.post() 裡的操作仍然可以保證是在 View 寬高計算完畢的,也就是開頭的問題 Q2,那麼這點的原理顯然就是在另一個 return 那邊的方法裡了:getRunQueue().post()。
那麼,我們就先解決 Q2 吧,為什麼 View.post() 可以保證操作是在 View 寬高計算完畢之後呢?跟進 getRunQueue() 看看:
getRunQueue().post()
所以呼叫的其實是 HandlerActionQueue.post() 方法,那麼我們再繼續跟進去看看:
post(Runnable) 方法內部呼叫了 postDelayed(Runnable, long),postDelayed() 內部則是將 Runnable 和 long 作為引數建立一個 HandlerAction 物件,然後新增到 mActions 數組裡。下面先看看 HandlerAction:
很簡單的資料結構,就一個 Runnable 成員變數和一個 long 成員變數。這個類作用可以理解為用於包裝 View.post(Runnable) 傳入的 Runnable 操作的,當然因為還有 View.postDelay() ,所以就還需要一個 long 型別的變數來儲存延遲的時間了,這樣一來這個資料結構就不難理解了吧。
所以,我們呼叫 View.post(Runnable) 傳進去的 Runnable 操作,在傳到 HandlerActionQueue 裡會先經過 HandlerAction 包裝一下,然後再快取起來。至於快取的原理,HandlerActionQueue 是通過一個預設大小為4的陣列儲存這些 Runnable 操作的,當然,如果陣列不夠用時,就會通過 GrowingArrayUtils 來擴充陣列,具體演算法就不繼續看下去了,不然越來越偏。
到這裡,我們先來梳理下:
當我們在 Activity 的 onCreate() 裡執行 View.post(Runnable) 時,因為這時候 View 還沒有 attachedToWindow,所以這些 Runnable 操作其實並沒有被執行,而是先通過 HandlerActionQueue 快取起來。
那麼到什麼時候這些 Runnable 才會被執行呢?我們可以看看 HandlerActionQueue 這個類,它的程式碼不多,裡面有個 executeActions() 方法,看命名就知道,這方法是用來執行這些被快取起來的 Runnable 操作的:
哇,看到重量級的人物了:Handler。看來被快取起來沒有執行的 Runnable 最後也還是通過 Hnadler 來執行的。那麼,這個 Handler 又是哪裡的呢?看來關鍵點還是這個方法在哪裡被呼叫了,那就找找看:
藉助 AS 的 Ctrl + Alt + F7
快捷鍵,可以查詢 SDK 裡的某個方法在哪些地方被呼叫了。
很好,找到了,而且只找到這個地方。其實,這個快捷鍵有時並沒有辦法找到一些方法被呼叫的地方,這也是原始碼閱讀過程中令人頭疼的一點,因為沒法找到這些方法到底在哪些地方被呼叫了,所以很難把流程梳理下來。如果方法是私有的,那很好辦,就用 Ctrl + F
在這個類裡找一下就可以,如果匹配結果太多,那就像開頭那樣把過濾條件詳細一點。如果方法不是私有的,那真的就很難辦了,這也是一開始找到 dispatchAttachedToWindow() 後為什麼不繼續跟蹤下去轉而來分析Q2:getRunQueue() 的原因,因為用 AS 找不到 dispatchAttachedToWindow() 到底在哪些地方被誰呼叫了。哇,好像又扯遠了,迴歸正題迴歸正題。
emmm,看來這裡也繞回來了,dispatchAttachedToWindow() 看來是個關鍵的節點。
那到這裡,我們再次來梳理一下:
我們使用 View.post() 時,其實內部它自己分了兩種情況處理,當 View 還沒有 attachedToWindow 時,通過 View.post(Runnable) 傳進來的 Runnable 操作都先被快取在 HandlerActionQueue,然後等 View 的 dispatchAttachedToWindow() 被呼叫時,就通過 mAttachInfo.mHandler 來執行這些被快取起來的 Runnable 操作。從這以後到 View 被 detachedFromWindow 這段期間,如果再次呼叫 View.post(Runnable) 的話,那麼這些 Runnable 不用再快取了,而是直接交給 mAttachInfo.mHanlder 來執行。
以上,就是到目前我們所能得知的資訊。這樣一來,Q2 是不是漸漸有一些頭緒了:View.post(Runnable) 的操作之所以可以保證肯定是在 View 寬高計算完畢之後才執行的,是因為這些 Runnable 操作只有在 View 的 attachedToWindow 到 detachedFromWiondow 這期間才會被執行。
那麼,接下去就還剩兩個關鍵點需要搞清楚了:
- dispatchAttachedToWindow() 是什麼時候被呼叫的?
- mAttachInfo 是在哪裡初始化的?
dispatchAttachedToWindow() & mAttachInfo
只借助 AS 的話,很難找到 dispatchAttachedToWindow() 到底在哪些地方被呼叫。所以,到這裡,我又藉助了 Source Insight 軟體。
很棒!找到了四個被呼叫的地方,三個在 ViewGroup 裡,一個在 ViewRootImpl.performTraversals() 裡。找到了就好,接下去繼續用 AS 來分析吧,Source Insight 用不習慣,不過分析原始碼時確實可以結合這兩個軟體。
哇,懵逼,完全懵逼。我就想看個 View.post(),結果跟著跟著,跟到這裡來了。ViewRootImpl 我在分析Android KeyEvent 點選事件分發處理流程時短暫接觸過,但這次顯然比上次還需要更深入去接觸,哎,力不從心啊。
我只能跟大夥肯定的是,mView 是 Activity 的 DecorView。咦~,等等,這樣看來 ViewRootImpl 是呼叫的 DecorView 的 dispatchAttachedToWindow() ,但我們在使用 View.post() 時,這個 View 可以是任意 View,並不是非得用 DecorView 吧。哈哈哈,這是不是代表著我們找錯地方了?不管了,我們就去其他三個被呼叫的地方: ViewGroup 裡看看吧:
addViewInner() 是 ViewGroup 在新增子 View 時的內部邏輯,也就是說當 ViewGroup addView() 時,如果 mAttachInfo 不為空,就都會去呼叫子 View 的 dispatchAttachedToWindow(),並將自己的 mAttachInfo 傳進去。還記得 View 的 dispatchAttachedToWindow() 這個方法麼:
mAttachInfo 唯一被賦值的地方也就是在這裡,那麼也就是說,子 View 的 mAttachInfo 其實跟父控制元件 ViewGroup 裡的 mAttachInfo 是同一個的。那麼,關鍵點還是這個 mAttachInfo 什麼時候才不為空,也就是說 ViewGroup 在 addViewInner() 時,傳進去的 mAttachInfo 是在哪被賦值的呢?我們來找找看:
咦,利用 AS 的 Ctrl + 左鍵
怎麼找不到 mAttachInfo 被定義的地方呢,不管了,那我們用 Ctrl + F
搜尋一下在 ViewGroup 類裡 mAttachInfo 被賦值的地方好了:
咦,怎麼一個地方也沒有。難道說,這個 mAttachInfo 是父類 View 定義的變數麼,既然 AS 找不到,我們換 Source Insight 試試:
還真的是,ViewGroup 是繼承的 View,並且處於同一個包裡,所以可以直接使用該變數,那這樣一來,我們豈不是又繞回來了。前面說過,dispatchAttachedToWindow() 在 ViewGroup 裡有三處呼叫的地方,既然 addViewInner() 這裡的看不出什麼,那去另外兩個地方看看:
剩下的兩個地方就都是在 ViewGroup 重寫的 dispatchAttachedToWindow() 方法裡了,這程式碼也很好理解,在該方法被呼叫的時候,先執行 super 也就是 View 的 dispatchAttachedToWindow() 方法,還沒忘記吧,mAttachInfo 就是在這裡被賦值的。然後再遍歷子 View,分別呼叫子 View 的 dispatchAttachedToWindow() 方法,並將 mAttachInfo 作為引數傳遞進去,這樣一來,子 View 的 mAttachInfo 也都被賦值了。
但這樣一來,我們就繞進死衚衕了。
我們還是先來梳理一下吧:
目前,我們知道,View.post(Runnable) 的這些 Runnable 操作,在 View 被 attachedToWindow 之前會先快取下來,然後在 dispatchAttachedToWindow() 被呼叫時,就將這些快取下來的 Runnable 通過 mAttachInfo 的 mHandler 來執行。在這之後再呼叫 View.post(Runnable) 的話,這些 Runnable 操作就不用再被快取了,而是直接交由 mAttachInfo 的 mHandler 來執行。
所以,我們得搞清楚 dispatchAttachedToWindow() 在什麼時候被呼叫,以及 mAttachInfo 是在哪被初始化的,因為需要知道它的變數如 mHandler 都是些什麼以及驗證 mHandler 執行這些 Runnable 操作是在 measure 之後的,這樣才能保證此時的寬高不為0。
然後,我們在跟蹤 dispatchAttachedToWindow() 被呼叫的地方時,跟到了 ViewGroup 的 addViewInner() 裡。在這裡我們得到的資訊是如果 mAttachInfo 不為空時,會直接呼叫子 View 的 dispatchAttachedToWindow(),這樣新 add 進來的子 View 的 mAttachInfo 就會被賦值了。但 ViewGroup 的 mAttachInfo 是父類 View 的變數,所以為不為空的關鍵還是回到了 dispatchAttachedToWindow() 被呼叫的時機。
我們還跟到了 ViewGroup 重寫的 dispatchAttachedToWindow() 方法裡,但顯然,ViewGroup 重寫這個方法只是為了將 attachedToWindow 這個事件通知給它所有的子 View。
所以,最後,我們能得到的結論就是,我們還得再回去 ViewRootImpl 裡,dispatchAttachedToWindow() 被呼叫的地方,除了 ViewRootImpl,我們都分析過了,得不到什麼資訊,只剩最後 ViewRootImpl 這裡了,所以關鍵點肯定在這裡。看來這次,不行也得上了。
ViewRootImpl.performTraversals()
這方法程式碼有八百多行!!不過,我們只關注我們需要的點就行,這樣一省略無關程式碼來看,是不是感覺程式碼就簡單得多了。
mFirst 初始化為 true,全文只有一處賦值,所以 if(mFirst) 塊裡的程式碼只會執行一次。我對 ViewRootImpl 不是很懂,performTraversals() 這個方法應該是通知 Activity 的 View 樹開始測量、佈局、繪製。而 DevorView 是 Activity 檢視的根佈局、View 樹的起點,它繼承 FrameLayout,所以也是個 ViewGroup,而我們之前對 ViewGroup 的 dispatchAttachedToWindow() 分析過了吧,在這個方法裡會將 mAttachInfo 傳給所有子 View。也就是說,在 Activity 首次進行 View 樹的遍歷繪製時,ViewRootImpl 會將自己的 mAttachInfo 通過根佈局 DecorView 傳遞給所有的子 View 。
那麼,我們就來看看 ViewRootImpl 的 mAttachInfo 什麼時候初始化的吧:
在建構函式裡對 mAttachInfo 進行初始化,傳入了很多引數,我們關注的應該是 mHandler 這個變數,所以看看這個變數定義:
終於找到 new Handler() 的地方了,至於這個自定義的 Handler 類做了啥,我們不關心,反正通過 post() 方式執行的操作跟它自定義的東西也沒有多大關係。我們關心的是在哪 new 了這個 Handler。因為每個 Handler 在 new 的時候都會繫結一個 Looper,這裡 new 的時候是無參建構函式,那預設繫結的就是當前執行緒的 Looper,而這句 new 程式碼是在主執行緒中執行的,所以這個 Handler 繫結的也就是主執行緒的 Looper。至於這些的原理,就涉及到 Handler 的原始碼和 ThreadLocal 的原理了,就不繼續跟進了,太偏了,大夥清楚結論這點就好。
這也就是為什麼 View.post(Runnable) 的操作可以更新 UI 的原因,因為這些 Runnable 操作都通過 ViewRootImpl 的 mHandler 切到主執行緒來執行了。
這樣 Q1 就搞定了,終於搞定了一個問題,不容易啊,本來以為很簡單的來著。
跟到 ViewRootImpl 這裡應該就可以停住了。至於 ViewRootImpl 跟 Activity 有什麼關係、什麼時候被例項化的、跟 DecroView 如何繫結的就不跟進了,因為我也還不是很懂,感興趣的可以自己去看看,我在末尾會給一些參考部落格。
至此,我們清楚了 mAttachInfo 的由來,也知道了 mAttachInfo.mHandler,還知道在 Activity 首次遍歷 View 樹進行測量、繪製時會通過 DecorView 的 dispatchAttachedToWindow() 將 ViewRootImpl 的 mAttachInfo 傳遞給所有子 View,並通知所有呼叫 View.post(Runnable) 被快取起來的 Runnable 操作可以執行了。
但不知道大夥會不會跟我一樣還有一點疑問:看網上對 ViewRootImpl.performTraversals() 的分析:遍歷 View 樹進行測量、佈局、繪製操作的程式碼顯然是在呼叫了 dispatchAttachedToWindow() 之後才執行,那這樣一來是如何保證 View.post(Runnable) 的 Runnable 操作可以獲取到 View 的寬高呢?明明測量的程式碼 performMeasure() 是在 dispatchAttachedToWindow() 後面才執行。
我在這裡卡了很久,一直沒想明白。我甚至以為是 PhoneWindow 在載入 layout 佈局到 DecorView 時就進行了測量的操作,所以一直跟,跟到 LayoutInflater.inflate(),跟到了 ViewGroup.addView(),最後發現跟測量有關的操作最終都又繞回到 ViewRootImpl 中去了。
原來是自己火候不夠,對 Android 的訊息機制還不大理解,這篇部落格前前後後寫了一兩個禮拜,就是在不斷查缺補漏,學習、理解相關的知識點。
大概的來講,就是我們的 app 都是基於訊息驅動機制來執行的,主執行緒的 Looper 會無限的迴圈,不斷的從 MessageQueue 裡取出 Message 來執行,當一個 Message 執行完後才會去取下一個 Message 來執行。而 Handler 則是用於將 Message 傳送到 MessageQueue 裡,等輪到 Message 執行時,又通過 Handler 傳送到 Target 去執行,等執行完再取下一個 Message,如此迴圈下去。
清楚了這點後,我們再回過頭來看看:
performTraversals() 會先執行 dispatchAttachedToWindow(),這時候所有子 View 通過 View.post(Runnable) 快取起來的 Runnable 操作就都會通過 mAttachInfo.mHandler 的 post() 方法將這些 Runnable 封裝到 Message 裡傳送到 MessageQueue 裡。mHandler 我們上面也分析過了,繫結的是主執行緒的 Looper,所以這些 Runnable 其實都是傳送到主執行緒的 MessageQueue 裡排隊,等待執行。然後 performTraversals() 繼續往下工作,相繼執行 performMeasure(),performLayout() 等操作。等全部執行完後,表示這個 Message 已經處理完畢,所以 Looper 才會去取下一個 Message,這時候,才有可能輪到這些 Runnable 執行。所以,這些 Runnable 操作也就肯定會在 performMeasure() 操作之後才執行,寬高也就可以獲取到了。畫張圖,幫助理解一下:
哇,Q2的問題終於也搞定了,也不容易啊。本篇也算是結束了。
總結
分析了半天,最後我們來稍微小結一下:
View.post(Runnable) 內部會自動分兩種情況處理,當 View 還沒 attachedToWindow 時,會先將這些 Runnable 操作快取下來;否則就直接通過 mAttachInfo.mHandler 將這些 Runnable 操作 post 到主執行緒的 MessageQueue 中等待執行。
如果 View.post(Runnable) 的 Runnable 操作被快取下來了,那麼這些操作將會在 dispatchAttachedToWindow() 被回撥時,通過 mAttachInfo.mHandler.post() 傳送到主執行緒的 MessageQueue 中等待執行。
mAttachInfo 是 ViewRootImpl 的成員變數,在建構函式中初始化,Activity View 樹裡所有的子 View 中的 mAttachInfo 都是 ViewRootImpl.mAttachInfo 的引用。
mAttachInfo.mHandler 也是 ViewRootImpl 中的成員變數,在宣告時就初始化了,所以這個 mHandler 繫結的是主執行緒的 Looper,所以 View.post() 的操作都會發送到主執行緒中執行,那麼也就支援 UI 操作了。
dispatchAttachedToWindow() 被呼叫的時機是在 ViewRootImol 的 performTraversals() 中,該方法會進行 View 樹的測量、佈局、繪製三大流程的操作。
Handler 訊息機制通常情況下是一個 Message 執行完後才去取下一個 Message 來執行(非同步 Message 還沒接觸),所以 View.post(Runnable) 中的 Runnable 操作肯定會在 performMeaure() 之後才執行,所以此時可以獲取到 View 的寬高。
好了,就到這裡了。至於開頭所提的問題,前兩個已經在上面的分析過程以及總結裡都解答了。而至於剩下的問題,這裡就稍微提一下:
使用 View.post(),還是有可能會造成記憶體洩漏的,Handler 會造成記憶體洩漏的原因是由於內部類持有外部的引用,如果任務是延遲的,就會造成外部類無法被回收。而根據我們的分析,mAttachInfo.mHandler 只是 ViewRootImpl 一個內部類的例項,所以使用不當還是有可能會造成記憶體洩漏的。
參考連結
雖然只是過一下 View.post() 的原始碼,但真正過下去才發現,要理解清楚,還得理解 Handler 的訊息機制、ViewRootImpl 的作用、ViewRootImpl 和 Activity 的關係,何時繫結等等。所以,需要學的還好多,也感謝各個前輩大神費心整理的部落格,下面列一些供大夥參考:
最近剛開通了公眾號,想激勵自己堅持寫作下去,初期主要分享原創的Android或Android-Tv方面的小知識,感興趣的可以點一波關注,謝謝支援~~