【面試官爸爸】繼續給我講講Handler?

前言

大家好,我是方木,一名在帝都干飯的程序員。本篇文章已經收錄到【面試官爸爸】系列,歡迎各位大兄弟捧場

歡迎關注我的微信公眾號 「方木 Rudy」,里面不僅有技術干貨,也記錄了一位北漂程序員掙扎向上的點點滴滴~

熟悉的黑影

一團黑影緩緩向我逼近

他走路不緊不慢,每一步都堅定而沉穩,像沉重的鼓槌一下一下敲在我的心上。稀疏的頭頂上閃耀著高 P 的光芒,銳利的眼神仿佛一下看穿了我的心虛

少頃,他坐在我對面

“來面試的?”

”對...對對對“

”那好,Handler 這東西你會吧?來給我講講吧“

什么是 Handler?

Handler 是 Android 的一種消息處理機制,與 Looper,MessageQueue 綁定,可以用來進行線程的切換。常用于接收子線程發送的數據并在主線程中更新 UI

你剛說 Handler 可以切換線程,它是怎么實現的?

“切換線程”其實是“線程通信”的一種。為了保證主線程不被阻塞,我們常常需要在子線程執行一些耗時任務,執行完畢后通知主線程作出相應的反應,這個過程就是線程間通信。

Linux 有一種進程間通信的方式叫消息隊列,簡單來說當兩個進程想要通信時,一個進程將消息放入隊列中,另一個進程從這個隊列中讀取消息,從而實現兩個進程的通信。

Handler 就是基于這一設計而實現的。在 Android 的多線程中,每個線程都有一個自己的消息隊列,線程可以開啟一個死循環不斷地從隊列中讀取消息。

當 B 線程要和 A 線程通信時,只需要往 A 的消息隊列中發送消息,A 的事件循環就會讀取這一消息從而實現線程間通信

呦呵,不錯嘛~ 你剛提到了事件循環和消息隊列,他們是怎么實現的呢?

Android 的事件循環和消息隊列是通過 Looper 類來實現的

Looper.prepare() 是一個靜態方法。它會構建出一個 Looper,同時創建一個 MessageQueue 作為 Looper 的成員變量。MessageQueue 是存放消息的隊列

當調用 Looper.loop() 方法時,會在線程內部開啟一個死循環,不斷地從 MessageQueue 中讀取消息,這就是事件循環

每個 Handler 都與一個 Looper 綁定,Looper 包含 MessageQueue

那這個 Looper 被存放在哪里呢?

Looper 是存放在線程中的。但如何把 Looper 存放在線程中就引入了 Android 消息機制的另一個重點 --- ThreadLocal

前面我們提到。Looper.prepare() 方法會創建出一個 Looper,它其實還做了一件事,就是將 Looper 放入線程的局部變量 ThreadLocal 中。

// Looper.java
private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        // sThreadLocal是一個靜態對象,類型是ThreadLocal<Looper>
        sThreadLocal.set(new Looper(quitAllowed));
 }

那么問題來了,什么是 ThreadLocal 呢?

ThreadLocal 又稱線程的局部變量。它最大的神奇之處在于,一個 ThreadLocal 實例在不同的線程中調用 get 方法可以取出不同的值。 用一個例子來表示這種用法:

fun main() {
    val threadLocal = ThreadLocal<Int>()
    threadLocal.set(100)

    Thread {
        threadLocal.set(20)
        println("子線程1 ${threadLocal.get()}")
    }.start()

    Thread {
        println("子線程2 ${threadLocal.get()}")
    }.start()

    println("主線程: ${threadLocal.get()}")

}

// 運行結果:
子線程1 20
主線程: 100
子線程2 null

ThreadLocal 的核心是 set 方法,它的作用總結成一句話就是:

ThreadLocal.set 可以將一個實例變成線程的成員變量

看一下源碼

// ThreadLocal.java
public void set(T value) {
        // ① 獲取當前線程對象
        Thread t = Thread.currentThread();
        // ② 獲取線程的成員屬性map
        ThreadLocalMap map = getMap(t);
        // ③ 將value放入map中,如果map為空則創建map
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
}

方法很簡單,就是根據當前線程獲取線程的一個 map 對象,然后把 value 放入 map 中,達到將 value 變成線程的成員變量的目的

多個 Theadlocal 將多個變量變成線程的成員變量。于是線程就用 ThreadlLocalMap 來管理,key 就是 threadLocal

知道了它 set 方法的奧秘,get 方法也就很簡單啦

//ThreadLocal.java
public T get() {
        // ① 獲取當前線程對象
        Thread t = Thread.currentThread();
        // ② 獲取線程對象的Map
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                 // ③ 獲取之前存放的value
                return result;
            }
        }
        return setInitialValue();
    }

和 set 方法差不多,區別就是一個將 value 寫入 map,一個從 map 中讀取 value。哼哼

