換個姿勢,帶着問題看Handler

換個姿勢,帶着問題看Handler

Handler,老生常談,網上關於它的文章也是“氾濫成災”,但實際開發很少手寫Handler,
畢竟,寫異步,RxAndroid鏈式調用 或者 Kotlin協程同步方式寫異步代碼 還是挺香的。
不過,面試官都喜歡章口就來一句:

當然,應對方法也很簡單,找一篇《…Handler詳解》之類的文章,背熟即可~
不過,對於我這種好刨根問底的人來說,自己過一遍源碼心理才踏實,
而且,我發現「帶着問題」看源碼,思考理解本質,印象更深,收穫更多,遂有此文。

羅列下本文提及的問題,如有答不出的可按需閱讀,謝謝~

  • 1、Handler問題三連:是什麼?有什麼用?爲什麼要用Handler,不用行不行?
  • 2、Android UI更新機制(GUI) 爲何設計成了單線程的?
  • 3、真的只能在主(UI)線程中更新UI嗎?
  • 4、真的不能在主(UI)線程中執行網絡操作嗎?
  • 5、Handler怎麼用?
  • 6、爲什麼建議使用Message.obtain()來創建Message實例?
  • 7、爲什麼子線程中不可以直接new Handler()而主線程中可以?
  • 8、主線程給子線程的Handler發送消息怎麼寫?
  • 9、HandlerThread實現的核心原理?
  • 10、當你用Handler發送一個Message,發生了什麼?
  • 11、Looper是怎麼揀隊列裏的消息的?
  • 12、分發給Handler的消息是怎麼處理的?
  • 13、IdleHandler是什麼?
  • 14、Looper在主線程中死循環,爲啥不會ANR?
  • 15、Handler泄露的原因及正確寫法
  • 16、Handler中的同步屏障機制
  • 17、Android 11 Handler相關變更

0x1、Handler問題三連


1.Handler是什麼


答:Android定義的一套 子線程與主線程間通訊消息傳遞機制


2.Handler有什麼用

答:把子線程中的 UI更新信息傳遞 給主線程(UI線程),以此完成UI更新操作。


3.爲什麼要用Handler,不用行不行


答:不行,Handler是android在設計之初就封裝了一套消息創建、傳遞、處理機制。

Android要求

我們在主線程(UI)線程中更新UI

是要求,建議,不是規定,你不聽,硬要:

在子線程中更新UI,也是可以的!!!

比如,在一個子線程中,創建一個對話框:

運行後:

沒有報錯,對話框正常彈出,而我們平時在 子線程中更新UI 的錯:

異常翻譯:只有創建這個view的線程才能操作這個view
引起原因:在子線程中更新了主線程創建的UI;
也就是說:子線程更新UI也行,但是隻能更新自己創建的View;
換句話說:Android的UI更新(GUI)被設計成了單線程

你可能會問,爲啥不設計成多線程?

答:多個線程同時對同一個UI控件進行更新,容易發生不可控的錯誤!

那麼怎麼解決這種線程安全問題?

答:最簡單的 加鎖,不是加一個,是每層都要加鎖(用戶代碼→GUI頂層→GUI底層…)這樣也意味着更多的 耗時,UI更新效率降低;如果每層共用同一把鎖的話,其實就是單線程

所以,結論是:

Android沒有采用「線程鎖」,而是採用「單線程消息隊列機制」,實現了一個「僞鎖

這個疑問解決了,再說一個網上很常見的主線程更新UI的例子:

上面這段代碼 直接在子線程中更新了UI,卻沒有報錯:

這是要打臉嗎?但如果在子線程中加句線程休眠模擬耗時操作的話:

程序就崩潰了,報錯如下:

前面說了 Android的UI更新被設計成單線程,這裏妥妥滴會報錯,但卻發生在延遲執行後?
限於篇幅,這裏就不去跟源碼了,直接說原因:

ViewRootImponCreate() 時還沒創建;
onResume()時,即ActivityThreadhandleResumeActivity() 執行後才創建,
調用 requestLayout(),走到 checkThread() 時就報錯了。

可以打個日誌簡單的驗證下:

