理解 HandlerThread 原理

我愛宋慧喬發表於2019-03-01

本人只是 Android小菜一個,寫技術文件只是為了總結自己在最近學習到的知識,從來不敢為人師,如果裡面有些不正確的地方請大家盡情指出,謝謝!

1. 概述

HandlerThreadAndroid提供用來建立含有Looper執行緒的,其實在之前分析IntentService的博文中已經看到了它的應用,再來回顧下IntentService的啟動過程:

public void onCreate() {
    // TODO: It would be nice to have an option to hold a partial wakelock
    // during processing, and to have a static startService(Context, Intent)
    // method that would launch the service & hand off a wakelock.

    super.onCreate();
    // 建立包含 Looper 的執行緒並啟動之
    HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
    thread.start();
    // 通過新執行緒的 Looper 建立 Handler 例項
    mServiceLooper = thread.getLooper();
    mServiceHandler = new ServiceHandler(mServiceLooper);
}
複製程式碼

這段IntentService的啟動程式碼中直接使用到了HandlerThread,但當時只是一筆帶過並沒有仔細分析HandlerThread的使用方法和實現原理,本文將詳細講解如何在專案中使用HandlerThread和其內部的實現原理。

本文假設您對Handler,Thread,Looper,Message 和 MessageQueue相關知識有了一定的瞭解,所以涉及到它們的地方,只會稍作說明不再深入分析。

2. HandlerThread 使用方法

在講解其具體使用方法前,還是先來看下對HandlerThread的宣告:

/**
 * Handy class for starting a new thread that has a looper. The looper can then be 
 * used to create handler classes. Note that start() must still be called.
 */
public class HandlerThread extends Thread { ... }
複製程式碼

從這段宣告裡可以看到:HandlerThread能夠很方便地啟動一個帶有looper的執行緒,而這個looper可以用來建立handler。這句話裡隱含了幾點重要知識:

  • HandlerThread是一個Thread執行緒,具有執行緒的特性。
  • Android中預設執行緒沒有looper,如果想建立帶有looper的執行緒需要在建立的過程中主動創造looper物件。
  • Handler中必須要有looper,它是整個訊息查詢、分發、處理的核心,在建立Handler的過程中可以指定任意執行緒的looper物件。

現在通過一個簡單的示例演示下HandlerThread的使用方法:

public class MainActivity extends Activity {
    private static final String TAG = "Android_Test";

    private Button mButton;
    private TextView mText;
    
    // 新執行緒和與之相關聯的 Handler 物件 
    private HandlerThread mHanderThread;
    private Handler mThreadHandler;
    
    // 和主執行緒相關的 Handler 物件
    private Handler mUiHandler;
    
    // 用於子執行緒和主執行緒中的訊息分發
    private static final int MESSAGE_CODE_GET = 1;
    private static final int MESSAGE_CODE_SET = 2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mButton = (Button) findViewById(R.id.main_button);
        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 主執行緒通過子執行緒 Handler 分發訊息,以達到在子執行緒中處理耗時任務的目的。
                mThreadHandler.sendEmptyMessage(MESSAGE_CODE_GET);
            }
        });
        mText = (TextView) findViewById(R.id.main_text);
        
        // 建立 HandlerThread 並啟動新執行緒
        mHanderThread = new HandlerThread("HandlerThread");
        mHanderThread.start();
        
        // 通過新執行緒中的 looper 建立相關的 Handler 物件
        mThreadHandler = new Handler(mHanderThread.getLooper()) {
          @Override
          public void handleMessage(Message msg) {
              Log.i(TAG, "mThreadHandler's thread: " + Thread.currentThread().getName());
              if (msg.what == MESSAGE_CODE_GET) {
                  try {
                      // 休眠 5 秒,模擬子執行緒處理耗時任務的過程。
                      Thread.sleep(5 * 1000);
                  } catch (InterruptedException ie) {
                      ie.printStackTrace();
                  }
                  // 向主執行緒 Handler 傳送處理結果
                  mUiHandler.sendEmptyMessage(MESSAGE_CODE_SET);
              }
          }
        };

        mUiHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                Log.i(TAG, "mUiHandler's thread: " + Thread.currentThread().getName());
                if (msg.what == MESSAGE_CODE_SET) {
                    // 主執行緒接收來自子執行緒的訊息就行後續處理,這裡是顯示當前時間資訊。
                    mText.setText(String.valueOf(SystemClock.uptimeMillis()));
                }
            }
        };
    }
}
複製程式碼

