Handler的原理

-l囧l-發表於2017-10-17

在使用Handler的過程中主要涉及到以下幾個類Looper、Handler、Message、還有一個隱藏的Message Queue,它直接與Looper互動,我們不會直接接觸。

Handler建立

Handler#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());
            }
        }
        //關聯Looper
        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");
        }
        //關聯MessageQueue 同時也說明了MessageQueue 屬於Looper
        mQueue = mLooper.mQueue;
       //注意這裡的mCallback 它也是訊息處理方式的一種,下文會有分析
        mCallback = callback;
        mAsynchronous = async;
    }複製程式碼

上面的程式碼展示了Handler如何與Looper、MessageQueue關聯,下面我們看下Looper是如何被建立得的,以及它的MessageQueue是怎麼建立的。
Looper#myLooper

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

public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }複製程式碼

可以看到Looper 是通過ThreadLocal.get得到的,那ThreadLocal又是什麼呢?
通過註釋我們可以發現,ThreadLocal是一個跟執行緒繫結的資料儲存類,它可以在指定的執行緒中儲存資料,同時也只能在指定執行緒中才能獲取資料,對於其他執行緒來時是無效的,既然是集合肯定有set和get方法。下面我們來看下
ThreadLocal#set

 public void set(T value) {
        //得到當前正在執行的執行緒
        Thread t = Thread.currentThread();
        //ThreadLocalMap 是一個自定義的hash map 用來儲存資料
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }複製程式碼

我們說過ThreadLocal是跟指定執行緒繫結的,其實從下面程式碼就能看出來

ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }複製程式碼

程式碼很簡單,我就不解釋了。
好了既然我們知道了ThreadLocal,那接下來我們就看下Looper。既然ThreadLocal.get()得到的是Looper,我們就理由相信這個Looper是跟UI執行緒繫結的。可是這個Looper又是在哪初始化的呢? 而且初始化肯定是通過ThreadLocal.set方式呼叫的。
Looper#prepare

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

private Looper(boolean quitAllowed) {
        //建立Looper的同時建立了MessageQueue
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }複製程式碼

那Looper#prepare又是在哪被呼叫了呢?最終我們在ActivityThread#main函式中找到了,也就是程式啟動時呼叫的。
ActivityThread#main

public static void main(String[] args) {
    //............. 無關程式碼...............
     此時跟UI執行緒繫結的Looper已經建立了
    Looper.prepareMainLooper();
    //開啟無線迴圈來不斷的從訊息佇列中拿訊息
    Looper.loop();
    throw new RuntimeException("Main thread loop unexpectedly exited");
}複製程式碼

Looper#loop(省略一部分程式碼)

public static void loop() {
    //獲得一個跟UI執行緒繫結的 Looper 物件
    final Looper me = myLooper();
    // 拿到 looper 對應的 mQueue 物件
    final MessageQueue queue = me.mQueue;
    //死迴圈監聽(如果沒有訊息變化,他不會工作的) 不斷輪訓 queue 中的 Message
    for (;;) {
        // 通過 queue 的 next 方法拿到一個 Message
        Message msg = queue.next(); // might block
        //空判斷
        if (msg == null)return;
        //訊息分發   此處的target其實就是繫結的Handler
        msg.target.dispatchMessage(msg);
        //回收操作  
        msg.recycleUnchecked();
    }
}複製程式碼

到這裡的時候我先你的腦子肯定會閃過一個念頭進入死迴圈那程式不就卡死了嗎,程式還在嗎執行呢?這個問題我們先放放,下面會簡答,這裡可以先說結論。首先MessageQueue不是傳統的阻塞佇列,因為它沒有繼承任何佇列,同時內部也沒有持有任何阻塞佇列的物件,那它是如何實現阻塞佇列的效果的呢?其實這裡使用了linux的epoll技術,感興趣的朋友可以深入研究下,下次如果有時間的話我也會寫一篇相關的博文來介紹。下面是摘自百度的epoll介紹:

epoll是Linux核心為處理大批量檔案描述符而作了改進的poll,是Linux下多路複用IO介面select/poll的增強版本,它能顯著提高程式在大量併發連線中只有少量活躍的情況下的系統CPU利用率。另一點原因就是獲取事件的時候,它無須遍歷整個被偵聽的描述符集,只要遍歷那些被核心IO事件非同步喚醒而加入Ready佇列的描述符集合就行了。epoll除了提供select/poll那種IO事件的水平觸發(Level Triggered)外,還提供了邊緣觸發(Edge Triggered),這就使得使用者空間程式有可能快取IO狀態,減少epoll_wait/epoll_pwait的呼叫,提高應用程式效率

Handler建立的另一種姿勢
上文我們分析了在UI執行緒(主執行緒)中建立的Handler,並會得到一個UI Looper,那假如我們新建一個執行緒並在其中建立Handler會發生什麼?

public class LooperThread extends Thread {
    private Handler handler1;
    private Handler handler2;
    @Override
    public void run() {
        // 將當前執行緒初始化為Looper執行緒
        Looper.prepare();
        // 例項化兩個handler
        handler1 = new Handler();
        handler2 = new Handler();
        // 開始迴圈處理訊息佇列
        Looper.loop();
    }
}複製程式碼

