正常情況下,Android需要在UI線程更新UI,然鵝,在特殊情況下,子線程也能更新UI不在討論之列,可參考Android中子線程真的不能更新UI嗎?這篇文章主要講一下個人理解的正常情況下為什麼不能在非UI線程更新UI。
先拿一句話來鎮樓android.view.ViewRootImpl$CalledFromWrongThreadException:Onlytheoriginalthreadthatcreatedaviewhierarchycantouchitsviews.然後曬出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的刷新操作觸發時,會統一先註冊到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
原文鏈接: https://blog.csdn.net/qq_39154578/article/details/83782287
-- END --
進技術交流群,掃碼添加我的微信:Byte-Flow
獲取相關資料和源碼
推薦:
Android FFmpeg 實現帶濾鏡的微信小視頻錄製功能
全網最全的 Android 音視頻和 OpenGL ES 乾貨,都在這了
一文掌握 YUV 圖像的基本處理
抖音傳送帶特效是怎麼實現的?
所有你想要的圖片轉場效果,都在這了
面試官:如何利用 Shader 實現 RGBA 到 NV21 圖像格式轉換?
我用 OpenGL ES 給小姐姐做了幾個抖音濾鏡