這個示例的主要功能是主執行緒中發起任務,在子執行緒中處理這些耗時任務,處理完成後通知主執行緒並更新介面,並列印出執行過程,從下面的執行結果可以看到:耗時任務確實是在子執行緒中執行的。

03-01 10:04:57.311 30673 30723 I Android_Test: mThreadHandler's thread: HandlerThread
03-01 10:05:02.313 30673 30673 I Android_Test: mUiHandler's thread: main
複製程式碼

從上面的示例可以總結得到HandlerThread的使用方法:

  1. 首先建立HandlerThread物件並執行它,在建立過程中需要指定執行緒名字;
  2. 獲取HandlerThread物件中的looper並通過它來構造一個子執行緒Handler物件;
  3. 主執行緒通過子執行緒Handler物件向子執行緒分發任務;
  4. 子執行緒處理耗時任務並把處理結果分發到主執行緒,主執行緒進行後續的處理。

3. HandlerThread 原理分析

HandlerThread和普通的Thread的區別就在於其內部是包含Looper的,所以我們分析的重點就是它是怎麼建立使用Looper以及在使用後如何退出。首先來看下它的建構函式:

public class HandlerThread extends Thread {
    // 執行緒優先順序
    int mPriority;
    // 執行緒號
    int mTid = -1;
    // 執行緒內部的 Looper 物件
    Looper mLooper;
    private @Nullable Handler mHandler;

    // 只指定執行緒名字並使用預設的執行緒優先順序來構造 HandlerThread 物件
    public HandlerThread(String name) {
        super(name);
        mPriority = Process.THREAD_PRIORITY_DEFAULT;
    }
    
    /**
     * Constructs a HandlerThread.
     * @param name
     * @param priority The priority to run the thread at. The value supplied must be from 
     * {@link android.os.Process} and not from java.lang.Thread.
     */
    
    // 同時指定執行緒名字和優先順序來構造 HandlerThread 物件
    public HandlerThread(String name, int priority) {
        super(name);
        mPriority = priority;
    }
    // 省略其他內容
    ...
}
複製程式碼

由於HandlerThread是直接繼承Thread的,所以在通過start()啟動執行緒後,其中的run()就會啟動,這也是執行緒內部的核心方法,來看下其實現:

@Override
public void run() {
    mTid = Process.myTid();
    // 建立一個和當前執行緒有關的 Looper 物件
    Looper.prepare();
    synchronized (this) {
        // 得到當前執行緒的 Looper 物件後喚醒等待
        mLooper = Looper.myLooper();
        notifyAll();
    }
    Process.setThreadPriority(mPriority);
    // 呼叫回撥方法,可以在開始訊息輪詢之前進行某些初始化設定,預設是空方法。
    onLooperPrepared();
    // 啟動訊息輪詢,進行訊息的查詢分發和處理。
    Looper.loop();
    mTid = -1;
}
複製程式碼

這段程式碼就是HandlerThread中建立Looper物件並啟動訊息迴圈的核心,我們來一步步分析其重要邏輯。

3.1 建立 Looper 物件

在核心程式碼run()中首先看到的是Looper.prepare(),其作用就是建立當前執行緒的Looper物件:

/** Initialize the current thread as a looper.
  * This gives you a chance to create handlers that then reference
  * this looper, before actually starting the loop. Be sure to call
  * {@link #loop()} after calling this method, and end it by calling
  * {@link #quit()}.
  */
public static void prepare() {
    prepare(true);
}

private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));
}
複製程式碼

在使用Looper.prepare()建立Looper物件的過程中利用ThreadLocal把這個物件和當前執行緒建立了關聯。

ThreadLocal是一個可以儲存執行緒區域性變數的類,如果大家感興趣可以自行查閱相關資料,在這裡就不對其進行詳細講述了。

3.2 獲取 Looper 物件

建立完Looper物件後會在同步程式碼塊裡去喚醒等待,那這個等待會發生在什麼時候呢?記得示例中是通過getLooper()得到Looper物件的,來看下它的內部實現:

/**
 * This method returns the Looper associated with this thread. If this thread not been started
 * or for any reason isAlive() returns false, this method will return null. If this thread
 * has been started, this method will block until the looper has been initialized.  
 * @return The looper.
 */
public Looper getLooper() {
    // 執行緒沒有啟動或者已經死亡時返回 null
    if (!isAlive()) {
        return null;
    }
    
    // If the thread has been started, wait until the looper has been created.
    synchronized (this) {
        // 執行緒已經啟動但是 Looper 物件還沒有建立完成時等待
        while (isAlive() && mLooper == null) {
            try {
                wait();
            } catch (InterruptedException e) {
            }
        }
    }
    // 等待結束說明此時 Looper 物件已經建立完成,返回之。
    return mLooper;
}
複製程式碼

在這裡看到當“執行緒已經啟動但是Looper物件還沒有建立完成”時會進行等待,當建立完成時會喚醒等待,這時getLooper()就可以返回已經建立完成的Looper物件了。之所以需要這個“等待-喚醒”機制,因為獲取Looper是在主執行緒中進行的,而建立Looper是在子執行緒中進行的,必須使用這個機制來完成兩者的狀態同步。

3.3 開啟 Looper 迴圈

前面已經講了Looper物件的建立以及如何在主執行緒中獲取,那麼如何通過Looper.loop()開啟迴圈呢?

/**
 * Run the message queue in this thread. Be sure to call
 * {@link #quit()} to end the loop.
 */
public static void loop() {
    // 獲取Looper物件
    final Looper me = myLooper();
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    // 獲取訊息佇列
    final MessageQueue queue = me.mQueue;

    // Make sure the identity of this thread is that of the local process,
    // and keep track of what that identity token actually is.
    Binder.clearCallingIdentity();
    final long ident = Binder.clearCallingIdentity();

    // Allow overriding a threshold with a system prop. e.g.
    // adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'
    final int thresholdOverride =
            SystemProperties.getInt("log.looper."
                    + Process.myUid() + "."
                    + Thread.currentThread().getName()
                    + ".slow", 0);

    boolean slowDeliveryDetected = false;

    // 開啟一個無限迴圈來從訊息佇列中獲取訊息
    for (;;) {
        Message msg = queue.next(); // might block
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return;
        }

        // This must be in a local variable, in case a UI event sets the logger
        final Printer logging = me.mLogging;
        if (logging != null) {
            logging.println(">>>>> Dispatching to " + msg.target + " " +
                    msg.callback + ": " + msg.what);
        }

        final long traceTag = me.mTraceTag;
        long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
        long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;
        if (thresholdOverride > 0) {
            slowDispatchThresholdMs = thresholdOverride;
            slowDeliveryThresholdMs = thresholdOverride;
        }
        final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0);
        final boolean logSlowDispatch = (slowDispatchThresholdMs > 0);

        final boolean needStartTime = logSlowDelivery || logSlowDispatch;
        final boolean needEndTime = logSlowDispatch;

        if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
            Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
        }

        final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
        final long dispatchEnd;
        try {
            // 獲取到訊息後,分發到 target 去處理。
            msg.target.dispatchMessage(msg);
            dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
        } finally {
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }
        if (logSlowDelivery) {
            if (slowDeliveryDetected) {
                if ((dispatchStart - msg.when) <= 10) {
                    Slog.w(TAG, "Drained");
                    slowDeliveryDetected = false;
                }
            } else {
                if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery",
                        msg)) {
                    // Once we write a slow delivery log, suppress until the queue drains.
                    slowDeliveryDetected = true;
                }
            }
        }
        if (logSlowDispatch) {
            showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", msg);
        }

        if (logging != null) {
            logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
        }

        // Make sure that during the course of dispatching the
        // identity of the thread wasn't corrupted.
        final long newIdent = Binder.clearCallingIdentity();
        if (ident != newIdent) {
            Log.wtf(TAG, "Thread identity changed from 0x"
                    + Long.toHexString(ident) + " to 0x"
                    + Long.toHexString(newIdent) + " while dispatching to "
                    + msg.target.getClass().getName() + " "
                       + msg.callback + " what=" + msg.what);
        }
        // 回收訊息物件
        msg.recycleUnchecked();
    }
}
複製程式碼

這段程式碼非常長,在分析的時候不需要弄懂每一行的意思,只需要瞭解其中關於訊息的大致處理流程即可,大家如果不想去看這大段程式碼,只需關注新增註釋的幾行即可,其基本流程是:通過一個無限迴圈從訊息佇列中查詢Message訊息,如果查詢不到就等待,如果查詢到就交給其target來處理,最後要回收資源。

3.4 退出 Looper 迴圈

在使用HandlerThread+Handler在子執行緒處理耗時任務後並且不再需要時,必須要退出Looper的訊息迴圈,可以通過quit()

/**
 * Quits the handler thread's looper.
 * <p>
 * Causes the handler thread's looper to terminate without processing any
 * more messages in the message queue.
 * </p><p>
 * Any attempt to post messages to the queue after the looper is asked to quit will fail.
 * For example, the {@link Handler#sendMessage(Message)} method will return false.
 * </p><p class="note">
 * Using this method may be unsafe because some messages may not be delivered
 * before the looper terminates.  Consider using {@link #quitSafely} instead to ensure
 * that all pending work is completed in an orderly manner.
 * </p>
 */
public boolean quit() {
    Looper looper = getLooper();
    if (looper != null) {
        looper.quit();
        return true;
    }
    return false;
}
複製程式碼

這份方法可以退出Looper迴圈同時會把當前訊息佇列中的所有訊息都拋棄,也無法再向該訊息佇列中傳送訊息。但有時我們並不想直接清空訊息佇列,這時可以使用另外一種方式:

/**
 * Quits the handler thread's looper safely.
 * <p>
 * Causes the handler thread's looper to terminate as soon as all remaining messages
 * in the message queue that are already due to be delivered have been handled.
 * Pending delayed messages with due times in the future will not be delivered.
 * </p><p>
 * Any attempt to post messages to the queue after the looper is asked to quit will fail.
 * For example, the {@link Handler#sendMessage(Message)} method will return false.
 * </p><p>
 * If the thread has not been started or has finished (that is if
 * {@link #getLooper} returns null), then false is returned.
 * Otherwise the looper is asked to quit and true is returned.
 * </p>
 *
 * @return True if the looper looper has been asked to quit or false if the
 * thread had not yet started running.
 */
public boolean quitSafely() {
    Looper looper = getLooper();
    if (looper != null) {
        looper.quitSafely();
        return true;
    }
    return false;
}
複製程式碼

這個方法可以更安全地退出,它會讓訊息佇列中的非延遲訊息繼續得到處理,是更推薦的退出方式。

4. 總結

本文介紹了HandlerThread的使用方法並分析其原始碼,通過分析原始碼,我們瞭解到了其內部Looper的建立、獲取、開啟、退出的過程,加深了對HandlerThread原理的理解,更有利於以後的使用。

相關文章