加上休眠

行吧,以後去面試別人問「子線程是不是一定不可以更新UI」別傻乎乎的說是了。


4.引生的另一個問題


說到「只能在主線程中更新UI」我又想到另一個問題「不能在主線程中進行網絡操作

上述代碼運行直接閃退,日誌如下:

NetworkOnMainThreadException:網絡請求在主線程進行異常。

em… 真的不能在主線程中做網絡操作嗎?

onCreate() 的 setContentView() 後插入下面兩句代碼:

運行下看看:

這…又打臉?先說下 StrictMode(嚴苟模式)

Android 2.3 引入,用於檢測兩大問題:ThreadPolicy(線程策略) 和 VmPolicy(VM策略)

相關方法如下

把嚴苟模式的網絡檢測關了,就可以 在主線程中執行網絡操作了,不過一般是不建議這樣做的:

在主線程中進行耗時操作,可能會導致程序無響應,即 ANR (Application Not Responding)。

至於常見的ANR時間,可以在對應的源碼中找到:

// ActiveServices.java → Service服務
static final int SERVICE_TIMEOUT = 20*1000;     // 前臺
static final int SERVICE_BACKGROUND_TIMEOUT = SERVICE_TIMEOUT * 10;     // 後臺

// ActivityManagerService.java → Broadcast廣播、InputDispatching、ContentProvider
static final int BROADCAST_FG_TIMEOUT = 10*1000;    // 前臺
static final int BROADCAST_BG_TIMEOUT = 60*1000;    // 後臺
static final int KEY_DISPATCHING_TIMEOUT = 5*1000;  // 關鍵調度
static final int CONTENT_PROVIDER_PUBLISH_TIMEOUT = 10*1000;    // 內容提供者

時間統計區間:

  • 起點System_Server 進程調用 startProcessLocked 後調用 AMS.attachApplicationLocked()
  • 終點Provider 進程 installProviderpublishContentProviders 調用到 AMS.publishContentProviders()
  • 超過這個時間,系統就會殺掉 Provider 進程。

0x2、Handler怎麼用


1.sendMessage() + handleMessage()


代碼示例如下

黃色部分會有如下警告

Handler不是靜態類可能引起「內存泄露」,原因以及正確寫法等下再講。
另外,建議調用 Message.obtain() 函數來獲取一個Message實例,爲啥?點進源碼:

從源碼,可以看到obtain()的邏輯:

  • 加鎖,判斷Message池是否爲空
  • ① 不爲空,取一枚Message對象,正在使用標記置爲0,池容量-1,返回;
  • ② 爲空,新建一個Message對象,返回;

Message池複用Message,可以「避免重複創建實例對象」節約內存,
另外,Message池其實是一個「單鏈表結構

上述獲取消息池的邏輯:

定位到下述代碼,還可以知道:池的容量爲50,超過

然後問題來了,Message信息什麼時候加到池中?

答:當Message 被Looper分發完後,會調用 recycleUnchecked()函數,回收沒有在使用的Message對象。

標誌設置爲FLAG_IN_USE,表示正在使用,相關屬性重置,加鎖,判斷消息池是否滿,
未滿,「單鏈表頭插法」把消息插入到表頭。(獲取和插入都發生在表頭,像不像 ~)


2.post(runnable)


代碼示例如下

跟下post():

實際上調用了 sendMessageDelayed() 發送消息,只不過延遲秒數爲0,
那Runnable是怎麼變成Message的呢?跟下getPostMessage()

噢,獲取一個新的Message示例後,把 Runnable 變量的值賦值給 callback屬性


3.附:其他兩個種在子線程中更新UI的方法


activity.runOnUiThread()

view.post() 與 view.postDelay()


0x3、Handler底層原理解析


終於來到稍微有點技術含量的環節,在觀摩源碼瞭解原理前,先說下幾個涉及到的類。


1.涉及到的幾個類



2.前戲


在我們使用Handler前,Android系統已爲我們做了一系列的工作,其中就包括了

創建「Looper」和「MessageQueue」對象

上圖中有寫:ActivityThreadmain函數是APP進程的入口,定位到 ActivityThread → main函數

