學習 Android Handler 訊息機制需要注意這些問題!

Android入墳之路發表於2019-04-24

學習 Android Handler 訊息機制需要注意這些問題!

一、提出問題

面試時常被問到的問題:

  • 簡述 Android 訊息機制
  • Android 中 Handler,Looper,MessageQueue,Message 有什麼關係?

這倆問題其實是一個問題,其實只要搞清楚了 Handler,Looper,MessageQueue,Message 的作用和聯絡,就理解了 Android 的 Handler 訊息機制。那麼再具體一點:

  • 為什麼在主執行緒可以直接使用 Handler?
  • Looper 物件是如何繫結 MessageQueue 的?
  • MessageQueue 裡的訊息從哪裡來?Handler是如何往MessageQueue中插入訊息的?
  • Message 是如何繫結 Handler 的?
  • Handler 如何繫結 MessageQueue?
  • 關於 handler,在任何地方 new handler 都是什麼執行緒下?
  • Looper 迴圈拿到訊息後怎麼處理?

二、解決問題

那麼,我們從主執行緒的訊息機制開始分析:

2.1 主執行緒 Looper 的建立和迴圈

Android 應用程式的入口是 main 函式,主執行緒 Looper 的建立也是在這裡完成的。

ActivityThread --> main() 函式

public static void main(){
        // step1: 建立主執行緒Looper物件
        Looper.prepareMainLooper();

        ActivityThread thread = new ActivityThread();
        // 繫結應用程式,布林標記是否為系統程式
        thread.attach(false);
        // 例項化主執行緒 Handler
        if(sMainThreadHandler == null){
           sMainThreadHandler = thread.getHandler();
        }
        // step2: 開始迴圈
        Loop.loop();

        throw new RuntimeException("Main thread loop unexpectedly exited");
}
複製程式碼

Looper.prepareMainLooper()用來建立主執行緒的 Looper 物件,接下來先看這個方法的實現。

2.1.1 建立主執行緒 Looper

Looper --> prepareMainLooper()

private static Looper sMainLooper;  // guarded by Looper.class

public static void prepareMainLooper(){
        // step1: 呼叫本類 prepare 方法
        prepare(false);
        // 執行緒同步,如果變數 sMainLooper 不為空丟擲主執行緒 Looper 已經建立
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            // step2: 呼叫本類 myLooper 方法
            sMainLooper = myLooper();
        }
}
複製程式碼

prepareMainLooper() 方法主要是使用 prepare(false) 建立當前執行緒的 Looper 物件,再使用 myLooper() 方法來獲取當前執行緒的 Looper 物件。

step1: Looper --> prepare()

// ThreadLocal 為每個執行緒儲存單獨的變數
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
// Looper 類的 MessageQueue 變數
final MessageQueue mQueue;
// quitAllowed 是否允許退出,這裡是主執行緒的 Looper 不可退出
private static void prepare(boolean quitAllowed) {
        // 首先判定 Looper 是否存在
        if(sThreadLocal.get() != null){
                throw new RuntimeException("Only one Looper may be created per thread");
        }
        // 儲存執行緒的副本變數
        sThreadLoacal.set(new Looper(quitAllowed));
}

private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
}
複製程式碼
  • prepare() 方法中用 ThreadLocal 來儲存主執行緒的 Looper 物件。ThreadLocal 可以看作是一個用來儲存資料的類,類似 HashMap、ArrayList等集合類,它存放著屬於當前執行緒的變數。

  • ThreadLocal 提供了 get/set 方法分別用來獲取和儲存變數。
    比如在主執行緒通過 prepare() 方法來建立 Looper 物件,並使用 sThreadLoacal.set(new Looper(quitAllowed)) 來儲存主執行緒的 Looper 物件,那麼在主執行緒呼叫 myLooper()(實際呼叫了 sThreadLocal.get() 方法) 就是通過 ThreadLocal 來獲取主執行緒的 Looper 物件。如果在子執行緒呼叫這些方法就是通過 ThreadLocal 儲存和獲取屬於子執行緒的 Looper 物件。

更多關於 ThreadLocal 的原理:

深入剖析ThreadLocal實現原理以及記憶體洩漏問題

問題1:為什麼在主執行緒可以直接使用 Handler?
因為主執行緒已經建立了 Looper 物件並開啟了訊息迴圈,通過上文的程式碼就可以看出來。