不錯不錯,ThreadLocal 就是這么簡單直接。那你說說為什么要將 ThreadLocal 作為 Looper 的設置和獲取工具呢?

因為 Looper 要放在線程中的,每個線程只需要一個事件循環,只需要一個 Looper。事件循環是個死循環,多余的事件循環毫無意義。ThreadLocal.set 可以將 Looper 設置為線程的成員變量

同時為了方便在不同線程中獲取到 Looper,Android 提供了一個靜態對象 Looper.sThreadLocal。這樣在線程內部調用 sThreadLocal.get 就可以獲取線程對應的 Looper 對象

綜上所述,使用 ThreadLocal 作為 Looper 的設置和獲取工具是十分方便合理的

好,你剛說 Looper 是個死循環是吧,如果消息隊列中沒有消息了,這個死循環會一直“空轉”嗎?

當然不會!如果事件循環中沒有消息要處理但仍然執行循環,相當于無意義的浪費 CPU 資源!Android 是不允許這樣的

為了解決這個問題,在 MessageQueue 中,有兩個 native 方法,nativePollOnce 和 nativeWake。

nativePollOnce 表示進行一次輪詢,來查找是否有可以處理的消息,如果沒有就阻塞線程,讓出 CPU 資源

nativeWake 表示喚醒線程

所以這兩個方法的調用時機也就顯而易見了

// MessageQueue.java
boolean enqueueMessage(Message msg, long when) {
    ···
    if (needWake) {
        nativeWake(mPtr);
    }
    ···
}

在 MessageQueue 類中,enqueueMessage 方法用來將消息入隊,如果此時線程是阻塞的,調用 nativeWake 喚醒線程

// MessageQueue.java
Message next() {
    ···
    nativePollOnce(ptr, nextPollTimeoutMillis);
    ···
}

next() 方法用來取出消息。取之前調用 nativePollOnce() 查詢是否有可以處理的消息,如果沒有則阻塞線程。等待消息入隊時喚醒。

不錯,看來你對 Looper 循環的一些邊界處理也注意到了。既然 Looper 是個死循環,為什么不會導致 ANR 呢?

首先要明確一下概念。ANR 是應用在特定時間內無法響應一個事件時拋出的異常。

典型例子的是在主線程中執行耗時任務。當一個觸摸事件來臨時,主線程忙于處理耗時任務而無法在 5s 內響應觸摸事件,此時就會拋出 ANR。

但 Looper 死循環是事件循環的基石,本身就是 Android 用來處理一個個事件的。正常情況下,觸摸事件會加入到這個循環中被處理。但如果前一個事件太過耗時,下一個事件等待時間太長超出特定時間,這時才會產生 ANR。所以 Looper 死循環并不是產生 ANR 的原因。

好的,看來這個小陷阱沒能誤導你。那你說說消息隊列中的消息是如何進行排序的呢?

這個就要看 MessageQueue 的 enqueueMessage 方法了

enqueueMessage 是消息的入隊方法。Handler 在進行線程間通信時,會調用 sendMessage 將消息發送到接收消息的線程的消息隊列中,消息隊列調用 enqueueMessage 將消息入隊。

// MessageQueue.java
boolean enqueueMessage(Message msg, long when) {
    synchronized (this) {
        // ① when是消息入隊的時間
        msg.when = when;
        // ② mMessages是鏈表的頭指針,p是哨兵指針
        Message p = mMessages;
        boolean needWake;
        if (p == null || when == 0 || when < p.when) {
            msg.next = p;
            mMessages = msg;
            needWake = mBlocked;
        } else {
            needWake = mBlocked && p.target == null && msg.isAsynchronous();
            Message prev;
            for (;;) {
                prev = p;
                p = p.next;
                // ③ 遍歷鏈表,比較when找到插入位置
                if (p == null || when < p.when) {
                    break;
                }
                if (needWake && p.isAsynchronous()) {
                    needWake = false;
                }
            }
            // ④ 將msg插入到鏈表中
            msg.next = p;
            prev.next = msg;
        }

        if (needWake) {
            nativeWake(mPtr);
        }
    }
    return true;
}

消息入隊分為 3 步:

① 將入隊的時間綁定在 when 屬性上

② 遍歷鏈表,通過比較 when 找到插入位置

③ 將 msg 插入到鏈表中

這就是消息的排序方式

好的,假如我有一個消息,想讓它優先執行,如何提高它的優先級呢?

根據上個問題,最容易想到的是修改 Message 的 when 屬性。這確實不失為一種方法,但 Android 為我們提供了更科學簡單的方式,異步消息和同步屏障。

在 Android 的消息機制中,消息分為同步消息、異步消息和同步屏障三種。(沒錯,同步屏障是 target 屬性為 null 的特殊消息)。通常我們調用 sendMessage 方法發送的是同步消息。異步消息需要和同步屏障配合使用,來提升消息的優先級。

