版權聲明:
本賬號發布文章均來自公眾號,承香墨影(cxmyDev),版權歸承香墨影所有。
每周會統一更新到這里,如果喜歡,可關注公眾號獲取最新文章。
未經允許,不得轉載。
序
這篇文章之前發過一遍,但是有讀者指出來有些地方描述的有問題,我后來再看的時候也覺得有問題,所以把之前的文章刪掉(主線是沒有問題的,刪掉只是是避免更多的人誤會),準備修改勘誤之后,再重新發布一遍,這次會補齊描述問題的 Demo 。
有問題繼續文章后面留言,再次感謝細心的讀者指出文章內的錯誤。
一、前言
有時候,我們會需要用到 View.post()
方法,來將一個 Runnable 發送到主線程去執行。這一切,看似很美好,它最終會通過一個 Handler.post()
方法去執行,又避免我們重新定義一個 Handler 對象。
但是,在 Android 7.0(Api level 24) 上,View.post()
將不再那么靠譜了,你 post()
出去的 Runnable ,可能永遠也不會有機會得到執行。我們先來看看它們的細節。
二、post 在 7.0 的差異
2.1 post 方法的差異
前面提到,這個問題只出現在 Android 7.0 上。那么就先從源碼分析 Android 7.0 到底對 View.post()
做了什么改動。
用 Diff 看一下它們的差異,左邊是 Api Level 24(以下簡稱 Api24) 的代碼,右邊是 Api level 23-(以下簡稱 Api23) 的代碼。
很明顯的可以看出來,它們只有在 mAttachInfo 為 null 的時候,執行的邏輯才會有差異。
Api24 中,會調用 getRunQueue().post(action)
,而 Api23 會調用 ViewRootImpl.getRunQueue().post(action)
方法,他們的差異就在這里。
2.2 Api23 post 的細節
先簡單理解一下,ViewRootImpl 是什么。
ViewRootImpl 可以理解是一個 Activity 的 ViewTree 的根節點的實例。每個 ViewRootImpl 就是用來管理 DecorView 和 ViewTree。
ViewRootImpl 中,用來承載 Runnable 的隊列是 sRunQueues ,它一個靜態的變量,也就是說在 App 的生命周期內,ViewRootImpl 中的這個消息隊列都是同一個。
再來看看前面提到的 ViewRootImpl.getRunQueue().post()
到底干了什么?
post()
方法只是單純的將它包裝成一個 HandlerAction 對象,然后放入 mActions 這個 ArrayList 中。繼續追查下去就需要知道 mActions 中添加的 HandlerAction 在何時被消費掉了。
消費 HandlerAction 的地方,是 executeActions()
方法。
它最終,還是調用的 handler.postDelayed()
,這沒什么好說的,關鍵點在于 executeAction()
方法,是在什么時候被調用的。
executeAction()
是被 TraversalRunnable 調用 doTraversa()
,在doTraversa()
方法中,進行調用的。而 TraversalRunnable 又是通過 Choreographer.postCallBack()
去循環調用的。這個 Choreographer 通過 doScheduleCallback()
發送一個 MSG_DO_SCHEDULE_CALLBACK 類型的消息循環調用,間隔就是一個 VSync 的間隔。
關于 Choreographer ,不是本文的重點,有興趣可以單獨了解一下。
而在 Api23 以下,executeAction()
是會被循環調用,基本上其內的 mActions 中,只要有未執行的 Runnable 立刻就會被消費掉。
所以在 Api23 以下的設備上,無論如何 View.post()
基本上是靠譜的,post 出去的 Runnable 都會有機會執行到。
2.3 Api24 的細節
再來看看在 Api24 中的實現細節,在 Api24 中,調用的是 getRunQueue().post()
方法,它操作的是一個 HandlerActionQueue 對象。
內部的結構其實和 Api23 很像,也是維護了一個 HandlerAction 的數組 mActions 。
最終消費 mActions 的地方,依然是一個 executeActions()
方法。
回到根本的問題,executeActions()
方法在什么時機會被調用到,繼續追查可以看到它在 View.dispatchAttachedToWindow()
方法中,會被調用。
既然,executeActions()
方法,在 Api24 及以上,只會在 dispatchAttachedToWindow()
的方法中,才有機會被調用到,而 View.dispatchAttachedToWindow()
方法,只有在這個 View 通過 addView()
方法,或者原本寫在頁面布局的 xml 中(實際上也是調用的 addView()
),加入到一個 ViewGroup 的時候,才會被調用到。
這就導致,如果你只是通過 new 或者使用 LayoutInflater 創建了一個 View ,而沒有將它通過 addView()
加入到 布局視圖中去,你通過這個 View.post()
出去的 Runnable ,將永遠不會被執行到。 這也就是到了 Api24 下,View.post()
表現的現象不一致的緣故。
三、舉個例子說明問題
既然只是復現這個問題,秉承最小改動原則,構造一個最簡單的場景,單獨 new 一個 View 出來,然后通過它去調用 post()
方法,看看執行的結果。
可以看到,這里直接 new 了一個 View,然后 post 出去了一個 Runnable ,間隔 10s 之后,將這個 View 加入到根布局中。
看看在 Api 23 下的執行效果:
可以看到,在 Api 23一下,這里是 Api19,新 new 出來的 View 對象,post 出去的 Runnable ,會立即得到執行,不需要等待 addView()
的執行。
再來看看在 Api24 下的執行效果:
從執行時間上可以看出來,post 出去的 Runnable ,并不是立即被執行了,而是等到了 addView()
的調用之后,才被執行的,這個中間正好被間隔了 10s。
據說這個問題,在 Android 8.0 上又被修改回去了,專門找了一款 8.0 的設備試試運行結果,如下圖:
25 是 Android 8.0 的預覽版,這里可以看到,依然是和在 7.0 上的表現一樣,會等到最終 addView()
的時候再執行,正式版不知道會不會有所改動,這個還有待驗證。
基本上確定,受到影響的是 Android Api 24+,但是依然是開發者需要注意的,畢竟發布出去的 App ,具體運行在什么設備上,這就不是我們能決定的了。
四、小結
View.post()
方法,在不同版本的差異,根本原因還是在于 Api23 和 Api24 中,executeActions()
方法的調用時機不同,導致 View 在沒有 mAttachInfo 對象的時候,表現不一樣了。
所以我們在使用的過程中需要慎用,區分出實際使用的場景,一般規范自己的代碼即可:
- 動態創建的 View ,如果視條件去決定是否加入到根布局中,則不要使用它來調用 post() 方法。
- 盡量避免使用 View.post() 方法,可以直接使用 Handler.post() 方法來替代。