1. 程式人生 > >為什麼Android必須在主執行緒更新UI?

為什麼Android必須在主執行緒更新UI?

為什麼Android必須在主執行緒更新UI?

站在各位大牛的肩膀上,謝謝!

正常情況下,Android需要在UI執行緒更新UI,然鵝,在特殊情況下,子執行緒也能更新UI不在討論之列,可參考Android中子執行緒真的不能更新UI嗎?這篇文章主要講一下個人理解的正常情況下為什麼不能在非UI執行緒更新UI。

先拿一句話來鎮樓

android.view.ViewRootImpl$CalledFromWrongThreadException: 
Only the original thread that created a view hierarchy can touch its views.

然後晒出Android官方的一句話來說:“The Android UI toolkit is not thread-safe and the view must always be manipulated on the UI thread.” 因為Android UI操作並不是執行緒安全的,並且這些操作必須在UI執行緒執行。我們就主要分析一下這句話背後包含的含義。

Android螢幕重新整理機制。

1, 介面上任何一個 View 的重新整理請求最終都會走到 ViewRootImpl 中的 scheduleTraversals() 裡來安排一次遍歷繪製 View 樹的任務;

2, scheduleTraversals() 會先過濾掉同一幀內的重複呼叫,在同一幀內只需要安排一次遍歷繪製 View 樹的任務即可,這個任務會在下一個螢幕重新整理訊號到來時呼叫 performTraversals() 遍歷View 樹,遍歷過程中會將所有需要重新整理的 View 進行重繪;

3接著 scheduleTraversals() 會往主執行緒的訊息佇列中傳送一個同步屏障,攔截這個時刻之後所有的同步訊息的執行,但不會攔截非同步訊息,以此來儘可能的保證當接收到螢幕重新整理訊號時可以儘可能第一時間處理遍歷繪製 View 樹的工作;

4 發完同步屏障後 scheduleTraversals() 才會開始安排一個遍歷繪製 View 樹的操作,作法是把 performTraversals() 封裝到 Runnable 裡面,然後呼叫 Choreographer 的 postCallback() 方法;

5,postCallback() 方法會先將這個 Runnable 任務以當前時間戳放進一個待執行的佇列裡,然後如果當前是在主執行緒就會直接呼叫一個native 層方法,如果不是在主執行緒,會發一個最高優先順序的 message 到主執行緒,讓主執行緒第一時間呼叫這個 native 層的方法;

6, native 層的這個方法是用來向底層註冊監聽下一個螢幕重新整理訊號,當下一個螢幕重新整理訊號發出時,底層就會回撥 Choreographer 的onVsync() 方法來通知上層 app;

7,onVsync() 方法被回撥時,會往主執行緒的訊息佇列中傳送一個執行 doFrame() 方法的訊息,這個訊息是非同步訊息,所以不會被同步屏障攔截住;

8,doFrame() 方法會去取出之前放進待執行佇列裡的任務來執行,取出來的這個任務實際上是 ViewRootImpl 的 doTraversal() 操作;

9,上述第4步到第8步涉及到的訊息都手動設定成了非同步訊息,所以不會受到同步屏障的攔截;

10,doTraversal() 方法會先移除主執行緒的同步屏障,然後呼叫 performTraversals() 開始根據當前狀態判斷是否需要執行performMeasure() 測量、perfromLayout() 佈局、performDraw() 繪製流程,在這幾個流程中都會去遍歷 View 樹來重新整理需要更新的View;

View重新整理流程時序圖詳細資訊可參考Android 螢幕重新整理機制

文章說的很詳細,簡單來說,
就是當View的重新整理操作觸發時,會統一先註冊到ViewRootImpl中;
螢幕每隔16.6ms觸發一次重新整理,這個訊號會通知ViewRootImpl進行UI重新整理,
然後在ViewRootImpl中實際執行View的測量,繪製的一系列操作

二,UI執行緒到底是什麼

在上述4-8步中,是在某個執行緒完成的,這個執行緒就是實際上的UI執行緒。UI執行緒的名字的意義是,遍歷View樹,測量繪製View,並將資料寫入到buffer的執行緒。在一個APP啟動的時候,會建立一個Main Thread,這時候仍要繪製頁面,因此這個Main Thread和UI Thread就是同一個執行緒。所以Main Thread和UI Thread相當於同一個概念。

三,為什麼說必須UI執行緒更新UI

對於開發來說的更新UI,實際上是將View的變化,通知到ViewRootImpl,由ViewRootImpl實現後續操作。這個通知ViewRootImpl的操作包括
1,invalidate(請求重繪)
2,requestLayout(重新佈局)
3,requestFocus(請求焦點)
4,startActivity(開啟新介面)
5,onRestart(重新開啟介面)
6,KeyEvent(遙控器事件,本質上是焦點導致的重新整理)
7,Animation(各種動畫,本質上是請求重繪導致的重新整理)
8,RecyclerView滑動(頁面滑動,本質上是動畫導致的重新整理)
9,setAdapter(各種adapter的更新)
10,…………
這些操作所在的執行緒必須和UI執行緒在同一個執行緒。否則就會出現,UI執行緒正在繪製頁面,而另外能操作UI的執行緒對View進行了操作,當UI執行緒繪製完上方的View後,那麼這個被其他執行緒操作後的VIew的很有可能會覆蓋到其他View之上,這並不是我們想看到的結果。

最後

“Android UI操作並不是執行緒安全的”這句話,個人理解是如果ViewRootImpl不強制檢查執行緒,那麼,任何都可以更改View的屬性,無法保證同一幀資料的完整性。
或許控制View繪製的執行緒和通知View更新的執行緒必須是同一執行緒,比主執行緒更新UI更能表達出這層一次吧。

參考資料:
https://www.cnblogs.com/dasusu/p/8311324.html
https://blog.csdn.net/aigestudio/article/details/43449123