一、‘非UI線程更新UI’問題探究
Android開發的時候非UI線程不能更新UI,這個是大家都知道的開發常識。但是當問到為什么?可能我們就會有些含糊了。
本文我們就針對這個問題進行探討並進行一定的思維發散,來加深我們對Android界面刷新機制的理解。
1. UI線程的工作機制
主線程的工作機制可以概況為 生產者 - 消費者 - 隊列 模型。
2. 為什么UI線程不設計成線程安全的
總所周知,如果設計成線程安全的,那性能肯定是大打折扣的,而UI更新的要求有如下特性:
- UI是具有可變性的,甚至是高頻可變。
- UI對響應時間很敏感,這就要求UI操作必須要高效。
- UI組件必須批量繪制來保證效率。
所以為了保證渲染性能,UI線程不能設計成線程安全的。Android設計了Handler機制來更新UI是避免多個子線程更新UI導致的UI錯亂的問題,也避免了通過加鎖機制設計成線程安全的,因為那樣會導致性能下降的很厲害。
3. 子線程能創建Handler嗎?
能。但是需要先調用Looper.prepare()方法,否則會拋出運行時異常[Can't create handler inside thread that has not call Looper.prepared()]。
4. 子線程的Looper和主線程的Looper有什么區別
子線程的Looper可以退出的,主線程的Looper時不能退出的。
5. 非UI線程一定不能更新UI嗎?
答:不一定。
說明:我們知道在Android提供的SurfaceView、GLSurfaceView里面是都能在非UI線程更新UI的。
並且在一些特定的場景下,子線程更新View也是能更新成功的。
例如,下面的代碼在子線程中更新界面是可以成功的:
import android.app.Activity; import android.os.Bundle; import android.widget.Button; public class TestActivity extends Activity { Button btn = null; public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); btn = (Button) findViewById(R.id.Button01); new TestThread(btn).start(); } class TestThread extends Thread {
Button btn = null;
public TestThread(Button btn) { this.btn = btn; } @Override public void run() { btn.setText("TestThread.run"); } } }
當我們深入分析其原理的時候,就可以知道,能否更新成功的關鍵點在於是否會觸發checkThead()導致更新失敗,拋出異常:
void checkThread() { if (mThread != Thread.currentThread()) { throw new CalledFromWrongThreadException("Only the original thread that created a view hierarchy can touch its views."); } }
而在ViewRootImpl中,會有這些方法調用到checkThread()方法:
經過分析,最終可以得到,在子線程中給TextView setText 不會拋出異常的兩個場景:
1:TextView 還沒來得及加入到ViewTree中
2:TextView已經被加入了ViewTree,但是被設置了固定寬高,且開啟了硬件加速
子線程操作View 確實不一定導致Crash,那是因為剛好滿足一定的條件並沒有觸發checkThread機制,但這並不代表我們在開發過程中可以這么寫,其實我們還是應該遵循google的建議,更新UI始終在UI線程里去做。
推薦資料:
《Android 子線程更新TextView的text 不拋出異常原因 分析總結》
二、Handler發送消息的delay設置是否可靠?
答案是:不可靠。
原因:當Handler所屬的線程(UI線程)要處理的內容非常多,當Looper出現事件積壓的時候會使得delay不可靠。如ANR的出現就是一個最極端的代表例子。
為了理解為何在事件擠壓的時候,handler會出現delay的不可靠,這里我們就加入一些核心邏輯的分析,來幫助我們進行此問題的探究。
三、Handler機制下消息隊列MessageQueue的優化
一般情況下,可以考慮從以下幾個方面進行優化
- 對消息隊列中重復消息的過濾,用於控制一些操作的頻率,既保證用戶體驗又保證性能(使用Handler Api)。
- 對消息隊列中的互斥消息進行取消,用於類似開關之類的操作(使用Handler Api)。
- 消息池復用,減少創建Message實例的開銷(Handler機制自帶消息池)。
- IdleHandler的合理使用,利用空閑的時機進行一些業務邏輯的處理(視業務而定)。
四、主線程的Looper為什么不會導致ANR
1. ANR的產生條件
- 輸入超時:5秒(最常見)
- 服務超時:前台服務20s、后台服務200s。
- 廣播隊列超時:前台廣播10秒、后台廣播60秒。
- ContentProvider超時:10秒
其實最終就是最終由系統的Api發送的Message,告知相關組件發生了ANR。
這里我們使用Service舉例,當Service超時的時候,發送的Message.what就是ActivityManagerService.SHOW_NOT_RESPONDING_UI_MSG。
ANR也算是Android系統提醒開發者程序編寫有問題的一個機制。
補充: WatchDog、BlockCanary、AndroidGodEye 都是比較不錯的卡頓異常檢測工具。
BlockCanary :https://github.com/markzhai/AndroidPerformanceMonitor(將近4年未更新維護,不推薦使用,但可以通過閱讀其代碼,來加深理解)
ANR-WatchDog:https://github.com/SalomonBrys/ANR-WatchDog
AndroidGodEye:https://github.com/Kyson/AndroidGodEye
2.Looper不會導致應用ANR的本質原因是什么?
當我們熟悉了Looper的工作機制,我們就會知道主線程執行的操作就是執行Loop.loop()不斷的處理消息。Looper是進程上的一個概念,ANR是程序執行到某一個環節對開發者占用主線程耗時的一個監控機制,是應用沒有在規定時間內完成AMS指定的任務導致的。ANR產生的根本原因是不是因為主線程Looper循環,而是因為主線程中有耗時任務。
3.Looper為什么不會導致CPU占用率高?(延伸)
這是因為使用了Linux底層的pipe管道機制和epoll機制,在主線程的MessageQueue沒有消息時,便阻塞在loop的queue.next()中的nativePollOnce() 方法里,在沒有消息時阻塞線程並進入休眠釋放cpu資源,有消息時喚醒線線程,這樣不會導致CPU占用率高。
總結:Android的應用層通過Message.java實現隊列,利用管道和epoll機制實現線程狀態的管理,配合起來實現了Android主線程的消息隊列模型。
拓展學習:
Linux 下 Epoll 機制概述:https://www.cnblogs.com/renhui/articles/12868221.html
Android中的Looper與epoll:https://www.jianshu.com/p/7bc2b86c4d89