Android Handler 機制(二):Handler 機制深入探究問題梳理


一、‘非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...》

《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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM