Android 面試(五):探索 Android 的 Handler

nanchen2251發表於2017-11-21

這是 面試系列 的第五期。本期我們將來探討一下 Android 非同步訊息處理執行緒 —— Handler。

往期內容傳遞:
Android 面試(一):說說 Android 的四種啟動模式
Android 面試(二):如何理解 Activity 的生命週期
Android 面試(三):用廣播 BroadcastReceiver 更新 UI 介面真的好嗎?
Android 面試(四):Android Service 你真的能應答自如了嗎?

開始

Android 的訊息機制,也就是 Handler 機制,相信各位都已經是爛熟於心了吧。即建立一個 Message 物件,然後藉助 Handler 傳送出去,之後在 HandlerhandleMessage() 方法中獲取剛才傳送的 Message 物件,然後在這裡進行 UI 操作就不會出現崩潰了。

既然 Handler 操作都爛熟於心,還講這個幹什麼?

嗯,對,在 Android 開發中,我們確實經常用到它,對於基本程式碼流程自然也是倒背如流,但瞭解它的原理的人卻不是很多,所以面試官通常會考驗你對 Handler 原始碼機制的理解,畢竟只有知己知彼,才能百戰不殆嘛。

我們都知道 UI 操作只能在主執行緒進行,通常是怎麼在子執行緒更新 UI 的?

  • Handler
  • Activity.runOnUiThread()
  • View.post(Runnable r)

講講 Handler 機制吧

Handler 主要由以下部分組成。

  • Handler
    Handler 是一個訊息輔助類,主要負責向訊息池傳送各種訊息事件Handler.sendMessage() 和處理相應的訊息事件Handler.handleMessage()

  • Message
    Message 即訊息,它能容納任意資料,相當於一個資訊載體。

  • MessageQueue
    MessageQueue 如其名,訊息佇列。它按時序將訊息插入佇列,最小的時間戳將被優先處理。

  • Looper
    Looper 負責從訊息佇列讀取訊息,然後分發給對應的 Handler 進行處理。它是一個死迴圈,不斷地呼叫 MessageQueue.next() 去讀取訊息,在沒有訊息分發的時候會變成阻塞狀態,在有訊息可用時繼續輪詢。

在 Android 開發中使用 Handler 有什麼需要注意的

首先自然是在工作執行緒中建立自己的訊息佇列必須要呼叫 Looper.prepare(),並且在一個執行緒中只能呼叫一次。當然,僅僅建立了 Looper 還不行,還必須使用 Looper.loop() 開啟訊息迴圈,要不然要 Looper 也沒用。

我們平時在開發中不用呼叫是因為預設會呼叫主執行緒的 Looper。
此外,一個執行緒中只能有一個 Looper 物件和一個 MessageQueue 物件。

大概的標準寫法是這樣。

Looper.prepare();
Handler mHandler = new Handler() {
   @Override
   public void handleMessage(Message msg) {
          Log.i(TAG, "在子執行緒中定義Handler,並接收到訊息。。。");
   }
};
Looper.loop();複製程式碼

另外一個比較常考察的就是 Handler 可能引起的記憶體洩漏了。

Handler 可能引起的記憶體洩漏

我們經常會寫這樣的程式碼。

 private final Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };複製程式碼

當你這樣寫的時候,你一定會收到編譯器的黃色警告。

In Android, Handler classes should be static or leaks might occur, Messages enqueued on the application thread’s MessageQueue also retain their target Handler. If the Handler is an inner class, its outer class will be retained as well. To avoid leaking the outer class, declare the Handler as a static nested class with a WeakReference to its outer class

在 Java 中,非靜態的內部類和匿名內部類都會隱式地持有其外部類的引用,而靜態的內部類不會持有外部類的引用。

要解決這樣的問題,我們在繼承 Handler 的時候,要麼是放在單獨的類檔案中,要麼直接使用靜態內部類。當需要在靜態內部類中呼叫外部的 Activity 的時候,我們可以直接採用弱引用進行處理,所以我們大概修改後的程式碼如下。

 private static final class MyHandler extends Handler{
        private final WeakReference<MainActivity> mWeakReference;

        public MyHandler(MainActivity activity){
            mWeakReference = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            MainActivity activity = mWeakReference.get();
            if (activity != null){
                // 開始寫業務程式碼
            }
        }
    }

private MyHandler mMyHandler = new MyHandler(this);複製程式碼

其實在我們實際開發中,不止一個地方可能會用到內部類,我們都需要在這樣的情況下儘量使用靜態內部類加弱引用的方式解決我們可能出現的記憶體洩漏問題。

用過 HandlerThread 嗎?它是幹嘛的?

HandlerThread 是 Android API 提供的一個便捷的類,使用它可以讓我們快速地建立一個帶有 Looper 的執行緒,有了 Looper 這個執行緒,我們又可以生成 Handler,本質上也就是一個建立了內部 Looper 的普通 Thread

我們在上面提到的子執行緒建立 Handler 大概程式碼是這樣。

new Thread(new Runnable() {
    @Override public void run() {
        // 準備一個 Looper,Looper 建立時對應的 MessageQueue 也會被建立
        Looper.prepare();
        // 建立 Handler 並 post 一個 Message 到 MessageQueue
        new Handler().post(new Runnable() {
            @Override 
            public void run() {
                  MLog.i("Handler in " + Thread.currentThread().getName());
            }
        });
        // Looper 開始不斷的從 MessageQueue 取出訊息並再次交給 Handler 執行
        // 此時 Lopper 進入到一個無限迴圈中,後面的程式碼都不會被執行
        Looper.loop();
    }
}).start();複製程式碼

而採用 HandlerThread 可以直接把步驟簡化為這樣:

// 1. 建立 HandlerThread 並準備 Looper
handlerThread = new HandlerThread("myHandlerThread");
handlerThread.start();

// 2. 建立 Handler 並繫結 handlerThread 的 Looper
new Handler(handlerThread.getLooper()).post(new Runnable() {
    @Override 
    public void run() {
          // 注意:Handler 繫結了子執行緒的 Looper,這個方法也會執行在子執行緒,不可以更新 UI
          MLog.i("Handler in " + Thread.currentThread().getName());
    }
});

// 3. 退出
@Override public void onDestroy() {
    super.onDestroy();
    handlerThread.quit();
}複製程式碼

其中必須注意的是,workerThread.start() 是必須要執行的。

至於如何使用 HandlerThread 來執行任務,主要是呼叫 Handler 的 API。

  • 使用 post 方法提交任務,postAtFrontOfQueue() 將任務加入到佇列前端, postAtTime() 指定時間提交任務, postDelayed() 延後提交任務。
  • 使用 sendMessage() 方法可以傳送訊息,sendMessageAtFrontOfQueue() 將該訊息放入訊息佇列前端,sendMessageAtTime() 指定時間傳送訊息, sendMessageDelayed() 延後提交訊息。

HandlerThread 的 quit() 和 quitSafety() 有啥區別?

兩個方法作用都是結束 Looper 的執行。它們的區別是,quit() 方法會直接移除 MessageQueue 中的所有訊息,然後終止 MesseageQueue,而 quitSafety() 會將 MessageQueue 中已有的訊息處理完成後(不再接收新訊息)再終止 MessageQueue

做不完的開源,寫不完的矯情。歡迎掃描下方二維碼或者公眾號搜尋「nanchen」關注我的微信公眾號,目前多運營 Android ,儘自己所能為你提升。如果你喜歡,為我點贊分享吧~
nanchen
nanchen

相關文章