同步屏障理解起來其實很簡單。剛才說同步屏障是一種特殊的消息,當事件循環檢測到同步屏障時,之后的行為不再像之前那樣根據 when 的值一個個取消息,而是遍歷整個消息隊列,查找到異步消息取出并執行。

這個特殊的消息在消息隊列中像一個標志,事件循環探測到它時就改變原來的行為,轉而去查找異步消息。表現上看起來像一個屏障一樣攔住了同步消息。所以形象地稱為同步屏障。

源碼實現非常非常簡單:

//MessageQueue.java
Message next() {
    ···
    // ① target為null表明是同步屏障
    if (msg != null && msg.target == null) {
        // ② 取出異步消息
       do {
            prevMsg = msg;
            msg = msg.next;
       } while (msg != null && !msg.isAsynchronous());
    }
    ···
}

了解的挺透徹的嘛。那假如說我插入了一個同步屏障,不移除,會發生什么事呢?

同步屏障是用來“攔住”同步消息,處理異步消息的。如果同步屏障不移除,消息隊列里的異步消息會一個一個被取出處理,知道異步消息被取完。如果此時隊列中沒有異步消息了,則線程會阻塞,隊列中的同步消息永遠不會執行。所以同步屏障要及時移除。

那你知道同步屏障有哪些應用場景嗎?

同步屏障的核心作用是提高消息優先級,保證 Message 被優先處理。Android 為了避免卡頓,應用在了 view 繪制中。具體可以看之前關于 view 繪制的總結~

為什么使用 Handler 會有內存泄漏問題呢?該如何解決呢?

內存泄漏歸根到底其實是生命周期“錯位”導致的:一個對象本來應該在一個短的生命周期中被回收,結果被一個長生命周期的對象引用,導致無法回收。 Handler 的內存泄漏其實是內部類持有外部類引用導致的。
形成方式有兩種:

(1)匿名內部類持有外部類引用

class Activity {
    var a = 10
    fun postRunnable() {
        Handler(Looper.getMainLooper()).post(object : Runnable {
            override fun run() {
                this@Activity.a = 20
            }
        })
    }
}

Handler 在發送消息時,message.target 屬性就是 handler 本身。message 被發送到消息隊列中,被線程持有,線程是一個無比“長”生命周期的對象,導致 activity 無法被及時回收從而引起內存泄漏。

解決辦法是在 activity destory 時及時移除 runnable

(2)非靜態內部類持有外部類引用

//非靜態內部類
protected class AppHandler extends Handler {
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {

        }
    }
}

解決方案是用靜態內部類,并將外部引用改為弱引用

private static class AppHandler extends Handler {
    //弱引用,在垃圾回收時,被回收
    WeakReference<Activity> activity;

    AppHandler(Activity activity){
        this.activity = new WeakReference<Activity>(activity);
    }

    public void handleMessage(Message message){
        switch (message.what){
        }
    }
}

好的,Handler,Looper 和 MessageQueue 的基礎知識我基本問完了,最后一個問題,你知道 HandlerThread 和 IdleHandler 嗎?它們是用來干什么的?

HandlerThread 顧名思義就是 Handler+Thread 的結合體,它本質上是一個 Thread。

我們知道,子線程是需要我們通過 Looper.prepare()和 Looper.loop()手動開啟事件循環的。HandlerThread 其實就幫我們做了這件事,它是一個實現了事件循環的線程。我們可以在這個線程中做一些 IO 耗時操作。

IdleHandler 雖然叫 Handler,其實和同步屏障一樣是一種特殊的”消息"。不同于 Message,它是一個接口

public static interface IdleHandler{
    boolean queueIdle();
}

Idle 是空閑的意思。與同步屏障不同,同步屏障是提高異步消息的優先級使其優先執行,IdleHandler 是事件循環出現空閑的時候來執行。

這里的“空閑”主要指兩種情況

(1)消息隊列為空

(2)消息隊列不為空但全部是延時消息,也就是 msg.when > now

利用這一特性,我們可以將一些不重要的初始化操作放在 IdleHandler 中執行,以此加快 app 啟動速度;由于 View 的繪制是事件驅動的,我們也可以在主線程的事件循環中添加一個 IdleHandler 來作為 View 繪制完成的回調,等等。 但應該注意的是,如果主線程中一直有任務執行,IdleHandler 被執行的時機會無限延后,使用的時候要注意哦~

本篇是【面試官爸爸】系列第三篇,后續我還會繼續更新這個系列,包括面試最常考的 Activity 啟動,編譯打包流程及優化,Java 基礎,設計模式,組件化等面試常問的問題。如果不想錯過,歡迎點贊,收藏,關注我!球球兄弟萌辣,這個對我真的很重要!!

我是方木

一個在互聯網世界掙扎向上的打工人

努力生活,努力向前

微信搜公眾號 方木 Rudy 第一時間獲取我的更新!里面不僅有技術,還有故事和感悟

搜索它,帶走我!我們下期見~

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容