中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

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

發布時間:2020-09-19 23:20:46 來源:腳本之家 閱讀:158 作者:請叫我大蘇 欄目:移動開發

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()

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

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

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

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

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

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

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

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()

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

我在這里卡了很久,一直沒想明白。我甚至以為是 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() 操作之后才執行,寬高也就可以獲取到了。畫張圖,幫助理解一下:

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

哇,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 一個內部類的實例,所以使用不當還是有可能會造成內存泄漏的。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

壶关县| 葫芦岛市| 精河县| 临夏市| 永德县| 台中市| 天柱县| 察雅县| 利辛县| 巴塘县| 平度市| 遵义县| 吉木萨尔县| 张掖市| 陈巴尔虎旗| 仁化县| 普安县| 探索| 东城区| 阜南县| 分宜县| 始兴县| 习水县| 龙陵县| 临沧市| 惠东县| 鲁山县| 永康市| 报价| 景泰县| 且末县| 宁南县| 江山市| 思南县| 涿州市| 桂平市| 澳门| 辰溪县| 阿城市| 泸西县| 青铜峡市|