在子執行緒中我們可以建立多個Handler,但是必須手動呼叫Looper.prepare();和Looper.loop();而且Looper.prepare()必須在Handler建立之前,這是為什麼呢?我們回到上文中的程式碼。因為在建立Handler的時候會檢查mLooper 是否為null,為null會丟擲異常,並且此處的Looper是跟當前執行緒繫結的。

public Handler(Callback callback, boolean async) {
        //關聯Looper
        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");
        }
    }複製程式碼

要保證mLooper 不為null,必須要先呼叫Looper.prepare()進行Looper的建立並繫結當前執行緒。

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));
    }複製程式碼

在建立完Handler之後還需要手動呼叫Looper.loop()開啟訊息迴圈佇列。

階段總結
好了到這,我們已經分析完Handler以及Looper和MessageQueue的建立和關聯,並且還知道,建立完Looper和MessageQueue之後會進入一個死迴圈一直等待訊息的到來並拿出訊息進行分發處理,否則會一直阻塞,其中這裡的阻塞佇列利用了linux的epoll技術。另外,我們還知道一個執行緒可以有多個Handler,但是隻能有一個Looper!

Handler傳送訊息

有了handler之後,我們就可以使用 post(Runnable), postAtTime(Runnable, long), postDelayed(Runnable, long), sendEmptyMessage(int), sendMessage(Message), sendMessageAtTime(Message, long)和 sendMessageDelayed(Message, long)這些方法向MQ上傳送訊息了。從上面這些方法你可能會以為我們可以傳送2中訊息型別:Message和Runnable,但其實發出的Runnable最終也會被封裝成Message,下面我們來看程式碼:
Handler#xxx

public final boolean post(Runnable r)
    {
       return  sendMessageDelayed(getPostMessage(r), 0);
    }

private static Message getPostMessage(Runnable r) {
        Message m = Message.obtain();
        m.callback = r;
        return m;
    }


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);
    }複製程式碼

可以看到不管通過哪種方法傳送訊息,最終都會進入到sendMessageAtTime方法,並執行enqueueMessage入隊操作。
Handler#enqueueMessage

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        //注意看這行程式碼 我們將Handler賦值給Message的target 
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }複製程式碼

階段總結
通過上面的程式碼得到一條原則就是:訊息傳送和處理遵循『誰傳送,誰處理』的原則。過程還是比較簡單的,下面有2點要注意的地方:

  1. 我們傳送的2中型別訊息最終都會被封裝成Message物件進行傳送
  2. 在建立完訊息之後我們會將Message和Handler通過msg.target = this進行繫結,方便下面進行處理。

Handler處理訊息

Looper#loop

public static void loop() {
    //獲得一個 Looper 物件
    final Looper me = myLooper();
    // 拿到 looper 對應的 mQueue 物件
    final MessageQueue queue = me.mQueue;
    //死迴圈監聽(如果沒有訊息變化,他不會工作的) 不斷輪訓 queue 中的 Message
    for (;;) {
        // 通過 queue 的 next 方法拿到一個 Message
        Message msg = queue.next(); // might block
        //空判斷
        if (msg == null)return;
        //訊息分發   
        msg.target.dispatchMessage(msg);
        //回收操作  
        msg.recycleUnchecked();
    }
}複製程式碼

MessageQueue的工作方式是當有訊息被放入的時候MessageQueue.next()會返回Message物件,否則就會阻塞在這,拿到訊息以後會呼叫Message繫結的Handler來出來訊息,然後回回收訊息。下面讓我們看下訊息是如何被處理。
Handler#dispatchMessage

public void dispatchMessage(Message msg) {
      //callback對應Runnable物件 
        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) {
    }複製程式碼

可以看到,訊息處理分成了2中方式,一種是我們傳遞的Runnable物件,另一種是普通的Message物件。程式碼很簡單,handleCallback直接呼叫了Runnable.run,而handleMessage是空實現,需要我們重寫並且實現它。還有一種處理方式就是mCallback,是在建立Handler的時候通過都找引數傳入的,大家回去最上面通過註釋可以看到。

階段總結
通過上面得到程式碼,我們瞭解到在處理訊息的時候有三種方式,並且是有順序的。

  1. 如果有Runnable訊息就直接處理Runnable訊息,然後忽略其他訊息,所以Runnable的優先順序是最高的。
  2. 沒有Runnable訊息,查詢時候有通過建構函式傳入的Callback物件,有就處理並檢查時候處理成功,成功就直接returan,否則才呼叫最普通的Message物件處理。
  3. 普通Message的優先順序是最低的,並且需要我們自己來實現handleMessage方法。
  4. 從上面的程式碼中我們也驗證了Handler誰傳送就誰處理的原則,實現方式是通過將Handler賦值給Message.target來實現的。

epoll的真相

到此關於Handler的建立、訊息傳送以及訊息處理都分析完畢了。現在還剩下那個死迴圈的問題一直困擾著我們,那就是linux底層epoll到底是如何處理訊息的呢。本來這段程式碼想自己分析的,但在查詢資料的時候發現已經有好多人分析過了,並且分析的比較透徹,其中MessageQueue涉及到很多native方法,我這裡就不分析,下面放上2篇分析的比較好的博文供大家自己來參考。
深入理解 MessageQueue
Looper 中的 loop() 方法是如何實現阻塞的

相關文章