Android 高階面試-1:Handler 相關

WngShhng發表於2019-02-18

要點

難點:

  1. MQ 的 next() 方法,enqueueMessage() 方法,因為它們與 Native 層的 Looper 和 MQ 關聯。

重點:

  1. 訊息如何分發
  2. next() 方法
  3. 如何退出
  4. Handler 與執行緒對應起來的原理

題目

  • Handler 實現機制(很多細節需要關注:如執行緒如何建立和退出訊息迴圈等等)
  • 關於 Handler,在任何地方 new Handler 都是什麼執行緒下?
  • Handler 發訊息給子執行緒,looper 怎麼啟動?
  • 在子執行緒中建立 Handler 報錯是為什麼?
  • 如何在子執行緒建立 Looper?
  • 為什麼通過 Handler 能實現執行緒的切換?

Handler 機制中有 4 個主要的物件:Handler、Message、MessageQueue 和 Looper. Handler 負責訊息的傳送和處理;Message 是訊息物件,類似於連結串列的一個結點;MessageQueue 是訊息佇列,用於存放訊息物件的資料結構;Looper 是訊息佇列的處理者(用於輪詢訊息佇列的訊息物件,取出後回撥 handler 的 dispatchMessage() 進行訊息的分發,dispatchMessage() 方法會回撥 handleMessage() 方法把訊息傳入,由 Handler 的實現類來處理。)

當我們在某個執行緒當中呼叫 new Handler() 的時候會使用當前執行緒的 Looper 建立 Handler. 當前執行緒的 Looper 存在於執行緒區域性變數 ThreadLocal 中。在使用 Handler 之前我們需要先呼叫 Looper.prepare() 方法例項化當前執行緒的 Looper,並將其放置到當前執行緒的執行緒區域性變數中(只放一次,以後會先從 TL 中獲取再使用,此時會呼叫 Looper 的構造方法,並在構造方法中初始化 MQ),然後呼叫 Looper.loop() 開啟訊息迴圈。主執行緒也是一樣,只是主執行緒的 Looper 在 ActivityThread 的 main() 方法中被例項化。我們可以使用 Looper.getMainLooper() 方法來獲取主執行緒的 Looper,並使用它來建立 Handler,這樣我們就可以在任何執行緒中向主執行緒傳送訊息了。

    Looper.prepare(); // 內部會呼叫 Looper 的 new 方法例項化 Looper 並將其放進 TL
    new Handler().post(() -> /* do something */);
    Looper.loop();
複製程式碼

當例項化 Looper 的時候會同時例項化一個 MessageQueue,而 MessageQueue 同時又會呼叫 Native 層的方法在 Native 層例項化一個 MessageQueue 還有 Looper. Java 層的 Looper 和 Native 層的 Looper 之間使用 epoll 進行通訊。當呼叫 Looper 的 loop() 方法的時候會啟動一個迴圈來對訊息進行處理。Java 層的 MQ 中沒有訊息的時候,Native 層的 Looper 會使其進入睡眠狀態,當有訊息到來的時候再將其喚醒起來處理訊息,以節省 CPU.

在 Looper 的 loop() 中開啟無限迴圈為什麼不會導致主執行緒 ANR 呢?這是因為 Android 系統本身就是基於訊息機制的,所謂的訊息就是指傳送到主執行緒當中的訊息。之所以產生 ANR 並不是因為主執行緒當中的任務無限迴圈,而是因為無限迴圈導致其他的事件得不到處理。

《Android 訊息機制:Handler、MessageQueue 和 Looper》

handler記憶體洩漏及解決辦法:如果 Handler 不是靜態內部類,Handler 會持有 Activity 的匿名引用。當 Activity 要被回收時,因為 Handler 在做耗時操作沒有被釋放,Handler Activity 的引用不能被釋放導致 Activity 沒有被回收停留在記憶體中造成記憶體洩露。

解決方法是:1). 將 Handler 設為靜態內部類;2). 使 Handler 持有 Activity 的弱引用;3). 在 Activity 生命週期 onDestroy() 中呼叫 Handler.removeCallback() 方法。

  • 為什麼不能在子執行緒中訪問 UI?

Android 中的控制元件不是執行緒安全的,之所以這樣設計是為了:1).設計成同步的可以簡化使用的複雜度;2).可以提升控制元件的效能(非同步加鎖在非多執行緒環境是額外的開銷)。

  • Handler.post() 的邏輯在哪個執行緒執行的,是由 Looper 所線上程還是 Handler 所線上程決定的?(這裡的 Handler 所在的執行緒指的是呼叫 Handler 的 post() 方法時 Handler 所在的執行緒)
  • Handler 的 post()/send() 的原理?
  • Handler 的 post() 和 postDelayed() 方法的異同?

post() 方法所在的執行緒由 Looper 所線上程決定的;最終邏輯是在 Looper.loop() 方法中,從 MQ 中拿出 Message,並且執行其邏輯。這是在 Looper 中執行的。因此由 Looper 所線上程決定。

不論你呼叫 send() 型別的方法還是 post() 型別的方法,最終都會呼叫到 sendMessageAtTime() 方法。post()postDelay() 的區別在於,前者使用當前時間,後者使用當前時間+delay 的時間來決定訊息觸發的時間。最終方法的引數都將被包裝成一個 Message 物件加入到 Handler 對應的 Looper 的 MQ 中被執行。

  • Looper 和 Handler 一定要處於一個執行緒嗎?子執行緒中可以用 MainLooper 去建立 Handler嗎?

Looper 和 Handler 不需要再一個執行緒中,預設的情況下會從 TL 中取當前執行緒對應的 Looper,但我們可以通過顯式地指定一個 Looper 的方式來建立 Handler. 比如,當我們想要在子執行緒中傳送訊息到主執行緒中,那麼我們可以

Handler handler = new Handler(Looper.getMainLooper());
複製程式碼
  • Handler.post() 方法傳送的是同步訊息嗎?可以傳送非同步訊息嗎?

使用者層面傳送的都是同步訊息,不能傳送非同步訊息;非同步訊息只能由系統傳送。

  • MessageQueue.next() 會因為發現了延遲訊息,而進行阻塞。那麼為什麼後面加入的非延遲訊息沒有被阻塞呢?
  • MessageQueue.enqueueMessage() 方法的原理,如何進行執行緒同步的?
  • MessageQueue.next() 方法內部的原理?
  • next() 是如何處理一般訊息的?
  • next() 是如何處理同步屏障的?
  • next() 是如何處理延遲訊息的?

呼叫 MessageQueue.next() 方法的時候會呼叫 Native 層的 nativePollOnce() 方法進行精準時間的阻塞。在 Native 層,將進入 pullInner() 方法,使用 epoll_wait 阻塞等待以讀取管道的通知。如果沒有從 Native 層得到訊息,那麼這個方法就不會返回。此時主執行緒會釋放 CPU 資源進入休眠狀態。

當我們加入訊息的時候,會呼叫 MessageQueue.enqueueMessage() 方法,新增完 Message 後,如果訊息佇列被阻塞,則會呼叫 Native 層的 nativeWake() 方法去喚醒。它通過向管道中寫入一個訊息,結束上述阻塞,觸發上面提到的 nativePollOnce() 方法返回,好讓加入的 Message 得到分發處理。

MessageQueue.enqueueMessage() 使用 synchronized 程式碼塊去進行同步。

資料:Android 中的 Handler 的 Native 層研究

  • Handler 的 dispatchMessage() 分發訊息的處理流程?

使用 Handler 的時候我們會覆寫 Handler 的 handleMessage() 方法。當我們呼叫該 Handler 的 send() 或者 post() 傳送一個訊息的時候,傳送的資訊會被包裝成 Message,並且將該 Message 的 target 指向當前 Handler,這個訊息會被放進 Looper 的 MQ 中。然後在 Looper 的迴圈中,取出這個 Message,並呼叫它的 target Handler,也就是我們定義的 Handler 的 dispatchMessage() 方法處理訊息,此時會呼叫到 Handler 的 handleMessage() 方法處理訊息,並回撥 Callback.

  • Handler 為什麼要有 Callback 的構造方法?

當 Handler 在訊息佇列中被執行的時候會直接呼叫 Handler 的 dispatchMessage() 方法回撥 Callback.

  • Handler構造方法中通過 Looper.myLooper() 是如何獲取到當前執行緒的 Looper 的?

從 TL 中獲取

  • MessageQueue 中底層是採用的佇列?

是單連結串列,不是佇列

  • Looper 的兩個退出方法?
  • quit() 和 quitSafely() 有什麼區別
  • 子執行緒中建立了 Looper,在使用完畢後,終止訊息迴圈的方法?
  • quit() 和 quitSafely() 的本質是什麼?

quit()quitSafely() 的本質就是讓訊息佇列的 next() 返回 null,以此來退出Looper.loop()

quit() 呼叫後直接終止 Looper,不在處理任何 Message,所有嘗試把 Message 放進訊息佇列的操作都會失敗,比如 Handler.sendMessage() 會返回 false,但是存在不安全性,因為有可能有 Message 還在訊息佇列中沒來的及處理就終止 Looper 了。

quitSafely() 呼叫後會在所有訊息都處理後再終止 Looper,所有嘗試把 Message 放進訊息佇列的操作也都會失敗。

    public void quit() {
        mQueue.quit(false);
    }
    public void quitSafely() {
        mQueue.quit(true);
    }
    void quit(boolean safe) {
        if (!mQuitAllowed)  throw new IllegalStateException("Main thread not allowed to quit.");
        synchronized (this) {
            if (mQuitting) return;
            mQuitting = true;
            if (safe)  removeAllFutureMessagesLocked(); // 把所有延遲訊息清除
            else       removeAllMessagesLocked();  // 直接把訊息佇列裡面的訊息清空
            nativeWake(mPtr);
        }
    }
複製程式碼
  • Looper.loop() 在什麼情況下會退出?

1).next() 方法返回的 msg == null;2).執行緒意外終止。

  • Looper.loop() 的原始碼流程?
  1. 獲取到 Looper 和訊息佇列;
  2. for 無限迴圈,阻塞於訊息佇列的 next() 方法;
  3. 取出訊息後呼叫 msg.target.dispatchMessage(msg) 進行訊息分發。
  • Looper.loop() 方法執行時,如果內部的 myLooper() 獲取不到Looper會出現什麼結果?

異常

  • Android 如何保證一個執行緒最多隻能有一個 Looper?如何保證只有一個 MessageQueue

通過保證只有一個 Looper 來保證只有以一個 MQ. 在一個執行緒中使用 Handler 之前需要使用 Looper.prepare() 建立 Looper,它會從 TL 中獲取,如果發現 TL 中已經存在 Looper,就拋異常。

  • Handler 訊息機制中,一個 Looper 是如何區分多個 Handler 的?

根據訊息的分發機制,Looper 不會區分 Handler,每個 Handler 會被新增到 Message 的 target 欄位上面,Looper 通過呼叫 Message.target.handleMessage() 來讓 Handler 處理訊息。

最後

關注作者,及時獲取更多高階面試題解 :)


Android 高階面試系列文章,關注作者及時獲取更多面試資料

本系列以及其他系列的文章均維護在 Github 上面:Github / Android-notes,歡迎 Star & Fork. 如果你喜歡這篇文章,願意支援作者的工作,請為這篇文章點個贊?!

相關文章