定位到:Looper → prepareMainLooper函數

定位到:Looper → prepare函數

定位到:Looper → Looper構造函數

另外這裏的 mQuitAllowed 變量,直譯「退出允許」,具體作用是?跟下 MessageQueue

em…用來 防止開發者手動終止消息隊列,停止Looper循環


3.消息隊列的運行


前戲過後,創建了Looper與MessageQueue對象,接着調用Looper.loop()開啓輪詢。
定位到:Looper → loop函數

接着有幾個問題,先是這個 myLooper() 函數:

這裏的 ThreadLocal線程局部變量JDK提供的用於解決線程安全的工具類
作用爲每個線程提供一個獨立的變量副本以解決併發訪問的衝突問題
本質

每個Thread內部都維護了一個ThreadLocalMap,這個map的key是ThreadLocal,
value是set的那個值。get的時候,都是從自己的變量中取值,所以不存在線程安全問題。

主線程和子線程的Looper對象實例相互隔離的!!!
意味着:主線程和子線程Looper不是同一個!!!

知道這個以後,有個問題就解惑了:

爲什麼子線程中不能直接 new Handler(),而主線程可以?

答:主線程與子線程不共享同一個Looper實例,主線程的Looper在啓動時就通過
prepareMainLooper() 完成了初始化,而子線程還需要調用 Looper.prepare()
Looper.loop()開啓輪詢,否則會報錯,不信,可以試試:

直接就奔潰了~

加上試試?

可以,程序正常運行,沒有報錯。
對了,既然說Handler用於子線程和主線程通信,試試在主線程中給子線程的Handler發送信息,修改一波代碼:

運行,直接報錯:

原因:多線程併發的問題,當主線程執行到sendEnptyMessage時,子線程的Handler還沒有創建
一個簡單的解決方法是:主線程延時給子線程發消息,修改後的代碼示例如下:

運行結果如下:

可以,不過其實Android已經給我們封裝好了一個輕量級的異步類「HandlerThread


4.HandlerThread


HandlerThread = 繼承Thread + 封裝Looper

使用方法很簡單,改造下我們上面的代碼:

用法挺簡單的,源碼其實也很簡單,跟一跟:

剩下一個quit()和quitSafely()停止線程,就不用說了,所以HandlerThread的核心原理就是:

  • 繼承Thread,getLooper()加鎖死循環wait()堵塞線程;
  • run()加鎖等待Looper對象創建成功,notifyAll()喚醒線程
  • 喚醒後,getLooper返回由run()中生成的Looper對象

是吧,HandlerThread的實現原理竟簡單如斯,另外,順帶提個醒!!!

Java中所有類的父類是 Object 類,裏面提供了wait、notify、notifyAll三個方法;
Kotlin 中所有類的父類是 Any 類,裏面可沒有上述三個方法!!!
所以你不能在kotlin類中直接調用,但你可以創建一個java.lang.Object的實例作爲lock
去調用相關的方法。

代碼示例如下

private val lock = java.lang.Object()

fun produce() = synchronized(lock) {
    while(items>=maxItems) { 
        lock.wait()
    }
    Thread.sleep(rand.nextInt(100).toLong())
    items++
    println("Produced, count is$items:${Thread.currentThread()}")
    lock.notifyAll()
}

fun consume() = synchronized(lock) {
    while(items<=0) {
        lock.wait()
    }
    Thread.sleep(rand.nextInt(100).toLong())
    items--
    println("Consumed, count is$items:${Thread.currentThread()}")
    lock.notifyAll()
}

5.當我們用Handler發送一個消息發生了什麼?


扯得有點遠了,拉回來,剛講到 ActivityThreadmain函數中調用 Looper.prepareMainLooper
完成主線程 Looper初始化,然後調用 Looper.loop() 開啓消息循環 等待接收消息

嗯,接着說下 發送消息,上面說了,Handler可以通過sendMessage()和 post() 發送消息,
上面也說了,源碼中,這兩個最後調用的其實都是 sendMessageDelayed()完成的:

