1. 程式人生 > >【Android原始碼解析】View.post()到底幹了啥

【Android原始碼解析】View.post()到底幹了啥

本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家釋出

View.post示例.png

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.png

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

如果 mAttachInfo 不為空,那就呼叫 mAttachInfo.mHanlder.post() 方法,如果為空,則呼叫 getRunQueue().post() 方法。

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

dispatchAttachedToWindow.png

dispatchDetachedFromWindow.png

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

  1. dispatchAttachedToWindow() 下文簡稱 attachedToWindow
  2. 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()

getRunQueue.png

所以呼叫的其實是 HandlerActionQueue.post() 方法,那麼我們再繼續跟進去看看:

HandlerActionQueue.png

post(Runnable) 方法內部呼叫了 postDelayed(Runnable, long),postDelayed() 內部則是將 Runnable 和 long 作為引數建立一個 HandlerAction 物件,然後新增到 mActions 數組裡。下面先看看 HandlerAction:

HandlerAction.png

很簡單的資料結構,就一個 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 操作的:

executeActions.png

哇,看到重量級的人物了:Handler。看來被快取起來沒有執行的 Runnable 最後也還是通過 Hnadler 來執行的。那麼,這個 Handler 又是哪裡的呢?看來關鍵點還是這個方法在哪裡被呼叫了,那就找找看:

查詢呼叫executeActions的地方.png

藉助 AS 的 Ctrl + Alt + F7 快捷鍵,可以查詢 SDK 裡的某個方法在哪些地方被呼叫了。

mRunQueue.executeActions.png

很好,找到了,而且只找到這個地方。其實,這個快捷鍵有時並沒有辦法找到一些方法被呼叫的地方,這也是原始碼閱讀過程中令人頭疼的一點,因為沒法找到這些方法到底在哪些地方被呼叫了,所以很難把流程梳理下來。如果方法是私有的,那很好辦,就用 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 這期間才會被執行。

那麼,接下去就還剩兩個關鍵點需要搞清楚了:

  1. dispatchAttachedToWindow() 是什麼時候被呼叫的?
  2. mAttachInfo 是在哪裡初始化的?

dispatchAttachedToWindow() & mAttachInfo

只借助 AS 的話,很難找到 dispatchAttachedToWindow() 到底在哪些地方被呼叫。所以,到這裡,我又藉助了 Source Insight 軟體。
sourceInsight查詢dispatchAttachedToWindow.png

很棒!找到了四個被呼叫的地方,三個在 ViewGroup 裡,一個在 ViewRootImpl.performTraversals() 裡。找到了就好,接下去繼續用 AS 來分析吧,Source Insight 用不習慣,不過分析原始碼時確實可以結合這兩個軟體。

ViewRootImpl.performTraversals.png

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

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

ViewGroup.addViewInner.png

addViewInner() 是 ViewGroup 在新增子 View 時的內部邏輯,也就是說當 ViewGroup addView() 時,如果 mAttachInfo 不為空,就都會去呼叫子 View 的 dispatchAttachedToWindow(),並將自己的 mAttachInfo 傳進去。還記得 View 的 dispatchAttachedToWindow() 這個方法麼:

View.dispatachAttachedToWindow.png

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

查詢ViewGroup的mAttachInfo.png

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

ViewGroup裡查詢mAttachInfo被賦值的地方.png

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

用SourceInsight查詢mAttachInfo.png

View.mAttachInfo.png

還真的是,ViewGroup 是繼承的 View,並且處於同一個包裡,所以可以直接使用該變數,那這樣一來,我們豈不是又繞回來了。前面說過,dispatchAttachedToWindow() 在 ViewGroup 裡有三處呼叫的地方,既然 addViewInner() 這裡的看不出什麼,那去另外兩個地方看看:

ViewGroup.dispatchAttachedToWindow.png

剩下的兩個地方就都是在 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()

ViewRootImpl.performTraversals.png

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

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 什麼時候初始化的吧:

ViewRootImpl建構函式.png

在建構函式裡對 mAttachInfo 進行初始化,傳入了很多引數,我們關注的應該是 mHandler 這個變數,所以看看這個變數定義:

mHandler.png

終於找到 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() 後面才執行。

performTraversals.png

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

最後,感謝通過View.post()獲取View的寬高引發的兩個問題這篇部落格的作者,解答了我的疑問。

原來是自己火候不夠,對 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() 操作之後才執行,寬高也就可以獲取到了。畫張圖,幫助理解一下:

Handler訊息機制.png

哇,Q2的問題終於也搞定了,也不容易啊。本篇也算是結束了。

總結

分析了半天,最後我們來稍微小結一下:

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

  2. 如果 View.post(Runnable) 的 Runnable 操作被快取下來了,那麼這些操作將會在 dispatchAttachedToWindow() 被回撥時,通過 mAttachInfo.mHandler.post() 傳送到主執行緒的 MessageQueue 中等待執行。

  3. mAttachInfo 是 ViewRootImpl 的成員變數,在建構函式中初始化,Activity View 樹裡所有的子 View 中的 mAttachInfo 都是 ViewRootImpl.mAttachInfo 的引用。

  4. mAttachInfo.mHandler 也是 ViewRootImpl 中的成員變數,在宣告時就初始化了,所以這個 mHandler 繫結的是主執行緒的 Looper,所以 View.post() 的操作都會發送到主執行緒中執行,那麼也就支援 UI 操作了。

  5. dispatchAttachedToWindow() 被呼叫的時機是在 ViewRootImol 的 performTraversals() 中,該方法會進行 View 樹的測量、佈局、繪製三大流程的操作。

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

好了,就到這裡了。至於開頭所提的問題,前兩個已經在上面的分析過程以及總結裡都解答了。而至於剩下的問題,這裡就稍微提一下:

使用 View.post(),還是有可能會造成記憶體洩漏的,Handler 會造成記憶體洩漏的原因是由於內部類持有外部的引用,如果任務是延遲的,就會造成外部類無法被回收。而根據我們的分析,mAttachInfo.mHandler 只是 ViewRootImpl 一個內部類的例項,所以使用不當還是有可能會造成記憶體洩漏的。

參考連結

雖然只是過一下 View.post() 的原始碼,但真正過下去才發現,要理解清楚,還得理解 Handler 的訊息機制、ViewRootImpl 的作用、ViewRootImpl 和 Activity 的關係,何時繫結等等。所以,需要學的還好多,也感謝各個前輩大神費心整理的部落格,下面列一些供大夥參考: