Android-Handler訊息機制實現原理

developerzjy發表於2019-06-25

一、訊息機制流程簡介

在應用啟動的時候,會執行程式的入口函式main(),main()裡面會建立一個Looper物件,然後通過這個Looper物件開啟一個死迴圈,這個迴圈的工作是,不斷的從訊息佇列MessageQueue裡面取出訊息即Message物件,並處理。然後看下面兩個問題:
迴圈拿到一個訊息之後,如何處理?
是通過在Looper的迴圈裡呼叫Handler的dispatchMessage()方法去處理的,而dispatchMessage()方法裡面會呼叫handleMessage()方法,handleMessage()就是平時使用Handler時重寫的方法,所以最終如何處理訊息由使用Handler的開發者決定。
MessageQueue裡的訊息從哪來?
使用Handler的開發者通過呼叫sendMessage()方法將訊息加入到MessageQueue裡面。

上面就是Android中訊息機制的一個整體流程,也是 “Android中Handler,Looper,MessageQueue,Message有什麼關係?” 的答案。通過上面的流程可以發現Handler在訊息機制中的地位,是作為輔助類或者工具類存在的,用來供開發者使用。

對於這個流程有兩個疑問:

  • Looper中是如何能呼叫到Handler的方法的?
  • Handler是如何能往MessageQueue中插入訊息的?

這兩個問題會在後面給出答案,下面先來通過原始碼,分析一下這個過程的具體細節:

二、訊息機制的原始碼分析

首先main()方法位於ActivityThread.java類裡面,這是一個隱藏類,原始碼位置:frameworks/base/core/java/android/app/ActivityThread.java

public static void main(String[] args) {
    ......
    Looper.prepareMainLooper();

    ActivityThread thread = new ActivityThread();
    thread.attach(false);

    if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler();
    }

    Looper.loop();

    throw new RuntimeException("Main thread loop unexpectedly exited");
}

Looper的建立可以通過Looper.prepare()來完成,上面的程式碼中prepareMainLooper()是給主執行緒建立Looper使用的,本質也是呼叫的prepare()方法。建立Looper以後就可以呼叫Looper.loop()開啟迴圈了。main方法很簡單,不多說了,下面看看Looper被建立的時候做了什麼,下面是Looper的prepare()方法和變數sThreadLocal:

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

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));
}

很簡單,new了一個Looper,並把new出來的Looper儲存到ThreadLocal裡面。ThreadLocal是什麼?它是一個用來儲存資料的類,類似HashMap、ArrayList等集合類。它的特點是可以在指定的執行緒中儲存資料,然後取資料只能取到當前執行緒的資料,比如下面的程式碼:

ThreadLocal<Integer> mThreadLocal = new ThreadLocal<>();
private void testMethod() {

    mThreadLocal.set(0);
    Log.d(TAG, "main  mThreadLocal=" + mThreadLocal.get());

    new Thread("Thread1") {
        @Override
        public void run() {
            mThreadLocal.set(1);
            Log.d(TAG, "Thread1  mThreadLocal=" + mThreadLocal.get());
        }
    }.start();

    new Thread("Thread2") {
        @Override
        public void run() {
            mThreadLocal.set(2);
            Log.d(TAG, "Thread1  mThreadLocal=" + mThreadLocal.get());
        }
    }.start();

    Log.d(TAG, "main  mThreadLocal=" + mThreadLocal.get());
}

輸出的log是

main  mThreadLocal=0
Thread1  mThreadLocal=1
Thread2  mThreadLocal=2
main  mThreadLocal=0

通過上面的例子可以清晰的看到ThreadLocal存取資料的特點,只能取到當前所線上程存的資料,如果所線上程沒存資料,取出來的就是null。其實這個效果可以通過HashMap<Thread, Object>來實現,考慮執行緒安全的話使用ConcurrentMap<Thread, Object>,不過使用Map會有一些麻煩的事要處理,比如當一個執行緒結束的時候我們如何刪除這個執行緒的物件副本呢?如果使用ThreadLocal就不用有這個擔心了,ThreadLocal保證每個執行緒都保持對其執行緒區域性變數副本的隱式引用,只要執行緒是活動的並且 ThreadLocal 例項是可訪問的;線上程消失之後,其執行緒區域性例項的所有副本都會被垃圾回收(除非存在對這些副本的其他引用)。更多ThreadLocal的講解參考:Android執行緒管理之ThreadLocal理解及應用場景