第二個參數:當前系統時間+延時時間,這個會影響「調度順序」,跟 sendMessageAtTime()

獲取當前線程Looper中的MessageQueue隊列,判空,空打印異常,否則返回 enqueueMessage(),跟:

這裏的 mAsynchronous異步消息的標誌,如果Handler構造方法不傳入這個參數,默認false:
這裏涉及到了一個「同步屏障」的東西,等等再講,跟:MessageQueue -> enqueueMessage

如果你瞭解數據結構中的單鏈表的話,這些都很簡單。
不瞭解的可以移步至【面試】數據結構與算法(二) 學習一波~


6.Looper是怎麼揀隊列的消息的?


MessageQueue裏有Message了,接着就該由Looper分揀了,定位到:Looper → loop函數

// Looper.loop()
final Looper me = myLooper();           // 獲得當前線程的Looper實例
final MessageQueue queue = me.mQueue;   // 獲取消息隊列
for (;;) {                              // 死循環
        Message msg = queue.next();     // 取出隊列中的消息
        msg.target.dispatchMessage(msg); // 將消息分發給Handler
}

queue.next() 從隊列拿出消息,定位到:MessageQueue -> next函數

這裏的關鍵其實就是:nextPollTimeoutMillis,決定了堵塞與否,以及堵塞的時間,三種情況:

等於0時,不堵塞,立即返回,Looper第一次處理消息,有一個消息處理完 ;
大於0時,最長堵塞等待時間,期間有新消息進來,可能會了立即返回(立即執行);
等於-1時,無消息時,會一直堵塞;

Tips:此處用到了Linux的pipe/epoll機制:沒有消息時阻塞線程並進入休眠釋放cpu資源,有消息時喚醒線程;


7.分發給Handler的消息是怎麼處理的?


通過MessageQueuequeue.next()揀出消息後,調用msg.target.dispatchMessage(msg)
把消息分發給對應的Handler,跟到:Handler -> dispatchMessage

到此,關於Handler的基本原理也說的七七八八了~


8.IdleHandler是什麼?


評論區有小夥子說:把idleHandler加上就完整了,那就安排下吧~
MessageQueue 類中有一個 static 的接口 IdleHanlder

翻譯下注釋:當線程將要進入堵塞,以等待更多消息時,會回調這個接口;
簡單點說:當MessageQueue中無可處理的Message時回調
作用:UI線程處理完所有View事務後,回調一些額外的操作,且不會堵塞主進程;

接口中只有一個 queueIdle() 函數,線程進入堵塞時執行的額外操作可以寫這裏,
返回值是true的話,執行完此方法後還會保留這個IdleHandler,否則刪除。

使用方法也很簡單,代碼示例如下:

輸出結果如下

看下源碼,瞭解下具體的原理:MessageQueue,定義了一個IdleHandler的列表和數組

定義了添加和刪除IdleHandler的函數:

next() 函數中用到了 mIdleHandlers 列表:

原理就這樣,一般使用場景:繪製完成回調,例子可參見:
《你知道 android 的 MessageQueue.IdleHandler 嗎?》
也可以在一些開源項目上看到IdleHandler的應用:
http://useof.org/java-open-source/android.os.MessageQueue.IdleHandler


0x4、一些其他問題


1.Looper在主線程中死循環,爲啥不會ANR?

答:上面說了,Looper通過queue.next()獲取消息隊列消息,當隊列爲空,會堵塞,
此時
主線程
也堵塞在這裏,好處是:main函數無法退出,APP不會一啓動就結束!

你可能會問:主線程都堵住了,怎麼響應用戶操作和回調Activity聲明週期相關的方法?

答:application啓動時,可不止一個main線程,還有其他兩個Binder線程ApplicationThreadActivityManagerProxy,用來和系統進程進行通信操作,接收系統進程發送的通知。

  • 當系統受到因用戶操作產生的通知時,會通過 Binder 方式跨進程通知 ApplicationThread;
  • 它通過Handler機制,往 ActivityThreadMessageQueue 中插入消息,喚醒了主線程;
  • queue.next() 能拿到消息了,然後 dispatchMessage 完成事件分發;
    PS:ActivityThread 中的內部類H中有具體實現