問題2:Looper 物件是如何繫結 MessageQueue 的?或者說 Looper 物件建立 MessageQueue 過程。
很簡單,Looper 有個一成員變數 mQueue,它就是 Looper 物件預設儲存的 MessageQueue。上面程式碼中 Looper 有一個構造器,新建 Looper 物件時會直接建立 MessageQueue 並賦值給 mQueue。
問題2解決:在 new Looper 時就建立了 MessageQueue 物件並賦值給 Looper 的成員變數 mQueue。

step2: Looper --> myLooper()

// 也就是使用本類的ThreadLocal物件獲取之前建立儲存的Looper物件
public static @Nullable Looper myLooper() {
     return sThreadLocal.get();
}
複製程式碼

這個方法就是通過 sThreadLocal 變數獲取當前執行緒的 Looper 物件,比較常用的一個方法。上文主執行緒 Looper 物件建立後使用該方法獲取了 Looper 物件。

2.1.2 開始迴圈處理訊息

回到最開始的 main() 函式,在建立了 Looper 物件以後就呼叫了 Looper.loop() 來迴圈處理訊息,貼一下大致程式碼:

public static void main(){
        // step1: 建立主執行緒Looper物件
        Looper.prepareMainLooper();
        ...
        // step2: 開始迴圈
        Loop.loop();
}
複製程式碼

Looper --> loop()

public static void loop() {
    // step1: 獲取當前執行緒的 Looper 物件
    final Looper me = myLooper();
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    // step2: 獲取 Looper 儲存的 MessageQueue 物件
    final MessageQueue queue = me.mQueue;

    ...
    // step3: 迴圈讀取訊息,如果有則呼叫訊息物件中儲存的 handler 進行傳送
    for (;;) {
        Message msg = queue.next(); // might block
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return;
        }
        ...
        try {
            // step4: 使用 Message 物件儲存的 handler 物件處理訊息
            msg.target.dispatchMessage(msg);
            end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
        } finally {
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }
        ...
        msg.recycleUnchecked();
    }
}
複製程式碼
  • step1 : myLooper() 方法就是通過 ThreadLocal 獲取當前執行緒的 Looper 物件,注意在哪個執行緒使用該方法就獲取的該執行緒的 Looper 物件。
  • step2 :me.mQueue,這個 mQueue 就是上面問題2所說的在 Looper 物件建立時新建的 MessageQueue 變數。
  • step3 :接下來是一個 for 迴圈,首先通過 queue.next() 來提取下一條訊息,具體是怎麼提取的可以參考下面文章的 4.2 節:

Android訊息機制1-Handler(Java層)

獲取到下一條訊息,如果 MessageQueue 中沒有訊息,就會進行阻塞。那麼如果存在訊息,它又是怎麼放入 MessageQueue 的呢?或者說MessageQueue 裡的訊息從哪裡來?Handler是如何往MessageQueue中插入訊息的?先不說這個,把這個問題叫作問題3後面分析。

  • step4 :msg.target.dispatchMessage(msg);這個方法最終會呼叫 Handler 的 handleMessage(msg) 方法。同時這裡又產生個問題:msg.target 是何時被賦值的?****,也就是說Message 是如何繫結 Handler 的?先稱之為問題4。那麼接著看 Handler 的 dispatchMessage 方法:

Handler --> dispatchMessage

public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}

private static void handleCallback(Message message) {
   message.callback.run();
}

public void handleMessage(Message msg) {
}
複製程式碼

可以看到該方法最後執行了 handleMessage() 方法,這是一個空方法也就是需要我們覆寫並實現的。另外 dispatchMessage() 也體現出一個問題:

訊息分發的優先順序:

  • Message 的回撥方法:message.callback.run(); 優先順序最高;
  • Handler 的回撥方法:mCallback.handleMessage(msg)優先順序次於上方;
  • Handler 的回撥方法:handleMessage() 優先順序最低。

到這裡 Looper 迴圈並通過 Handler 傳送訊息有一個整體的流程了,接下來分析 Handler 在訊息機制中的主要作用以及和 Looper、Message 的關係。

2.2 Handler 的建立和作用

上面說到 loop() 方法在不斷從訊息佇列 MessageQueue 中取出訊息(queue.next() 方法),如果沒有訊息則阻塞,反之交給 Message 繫結的 Handler 處理。回顧一下沒解決的兩個問題:

-問題3:MessageQueue 裡的訊息從哪裡來?Handler 是如何往 MessageQueue 中插入訊息的?
-問題4:msg.target 是何時被賦值的?,也就是說Message 是如何繫結 Handler 的?

