View.post() 不靠譜的地方你知道嗎?

承香墨影發表於2017-09-05

版權宣告:

本賬號釋出文章均來自公眾號,承香墨影(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() 做了什麼改動。

/p-postmethpd.png
/p-postmethpd.png

用 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() 到底幹了什麼?

/p-23postqueue.png
/p-23postqueue.png

post() 方法只是單純的將它包裝成一個 HandlerAction 物件,然後放入 mActions 這個 ArrayList 中。繼續追查下去就需要知道 mActions 中新增的 HandlerAction 在何時被消費掉了。

消費 HandlerAction 的地方,是 executeActions() 方法。

/p-23execute.png
/p-23execute.png

它最終,還是呼叫的 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 物件。

/p-24add.png
/p-24add.png

內部的結構其實和 Api23 很像,也是維護了一個 HandlerAction 的陣列 mActions 。

最終消費 mActions 的地方,依然是一個 executeActions() 方法。

/p-24execute.png
/p-24execute.png

回到根本的問題,executeActions() 方法在什麼時機會被呼叫到,繼續追查可以看到它在 View.dispatchAttachedToWindow() 方法中,會被呼叫。

/p-24done.png
/p-24done.png

既然,executeActions() 方法,在 Api24 及以上,只會在 dispatchAttachedToWindow() 的方法中,才有機會被呼叫到,而 View.dispatchAttachedToWindow() 方法,只有在這個 View 通過 addView() 方法,或者原本寫在頁面佈局的 xml 中(實際上也是呼叫的 addView()),加入到一個 ViewGroup 的時候,才會被呼叫到。

這就導致,如果你只是通過 new 或者使用 LayoutInflater 建立了一個 View ,而沒有將它通過 addView() 加入到 佈局檢視中去,你通過這個 View.post() 出去的 Runnable ,將永遠不會被執行到。 這也就是到了 Api24 下,View.post() 表現的現象不一致的緣故。

三、舉個例子說明問題

既然只是復現這個問題,秉承最小改動原則,構造一個最簡單的場景,單獨 new 一個 View 出來,然後通過它去呼叫 post() 方法,看看執行的結果。

/p-demo.png
/p-demo.png

可以看到,這裡直接 new 了一個 View,然後 post 出去了一個 Runnable ,間隔 10s 之後,將這個 View 加入到根佈局中。

看看在 Api 23 下的執行效果:

/p-demo23.png
/p-demo23.png

可以看到,在 Api 23一下,這裡是 Api19,新 new 出來的 View 物件,post 出去的 Runnable ,會立即得到執行,不需要等待 addView() 的執行。

再來看看在 Api24 下的執行效果:

/p-demo24.png
/p-demo24.png

從執行時間上可以看出來,post 出去的 Runnable ,並不是立即被執行了,而是等到了 addView() 的呼叫之後,才被執行的,這個中間正好被間隔了 10s。

據說這個問題,在 Android 8.0 上又被修改回去了,專門找了一款 8.0 的裝置試試執行結果,如下圖:

此處缺個圖。
此處缺個圖。

25 是 Android 8.0 的預覽版,這裡可以看到,依然是和在 7.0 上的表現一樣,會等到最終 addView() 的時候再執行,正式版不知道會不會有所改動,這個還有待驗證。

基本上確定,受到影響的是 Android Api 24+,但是依然是開發者需要注意的,畢竟釋出出去的 App ,具體執行在什麼裝置上,這就不是我們能決定的了。

四、小結

View.post() 方法,在不同版本的差異,根本原因還是在於 Api23Api24 中,executeActions() 方法的呼叫時機不同,導致 View 在沒有 mAttachInfo 物件的時候,表現不一樣了。

所以我們在使用的過程中需要慎用,區分出實際使用的場景,一般規範自己的程式碼即可:

  1. 動態建立的 View ,如果視條件去決定是否加入到根佈局中,則不要使用它來呼叫 post() 方法。
  2. 儘量避免使用 View.post() 方法,可以直接使用 Handler.post() 方法來替代。

公眾號二維碼.jpg
公眾號二維碼.jpg

相關文章