死循環不會ANR,但是 dispatchMessage 中又可能會ANR哦!如果你在此執行一些耗時操作
導致這個消息一直沒處理完,後面又接收到了很多消息,堆積太多,就會引起ANR異常!!!


2.Handler泄露的原因及正確寫法


上面說了,如果直接在Activity中初始化一個Handler對象,會報如下錯誤:

原因是

在Java中,非靜態內部類會持有一個外部類的隱式引用,可能會造成外部類無法被GC;
比如這裏的Handler,就是非靜態內部類,它會持有Activity的引用從而導致Activity無法正常釋放。

而單單使用靜態內部類,Handler就不能調用Activity裏的非靜態方法了,所以加上「弱引用」持有外部Activity。

代碼示例如下

private static class MyHandler extends Handler {
    //創建一個弱引用持有外部類的對象
    private final WeakReference<MainActivity> content;

    private MyHandler(MainActivity content) {
        this.content = new WeakReference<MainActivity>(content);
    }

    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        MainActivity activity= content.get();
        if (activity != null) {
            switch (msg.what) {
                case 0: {
                    activity.notifyUI();
                }
            }
        }
    }
}

轉換成Kotlin:(Tips:Kotlin 中的內部類,默認是靜態內部類,使用inner修飾才爲非靜態~)

private class MyHandler(content: MainActivity) : Handler() {
    //創建一個弱引用持有外部類的對象
    private val content: WeakReference<MainActivity> = WeakReference(content)

    override fun handleMessage(msg: Message) {
        super.handleMessage(msg)
        val activity = content.get()
        if (activity != null) {
            when (msg.what) {
                0 -> {
                    activity.notifyUI()
                }
            }
        }
    }
}

3.同步屏障機制


通過上面的學習,我們知道用Handler發送的Message後,MessageQueueenqueueMessage()按照 時間戳升序 將消息插入到隊列中,而Looper則按照順序,每次取出一枚Message進行分發,一個處理完到下一個。這時候,問題來了:有一個緊急的Message需要優先處理怎麼破?你可能或說**直接sendMessage()**不就可以了,不用等待立馬執行,看上去說得過去,不過可能有這樣一個情況:

一個Message分發給Handler後,執行了耗時操作,後面一堆本該到點執行的Message在那裏等着,這個時候你sendMessage(),還是得排在這堆Message後,等他們執行完,再到你!

對吧?Handler中加入了「同步屏障」這種機制,來實現「異步消息優先執行」的功能。

添加一個異步消息的方法很簡單:

  • 1、Handler構造方法中傳入async參數,設置爲true,使用此Handler添加的Message都是異步的;
  • 2、創建Message對象時,直接調用setAsynchronous(true)

一般情況下:同步消息和異步消息沒太大差別,但僅限於開啓同步屏障之前。可以通過 MessageQueuepostSyncBarrier 函數來開啓同步屏障:

行吧,這一步簡單的說就是:往消息隊列合適的位置插入了同步屏障類型的Message (target屬性爲null)
接着,在 MessageQueue 執行到 next() 函數時:

遇到target爲null的Message,說明是同步屏障,循環遍歷找出一條異步消息,然後處理。
在同步屏障沒移除前,只會處理異步消息,處理完所有的異步消息後,就會處於堵塞
如果想恢復處理同步消息,需要調用 removeSyncBarrier() 移除同步屏障:

在API 28的版本中,postSyncBarrier()已被標註hide,但依舊可在系統源碼中找到相關應用,比如:
爲了更快地響應UI刷新事件,在ViewRootImplscheduleTraversals函數中就用到了同步屏障:


4、Android 11 R Handler 變更


官方文檔https://developer.android.google.cn/reference/android/os/Handle

構造函數

  • Handler() 廢棄 → Handler(Looper.myLooper())
  • Handler(Handler.Callback callback) 廢棄 → Handler(Looper.myLooper(), callback)

Looper.prepareMainLooper () 廢棄
原因:主線程的Looper是由系統自動創建的,無需用戶自行調用。


參考文獻



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章