【Andorid源碼解析】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() 有可能致使內存泄漏麼?app

ps:本篇分析的源碼基於 andoird-25 版本,版本不同源碼可能有些區別,大夥本身過源碼時能夠注意一下。另,下面分析過程有點長,慢慢看哈。異步

源碼分析

好了,就帶着這幾個問題來跟着源碼走吧。其實,這些問題大夥內心應該都有數了,看源碼也就是爲了驗證內心的想法。第一個問題,之因此能夠對 UI 進行操做,那內部確定也是經過 Handler 來實現了,因此看源碼的時候就能夠看看內部是如何對 Handler 進行封裝的。而至於剩下的問題,那就在看源碼過程當中順帶看看可否找到答案吧。函數

View.post()

View.post.png

View.post() 方法很簡單,代碼不多。那咱們就一行行的來看。oop

若是 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 時才賦值的,因此接下去關鍵的一點是搞懂 attachedToWindowdetachedFromWindow 這個生命週期分別在何時在哪裏被調用了。

雖然咱們如今還不清楚,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 的 attachedToWindowdetachedFromWiondow 這期間纔會被執行。

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

  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 的關係,什麼時候綁定等等。因此,須要學的還好多,也感謝各個前輩大神費心整理的博客,下面列一些供大夥參考:

  1. scnuxisan225#經過View.post()獲取View的寬高引起的兩個問題

  2. kc專欄#Activity WMS ViewRootImpl三者關係

  3. 廢墟的樹#從ViewRootImpl類分析View繪製的流程

  4. 兇殘的程序員#Android 消息機制——你真的瞭解Handler?


QQ圖片20180316094923.jpg 最近剛開通了公衆號,想激勵本身堅持寫做下去,初期主要分享原創的Android或Android-Tv方面的小知識,感興趣的能夠點一波關注,謝謝支持~~