既然要解決 Handler 插入訊息的問題,就要看 Handler 傳送訊息的過程。

2.2.1 Handler 傳送訊息

Handler --> sendMessage(Message msg);

final MessageQueue mQueue;
public final boolean sendMessage(Message msg){
    return sendMessageDelayed(msg, 0);
}
// 傳送延時訊息
public final boolean sendMessageDelayed(Message msg, long delayMillis){
    if (delayMillis < 0) {
        delayMillis = 0;
    }
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
// 指定時間傳送訊息
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
    MessageQueue queue = mQueue;
    if (queue == null) {
        RuntimeException e = new RuntimeException(
                this + " sendMessageAtTime() called with no mQueue");
        Log.w("Looper", e.getMessage(), e);
        return false;
    }
    return enqueueMessage(queue, msg, uptimeMillis);
}
// 處理訊息,賦值 Message 物件的 target,訊息佇列插入訊息
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    msg.target = this;
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}
複製程式碼

可以看到呼叫 sendMessage(Message msg) 方法最終會呼叫到 enqueueMessage() 方法,這個方法主要有兩個作用:賦值 Message 物件的 target、訊息佇列插入訊息。

  • 賦值 msg 的 target:msg.target = this 把傳送訊息的 Handler 賦值給 msg 物件的 target。那麼問題 4 就解決了:Handler 執行傳送訊息的過程中將自己繫結給了 Message 的 target,這樣兩者之間就產生了聯絡;
  • 訊息佇列插入訊息:queue.enqueueMessage(msg, uptimeMillis) queue 是 MessageQueue 的一個例項,queue.enqueueMessage(msg, uptimeMillis)是執行 MessageQueue 的enqueueMessage方法來插入訊息。這樣問題 3 就找到答案:Handler 在傳送訊息的時候執行 MessageQueue 的enqueueMessage方法來插入訊息;關於 MessageQueue 是怎麼執行插入訊息的過程,參考下方文章 4.3 節

Android訊息機制1-Handler(Java層)

  • 上面 Handler 傳送訊息使用了 MessageQueue 的例項 queue,可以看到這個 queue 是上一個方法 sendMessageAtTime 中由 Handler 的成員變數 mQueue 賦值的,那麼 mQueue 是哪來的?問題 5:Handler 如何繫結 MessageQueue?先劇透一下 Handler 繫結的是 Looper 的 MessageQueue 物件,Looper 的 MessageQueue 物件是在 Looper 建立時就 new 的。
    要了解 Handler 的 MessageQueue 物件是怎麼賦值的就要看 Handler 的建構函式了,Handler 建立的時候作了一些列操作比如獲取當前執行緒的 Looper,繫結 MessageQueue 物件等。

2.2.2 Handler 的建立

下面是 Handler 無參構造器和主要的構造器,另外幾個過載的構造器有些是通過傳遞不同引數呼叫包含兩個引數的構造器。兩個引數建構函式第一個引數為 callback 回撥,第二個函式用來標記訊息是否非同步。

// 無參構造器
public Handler() {
     this(null, false);
}

public Handler(Callback callback, boolean async) {
    if (FIND_POTENTIAL_LEAKS) {
        final Class<? extends Handler> klass = getClass();
        if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &amp;&amp;
                (klass.getModifiers() &amp; Modifier.STATIC) == 0) {
            Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                    klass.getCanonicalName());
        }
    }
    // step1:獲取當前執行緒 Looper
    mLooper = Looper.myLooper();
    if (mLooper == null) {
        throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");
    }
    // step2:獲取 Looper 物件繫結的 MessageQueue 物件並賦值給 Handler 的 mQueue
    mQueue = mLooper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}
複製程式碼
  • step1:呼叫myLooper() 方法,該方法是使用 sThreadLocal 物件獲取當前執行緒的 Looper 物件,回顧一下:
public static @Nullable Looper myLooper() {
     return sThreadLocal.get();
}
複製程式碼