好了回到正題,prepare()建立Looper的時候同時把建立的Looper儲存到了ThreadLocal中,通過對ThreadLocal的介紹,獲取Looper物件就很簡單了,sThreadLocal.get()即可,原始碼提供了一個public的靜態方法可以在主執行緒的任何地方獲取這個主執行緒的Looper(注意一下方法名myLooper(),多個地方會用到):

public static @Nullable Looper myLooper() {
    return sThreadLocal.get();
}

Looper建立完了,接下來開啟迴圈,loop方法的關鍵程式碼如下:

public static void loop() {
    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;

    for (;;) {
        Message msg = queue.next(); // might block
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return;
        }

        try {
            msg.target.dispatchMessage(msg);
        } finally {
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }

        msg.recycleUnchecked();
    }
}

上面的程式碼,首先獲取主執行緒的Looper物件,然後取得Looper中的訊息佇列final MessageQueue queue = me.mQueue;,然後下面是一個死迴圈,不斷的從訊息佇列裡取訊息Message msg = queue.next();,可以看到取出的訊息是一個Message物件,如果訊息佇列裡沒有訊息,就會阻塞在這行程式碼,等到有訊息來的時候會被喚醒。取到訊息以後,通過msg.target.dispatchMessage(msg);來處理訊息,msg.target 是一個Handler物件,所以這個時候就呼叫到我們重寫的Hander的handleMessage()方法了。
msg.target 是在什麼時候被賦值的呢?要找到這個答案很容易,msg.target是被封裝在訊息裡面的,肯定要從傳送訊息那裡開始找,看看Message是如何封裝的。那麼就從Handler的sendMessage(msg)方法開始,過程如下:

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);
}

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    msg.target = this;
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}

可以看到最後的enqueueMessage()方法中msg.target = this;,這裡就把傳送訊息的handler封裝到了訊息中。同時可以看到,傳送訊息其實就是往MessageQueue裡面插入了一條訊息,然後Looper裡面的迴圈就可以處理訊息了。Handler裡面的訊息佇列是怎麼來的呢?從上面的程式碼可以看到enqueueMessage()裡面的queue是從sendMessageAtTime傳來的,也就是mQueue。然後看mQueue是在哪初始化的,看Handler的構造方法如下:

public Handler(Callback callback, boolean async) {
    if (FIND_POTENTIAL_LEAKS) {
        final Class<? extends Handler> klass = getClass();
        if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                (klass.getModifiers() & Modifier.STATIC) == 0) {
            Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                klass.getCanonicalName());
        }
    }

    mLooper = Looper.myLooper();
    if (mLooper == null) {
        throw new RuntimeException(
            "Can't create handler inside thread that has not called Looper.prepare()");
    }
    mQueue = mLooper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}

mQueue的初始化很簡單,首先取得Handler所線上程的Looper,然後取出Looper中的mQueue。這也是Handler為什麼必須在有Looper的執行緒中才能使用的原因,拿到mQueue就可以很容易的往Looper的訊息佇列裡插入訊息了(配合Looper的迴圈+阻塞就實現了傳送接收訊息的效果)。

以上就是主執行緒中訊息機制的原理。

那麼,在任何執行緒下使用handler的如下做法的原因、原理、內部流程等就非常清晰了:

new Thread() {
    @Override
    public void run() {
        Looper.prepare();
        Handler handler = new Handler();
        Looper.loop();
    }
}.start();
  1. 首先Looper.prepare()建立Looper並初始化Looper持有的訊息佇列MessageQueue,建立好後將Looper儲存到ThreadLocal中方便Handler直接獲取。
  2. 然後Looper.loop()開啟迴圈,從MessageQueue裡面取訊息並呼叫handler的 dispatchMessage(msg) 方法處理訊息。如果MessageQueue裡沒有訊息,迴圈就會阻塞進入休眠狀態,等有訊息的時候被喚醒處理訊息。
  3. 再然後我們new Handler()的時候,Handler構造方法中獲取Looper並且拿到Looper的MessageQueue物件。然後Handler內部就可以直接往MessageQueue裡面插入訊息了,插入訊息即傳送訊息,這時候有訊息了就會喚醒Looper迴圈去處理訊息。處理訊息就是呼叫dispatchMessage(msg) 方法,最終呼叫到我們重寫的Handler的handleMessage()方法。


三、通過一些問題的研究加強對訊息機制的理解

原始碼分析完了,下面看一下文章開頭的兩個問題:

  • Looper中是如何能呼叫到Handler的方法的?
  • Handler是如何能往MessageQueue中插入訊息的?

這兩個問題原始碼分析中已經給出答案,這裡做一下總結,首先搞清楚以下物件在訊息機制中的關係:

Looper,MessageQueue,Message,ThreadLocal,Handler
  1. Looper物件有一個成員MessageQueue,MessageQueue是一個訊息佇列,用來儲存訊息Message
  2. Message訊息中帶有一個handler物件,所以Looper取出訊息後,可以很方便的呼叫到Handler的方法(問題1解決)
  3. Message是如何帶有handler物件的?是handler在傳送訊息的時候把自己封裝到訊息裡的。
  4. Handler是如何傳送訊息的?是通過獲取Looper物件從而取得Looper裡面的MessageQueue,然後Handler就可以直接往MessageQueue裡面插入訊息了。(問題2解決)
  5. Handler是如何獲取Looper物件的?Looper在建立的時候同時把自己儲存到ThreadLocal中,並提供一個public的靜態方法可以從ThreadLocal中取出Looper,所以Handler的構造方法裡可以直接呼叫靜態方法取得Looper物件。

帶著上面的一系列問題看原始碼就很清晰了,下面是知乎上的一個問答:

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

原因很簡單,迴圈裡有阻塞,所以死迴圈並不會一直執行,相反的,大部分時間是沒有訊息的,所以主執行緒大多數時候都是處於休眠狀態,也就不會消耗太多的CPU資源導致卡死。

  1. 阻塞的原理是使用Linux的管道機制實現的
  2. 主執行緒沒有訊息處理時阻塞在管道的讀端
  3. binder執行緒會往主執行緒訊息佇列裡新增訊息,然後往管道寫端寫一個位元組,這樣就能喚醒主執行緒從管道讀端返回,也就是說looper迴圈裡queue.next()會呼叫返回...

這裡說到binder執行緒,具體的實現細節不必深究,考慮下面的問題:
主執行緒的死迴圈如何處理其它事務?
首先需要看懂這個問題,主執行緒進入Looper死迴圈後,如何處理其他事務,比如activity的各個生命週期的回撥函式是如何被執行到的(注意這裡是在同一個執行緒下,程式碼是按順序執行的,如果在死迴圈這阻塞了,那麼進入死迴圈後迴圈以外的程式碼是如何執行的)。
首先再看main函式的原始碼

Looper.prepareMainLooper();

ActivityThread thread = new ActivityThread();
thread.attach(false);

if (sMainThreadHandler == null) {
    sMainThreadHandler = thread.getHandler();
}

Looper.loop();

在Looper.prepare和Looper.loop之間new了一個ActivityThread並呼叫了它的attach方法,這個方法就是開啟binder執行緒的,另外new ActivityThread()的時候同時會初始化它的一個H型別的成員,H是一個繼承了Handler的類。此時的結果就是:在主執行緒開啟loop死迴圈之前,已經啟動binder執行緒,並且準備好了一個名為H的Handler,那麼接下來在主執行緒死迴圈之外做一些事務處理就很簡單了,只需要通過binder執行緒向H傳送訊息即可,比如傳送 H.LAUNCH_ACTIVITY 訊息就是通知主執行緒呼叫Activity.onCreate() ,當然不是直接呼叫,H收到訊息後會進行一系列複雜的函式呼叫最終呼叫到Activity.onCreate()。
至於誰來控制binder執行緒來向H發訊息就不深入研究了,下面是《Android開發藝術探索》裡面的一段話:

ActivityThread 通過 ApplicationThread 和 AMS 進行程式間通訊,AMS 以程式間通訊的方式完成 ActivityThread 的請求後會回撥 ApplicationThread 中的 Binder 方法,然後 ApplicationThread 會向 H 傳送訊息,H 收到訊息後會將 ApplicationThread 中的邏輯切換到 ActivityThread 中去執行,即切換到主執行緒中去執行,這個過程就是主執行緒的訊息迴圈模型。

這個問題就到這裡,更多內容看知乎原文

最後

和其他系統相同,Android應用程式也是依靠訊息驅動來工作的。網上的這句話還是很有道理的。


文章參考:

《Android開發藝術探索》
Android中為什麼主執行緒不會因為Looper.loop()裡的死迴圈卡死?
Android執行緒管理之ThreadLocal理解及應用場景
Android 訊息機制——你真的瞭解Handler
Android Handler到底是什麼

相關文章