如果獲取的 Looper 物件為 null,說明沒有執行 Looper.prepare() 為當前執行緒儲存 Looper 變數,就會丟擲 RuntimeException。這裡又說明了Handler 必須在有 Looper 的執行緒中使用,報錯不說,沒有 Looper 就無法繫結 MessageQueue 物件也就無法進行更多有關訊息的操作。

  • step2:mQueue = mLooper.mQueue 說明了 Handler 的 MessageQueue 物件是由當前執行緒 Looper 的 MessageQueue 物件賦值的。這裡問題 5 解決:Handler 在建立時繫結了當前執行緒 Looper 的 MessageQueue 物件。

  • 由於 Handler 和 Looper 可以看作使用的是同一個 MessageQueue 物件,所以 Handler 和 Looper 可以共享訊息佇列 MessageQueue。Handler 傳送訊息(用 mQueue 往訊息對列插入訊息),Looper 可以方便的迴圈使用 mQueue 查詢訊息,如果查詢到訊息,就可以用 Message 物件繫結的 Handler 物件 target 去處理訊息,反之則阻塞。

既然說到了 Handler 的構造器,就想到一個問題:問題 6:關於 handler,在任何地方 new handler 都是什麼執行緒下?這個問題要分是否傳遞 Looper 物件來看。

  • 不傳遞 Looper 建立 Handler:Handler handler = new Handler();上文就是 Handler 無參建立的原始碼,可以看到是通過 Looper.myLooper() 來獲取 Looper 物件,也就是說對於不傳遞 Looper 物件的情況下,在哪個執行緒建立 Handler 預設獲取的就是該執行緒的 Looper 物件,那麼 Handler 的一系列操作都是在該執行緒進行的。

  • 傳遞 Looper 物件建立 Handler:Handler handler = new Handler(looper);那麼看看傳入 Looper 的建構函式:

public Handler(Looper looper) {
    this(looper, null, false);
}
public Handler(Looper looper, Callback callback) {
    this(looper, callback, false);
}
// 第一個引數是 looper 物件,第二個 callback 物件,第三個訊息處理方式(是否非同步)
public Handler(Looper looper, Callback callback, boolean async) {
    mLooper = looper;
    mQueue = looper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}
複製程式碼

可以看出來傳遞 Looper 物件 Handler 就直接使用了。所以對於傳遞 Looper 物件建立 Handler 的情況下,傳遞的 Looper 是哪個執行緒的,Handler 繫結的就是該執行緒。

到這裡 Looper 和 Handler 就有一個大概的流程了,接下來看一個簡單的子執行緒 Handler 使用例子:

new Thread() {
    @Override
    public void run() {
        // step1
        Looper.prepare();
         // step2
        Handler handler = new Handler(){
            @Override
            public void handleMessage(Message msg) {
                if(msg.what == 1){
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            Toast.makeText(MainActivity.this,"HandlerTest",Toast.LENGTH_SHORT).show();
                        }
                    });
                     // step5
                    Looper.myLooper().quit();
                }
            }
        };
         // step3
        handler.sendEmptyMessage(1);
         // step4
        Looper.loop();
    }
}.start();
複製程式碼
  • step1: 呼叫 Looper.prepare(); 為當前執行緒建立 Looper 物件,同時也就建立了 MessageQueue,之後將該執行緒的 Looper 物件儲存在 ThreadLocal 中。注意這裡的一切操作都在子執行緒中,如果不呼叫 Looper.prepare() 就使用 Handler 會報錯。
  • step2: 建立 Handler 物件,覆寫 handleMessage 處理訊息,等待該 Handler 傳送的訊息處理時會呼叫該方法。
  • step3: 使用 handler 傳送訊息,這裡只是示例,畢竟自己給自己傳送訊息沒啥必要。傳送的過程中會將自己賦值給 msg.target,然後再將訊息插入到 Looper 繫結的 MessageQueue 物件中。
  • step4: 呼叫 Looper.loop(); 首先獲取當前執行緒的 Looper 物件,根據 Looper 物件就可以拿到 Looper 儲存的 MessageQueue 物件 mQueue。有了 MessageQueue 物件就可以 for 迴圈獲取它儲存的訊息 Message 物件,如果訊息不存在就返回 null 阻塞,反之則使用 Message 中儲存的 Handler:msg.target 來處理訊息,最終呼叫 handleMessage 也就是之前覆寫的方法來處理訊息。
  • step5: 邏輯處理完畢以後,應在最後使用 quit 方法來終止訊息迴圈,否則這個子執行緒就會一直處於等待的狀態,而如果退出Looper以後,這個執行緒就會立刻終止,因此建議不需要的時候終止Looper。

三、總結和其它

3.1 Handler、Looper、MessageQueue、Message

  • Handler 用來傳送訊息,建立時先獲取預設或傳遞來的 Looper 物件,並持有 Looper 物件包含的 MessageQueue,傳送訊息時使用該 MessageQueue 物件來插入訊息並把自己封裝到具體的 Message 中;
  • Looper 用來為某個執行緒作訊息迴圈。Looper 持有一個 MessageQueue 物件 mQueue,這樣就可以通過迴圈來獲取 - MessageQueue 所維護的 Message。如果獲取的 MessageQueue 沒有訊息時,便阻塞在 loop 的queue.next() 中的 nativePollOnce() 方法裡,反之則喚醒主執行緒繼續工作,之後便使用 Message 封裝的 handler 物件進行處理。
  • MessageQueue 是一個訊息佇列,它不直接新增訊息,而是通過與 Looper 關聯的 Handler 物件來新增訊息。
  • Message 包含了要傳遞的資料和資訊。

3.2 Android中為什麼主執行緒不會因為Looper.loop()裡的死迴圈卡死?

這是知乎上的問題,感覺問的挺有意思。平時可能不太會太深究這些問題,正好有大神回答那就記錄一下吧。

  • 為什麼不會因為死迴圈卡死?
    執行緒可以看作是一段可執行程式碼,當程式碼執行完畢執行緒的生命週期就該終止了。對於主執行緒來說我們不希望它執行一段時間後退出,所以簡單做法就是可執行程式碼是能一直執行下去的,死迴圈便能保證不會被退出。既然是死迴圈那麼怎麼去處理訊息呢,通過建立新執行緒的方式。
  • 為這個死迴圈準備了一個新執行緒
    在進入死迴圈之前便建立了新binder執行緒,在程式碼ActivityThread.main()中:
public static void main(){
        ...
        Looper.prepareMainLooper();

        //建立ActivityThread物件
        ActivityThread thread = new ActivityThread();

        //建立Binder通道 (建立新執行緒)
        thread.attach(false);

        if(sMainThreadHandler == null){
           sMainThreadHandler = thread.getHandler();
        }
        // step2: 開始迴圈
        Loop.loop();

        throw new RuntimeException("Main thread loop unexpectedly exited");
}
複製程式碼

thread.attach(false);便會建立一個Binder執行緒(具體是指ApplicationThread,Binder的服務端,用於接收系統服務AMS傳送來的事件),該Binder執行緒通過Handler將Message傳送給主執行緒。

  • 主執行緒的死迴圈一直執行是不是特別消耗CPU資源呢?
    其實不然,這裡就涉及到Linux pipe/epoll機制,簡單說就是在主執行緒的MessageQueue沒有訊息時,便阻塞在loop的queue.nextAndroid訊息機制1-Handler(Java層),則立刻通知相應程式進行讀或寫操作,本質同步I/O,即讀寫是阻塞的。所以說,主執行緒大多數時候都是處於休眠狀態,並不會消耗大量CPU資。

  • Activity的生命週期是怎麼實現在死迴圈體外能夠執行起來的?
    上文 main 函式有一部分獲取 sMainThreadHandler 的程式碼:

final H mH = new H();

public static void main(){
        ...
        if(sMainThreadHandler == null){
           sMainThreadHandler = thread.getHandler();
        }
        ...
}

final Handler getHandler() {
    return mH;
}
複製程式碼

類 H 繼承了 Handler,在主執行緒建立時就建立了這個 Handler 用於處理 Binder 執行緒傳送來的訊息。

Activity的生命週期都是依靠主執行緒的Looper.loop,當收到不同Message時則採用相應措施:

在H.handleMessage(msg)方法中,根據接收到不同的msg,執行相應的生命週期。
比如收到msg=H.LAUNCH_ACTIVITY,則呼叫ActivityThread.handleLaunchActivity()方法,最終會通過反射機制,建立Activity例項,然後再執行Activity.onCreate()等方法;
再比如收到msg=H.PAUSE_ACTIVITY,則呼叫ActivityThread.handlePauseActivity()方法,最終會執行Activity.onPause()等方法。 上述過程,我只挑核心邏輯講,真正該過程遠比這複雜。

3.3 Handler 使用造成記憶體洩露

  • 有延時訊息,要在Activity銷燬的時候移除Messages
  • 匿名內部類導致的洩露改為匿名靜態內部類,並且對上下文或者Activity使用弱引用。

最後

大家可以點贊關注下,以後還會更新更多精選技術分享給大家!


相關文章