天天看點

深入了解 Handler 消息機制

上一篇 - 消息機制 Handler 使用 文章講了 Handler 的一個概述和基本的使用方法,這裡還有一點需要強調一下:對于初學者一定要将你建立的子線程區分開,Handler 是你在子線程執行完,準備進行線程切換執行其他操作時才開始使用 Handler

本篇主要對 Handler 的工作原理進行分析,即 Handler、Looper、MessageQueue 三者是如何工作的,從源碼層面來分析下,本篇的主要内容如下:

深入了解 Handler 消息機制

Looper

首先來說 Looper,實際上 Handler 最開始是和 Looper 關聯起來的,上一篇中對 Handler 的構造函數進行了介紹,無論有參構造還是無參構造,其實都是需要有 Looper的。

public Handler(Looper looper) {
        this(looper, null, false);
    }

public Handler(Looper looper, Callback callback) {
    this(looper, callback, false);
}

           

這兩個好了解,構造中直接需要有 Looper,可以使主線程的 Looper,也可以是你自己建立的線程所屬的 Looper。

主線程 Looper 擷取方法:

public static Looper getMainLooper() {
        synchronized (Looper.class) {
            return sMainLooper;
        }
}
           

如果是你自己建立的子線程,那麼如果建立子線程的 Looper 呢?

// 在目前線程中建立 Looper,但是隻能建立一次,重複建立(即重複調用這個方法,或報錯)
public static void prepare() {
        prepare(true);
}

// 建立之後,還需要将 Looper運轉起來,才能執行消息隊列中的消息
public static void loop() {
    ...
}
           

就像下面這個小例子,就是在子線程中使用 Handler,其他線程就可以使用這個 Handler,發送消息後,消息會在這個 new Thread 線程中執行。

private Handler handler;

new Thread()
		{
			public void run()
			{
 
				Looper.prepare();
				
				handler = new Handler()
				{
					public void handleMessage(android.os.Message msg)
					{
						Log.e("TAG",Thread.currentThread().getName());
					};
				};
                 Looper.loop();
			    
			}
		    
		}

           

好,回到 Looper 分析中來,那麼無參數構造的 Handler 呢?最終是調用這個構造方法。

public Handler(Callback callback, boolean async) {
    
        ...

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

這個方法中有一個 Looper.myLooper() 方法,通過這個方法就是擷取到了 Looper,具體是怎麼擷取?分下這個方法。

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

看到是通過 ThreadLocal 來擷取的,ThreadLocal 是線程内部維護資料的一個變量,能夠存儲目前線程的一些資料,隻有在目前線程才能夠擷取目前線程中的資料, Looper 屬于線程的資料,是以可以通過 ThreadLocal 可以擷取,主要的過程就是通過 ThreadLocal 的 put 和 get 方法,以 key-value 的形式來完成的,key 就是目前線程。那麼什麼時候将線程的 Looper 存儲到 ThreadLocal 中的呢?就是建立 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));
}
           

看到這段代碼是不是就了解了。

好,接着往下走,擷取到 Looper 的目的是什麼呢?mQueue = mLooper.mQueue, 答案是為了擷取到 MessageQueue。

權限是包内權限,可以知道 Handler 和 Looper 類處于同一個包下 .../android/os 
final MessageQueue mQueue;
           

擷取目前線程的 MessageQueue,有這幾種方法,都是通過 Looper 來擷取的

// (1)同一個包内,直接使用
mQueue = mLooper.mQueue;

// (2)已經擷取了 Looper
public @NonNull MessageQueue getQueue() {
    return mQueue;
}

// (3)沒有擷取looper ,不在同一個保内,當時在目前線程
public static @NonNull MessageQueue myQueue() {
    return myLooper().mQueue;
}
           

Looper 持有 MessageQueue,MessageQueue 是在什麼時候建立的呢?看下 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));
}

private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
}

// MessageQueue.java
MessageQueue(boolean quitAllowed) {
        mQuitAllowed = quitAllowed;
        mPtr = nativeInit();
}
           

代碼比較清晰,就是在建立 Looper 時建立 MessageQueue,既然 一個線程的 Looper 隻能有一個,同理,MessageQueue 也是隻能有一個。本文最開始的的問題,如何切換到主線程?,Looper 和 MessageQueue 是在主線程,那麼通過 Handler 發送的消息自然就在主線程中了。

對于 Looper 來講,剩下就是一個最重要的方法,loop()

/**
     * Run the message queue in this thread. Be sure to call
     * {@link #quit()} to end the 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;

        // 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();
        ...

        boolean slowDeliveryDetected = false;

        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);
                dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
            
            ...

            msg.recycleUnchecked();
        }
}
    
           

首先是擷取 MessageQueue,然後 Looper 中通過一個無限循環,不斷從消息隊列中取出消息,然後通過調用 msg.target.dispatchMessage(msg); 來執行消息,雖然我們可能不知道 msg 中的 target 是啥?但是可以猜想一下,是不是應該是 Handler 呢?因為我們通過 Handler 來執行我們在主線程(準确說是 Handler 線程,這裡隻是以主線程為例)想幹事的事情,即 handleMessage 或者是 Runnable 的 run 方法,下面再去驗證想法。

到這裡會有一些疑問?

  • 為什麼是無限循環,這樣的話,CPU 滿載,手機豈不是一直滿負荷在跑,變得燙手。。。
  • 什麼時候 msg 為 null,這時候 return 之後會怎麼樣?
  • Message msg = queue.next(); // might block 可能會阻塞,含義是什麼?

這幾個問題中,到這裡暫時隻能解釋第二個,如果 msg 為空,傳回之後 loop() 方法結束。這裡以 應用程式的主線程為例,在主線程,主要是更新 UI,也就是在 Looper 中取出來的消息是用來更新 UI 的操作,既然都不能更新 UI,那麼也隻有應用程式死掉了,主線程退出的情況。

對于其他的子線程,也是消息隊列中沒有消息了,消息隊列退出,目前子線程也就結束了。

現在我們知道 Handler 擷取了 Loopper,Looper 中有消息隊列,也能夠擷取到,即 Handler 持有 Looper 和 MessageQueue,對于 Handler 來說,有了這些,能夠做些什麼,下面就來看看 Handler。

Handler

Handler 的主要原理,可以看一下下圖:

深入了解 Handler 消息機制

實際上,Handler 做的主要就是兩件事,發送消息,然後等到消息處理時進行執行。先來看發送消息,在上一篇文章中提到,發消息有兩種方式,send 和 post 方式,最終都是調用 sendMessageAtTime 方法。

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 方法中 uptimeMillis 參數指定消息執行的絕對時間,接着将消息加入到消息隊列中,MessageQueue 就是上面提到的,通過 Looper 擷取的消息隊列。

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

           

(1)msg.target = this 這一句驗證了我們上面的猜想,target 就是 Handler 本身,在 Looper 中 loop() 方法中執行的 msg.target.dispatchMessage(msg),就是回調 Handler 的 dispatchMessage () 方法。

(2)中間部分代碼,如果設定的是異步執行,需要将消息打上異步的标記。預設情況下,消息隊列中都是同步執行的,但是會根據設定的時間來區分消息執行的先後順序。

将消息加入到消息隊列中的具體操作在下文介紹 MessageQueue 時再詳細分析。接下來看一下,消息的執行,即 dispatchMessage。

/**
 * Handle system messages here.
 */
public void dispatchMessage(Message msg) {
    // 通過 post 方法發送的消息
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        // 通過 send 方法發送的消息
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        } 
        // handler 的預設方法,空方法
        handleMessage(msg);
    }
}
           

這個方法就比較簡單,分為 3 種情況:

(1) 第 1 種,通過 post 方法發送的消息,post(Runnable r),是一個 Runnable 對象,然後指派到 Message 的 callback 變量,執行時也就是調用 callback 的 run 方法,注意這裡雖然是 Runnable 對象,但是跟 Thread 中的 Runnable 是不一樣的,這裡僅僅是為了執行 run 方法而已。

private static void handleCallback(Message message) {
        message.callback.run();
}
           

(2) 第 2 種, 通過 send 方法發送的消息,這個就是我們構造 public Handler(Callback callback) 這樣一個 Handler,執行時就會通過 Callback 對象來執行消息。Callback 是一個接口,方法就是我們熟悉的 handleMessage 方法,是以 mCallback.handleMessage(msg) 就是調用這個接口的方法了。

public interface Callback {
        public boolean handleMessage(Message msg);
}
           

(3) 第 3 種,在 Handler 中 handleMessage 是一個空方法

public void handleMessage(Message msg) {
    }
           

也可以進行重寫,但是一般情況下,我們使用前兩種方式較多。

MessageQueue

Message

介紹消息隊列之前,先介紹下 Message。主要看下 Message 中的參數

public int what;

    public int arg1;

    public int arg2;

    public Object obj;
    
    /*package*/ long when;

    /*package*/ Bundle data;

    /*package*/ Handler target;

    /*package*/ Runnable callback;

    // sometimes we store linked lists of these things
    /*package*/ Message next;
           

以上是 Message 中能夠攜帶的參數:

  • 一般我們使用 what 來作為辨別,表明是哪種消息
  • arg1 和 arg2 是 int 類型參數
  • obj 是對象類型參數,
  • 當然也可以使用 Bundle
  • when 是指定的消息執行的時刻
  • target 就是我們上面猜測得 Handler 對象
  • callback 使用 post 方法發送消息的 Runnable 對象
  • next 指向下一個消息,可以得知,消息隊列中是單連結清單形式的

MessageQueue

先來看一下消息隊列的構造,還記得上面在建立 Looper 時就是建立 MessageQueue 的時刻。

MessageQueue(boolean quitAllowed) {
        mQuitAllowed = quitAllowed;
        mPtr = nativeInit();
}
           

mQuitAllowed 為 true 時表明消息隊列可以退出,意味着為 false 是不能退出,具體這個有什麼影響,這裡不去探究了。

mPtr = nativeInit() 是一個 native 方法,下面一段話是引自老羅Android應用程式消息處理機制(Looper、Handler)分析這篇文章

(1)在 Java 層,建立了一個 Looper 對象,這個 Looper 對象是用來進入消息循環的,它的内部有一個消息隊列 MessageQueue 對象 mQueue;

(2)在 JNI 層,建立了一個 NativeMessageQueue 對象,這個 NativeMessageQueue 對象儲存在 Java 層的消息隊列對象 mQueue 的成員變量 mPtr 中;

(3)在 C++ 層,建立了一個 Looper 對象,儲存在 JNI 層的 NativeMessageQueue 對象的成員變量 mLooper 中,這個對象的作用是,當 Java 層的消息隊列中沒有消息時,就使 Android 應用程式主線程進入等待狀态,而當 Java 層的消息隊列中來了新的消息後,就喚醒 Android 應用程式的主線程來處理這個消息。

實際上,java 層 Looper 在底層都能對應到底層,因為是涉及到線程操作,包括等待、喚醒,這些操作需要在底層來完成。

添加消息

消息隊列的構造分析之後,接下來看一下如何把消息加入到消息隊列當中的。

boolean enqueueMessage(Message msg, long when) {
        if (msg.target == null) {
            throw new IllegalArgumentException("Message must have a target.");
        }
        if (msg.isInUse()) {
            throw new IllegalStateException(msg + " This message is already in use.");
        }

        synchronized (this) {
            if (mQuitting) {
                IllegalStateException e = new IllegalStateException(
                        msg.target + " sending message to a Handler on a dead thread");
                Log.w(TAG, e.getMessage(), e);
                msg.recycle();
                return false;
            }

            msg.markInUse();
            msg.when = when;
            Message p = mMessages;
            boolean needWake;
            // 情況一:直接将消息加入到消息隊列頭部(單連結清單頭部)
            if (p == null || when == 0 || when < p.when) {
                // New head, wake up the event queue if blocked.
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                // Inserted within the middle of the queue.  Usually we don't have to wake
                // up the event queue unless there is a barrier at the head of the queue
                // and the message is the earliest asynchronous message in the queue.
                
                // 情況二:根據時間點比較将消息插入到連結清單中
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;
            }

            // We can assume mPtr != 0 because mQuitting is false.
            if (needWake) {
                // 通過底層操作來喚醒
                nativeWake(mPtr);
            }
        }
        return true;
    }
           

分析:将消息加入到消息隊列中,實際上就是操作單連結清單的過程,分為兩種情況:

(1)情況一:直接将消息加入到消息隊列頭部(單連結清單頭部)這種情況的條件是下列條件中的一個:

  • p == null 消息隊列為空
  • when == 0 目前需要加入的消息的時間點為0,意味着需要立即執行的消息,是以需要加入到隊列頭部
  • when < p.when 目前需要加入的消息的時間點比消息隊列頭部的消息的時間小早,也就是說需要先執行的消息,但不一定立即執行

(2) 情況二:根據時間點比較将消息插入到連結清單中

典型的單連結清單插入操作,首先找到根據消息的時間點來找到插入的位置,然後将消息插入到連結清單中。

取出消息

取出消息是通過 next() 取出消息的,該方法在 Looper 的 loop() 方法中被調用,用來擷取需要執行的消息

Message next() {
    
        ...
            
        final long ptr = mPtr;
        if (ptr == 0) {
            return null;
        }

        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        int nextPollTimeoutMillis = 0;
        for (;;) {
        
            // 線程等待狀态
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }
            nativePollOnce(ptr, nextPollTimeoutMillis);

            synchronized (this) {
                // Try to retrieve the next message.  Return if found.
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                if (msg != null && msg.target == null) {
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                if (msg != null) {
                    if (now < msg.when) {
                        // Next message is not ready.  Set a timeout to wake up when it is ready.
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        // Got a message.
                        mBlocked = false;
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }

                // Process the quit message now that all pending messages have been handled.
                if (mQuitting) {
                    dispose();
                    return null;
                }

                
            }

            ... 
            
            // While calling an idle handler, a new message could have been delivered
            // so go back and look again for a pending message without waiting.
            nextPollTimeoutMillis = 0;
        }
    }

           

将代碼簡化了一下,for 循環中主要執行取出消息的操作,首先是找到一個消息,這個消息是最先執行的。找到之後,先看這個消息的時間點,如果到了執行的時間就傳回消息,并從連結清單中删除,傳回給 Looper;如果時間還沒到就等待,nativePollOnce(ptr, nextPollTimeoutMillis);

什麼情況下線程會進入等待狀态?兩種情況,一是當消息隊列中沒有消息時,它會使線程進入等待狀态;二是消息隊列中有消息,但是消息指定了執行的時間,而現在還沒有到這個時間,線程也會進入等待狀态。消息隊列中的消息是按時間先後來排序的。

下面回答下上面的未給出答案的兩個問題:

(1)為什麼是無限循環,手機不燙手?

(2)Message msg = queue.next(); // might block 可能會阻塞,含義是什麼?

線程的等待喚醒底層采用的是管道機制,管道就是一個檔案,在管道的兩端,分别是兩個打開檔案檔案描述符,這兩個打開檔案描述符都是對應同一個檔案,其中一個是用來讀的,别一個是用來寫的,一般的使用方式就是,一個線程通過讀檔案描述符中來讀管道的内容,當管道沒有内容時,這個線程就會進入等待狀态,而另外一個線程通過寫檔案描述符來向管道中寫入内容,寫入内容的時候,如果另一端正有線程正在等待管道中的内容,那麼這個線程就會被喚醒。

是以阻塞時,線程處于等待狀态,這時候 Linux 系統中的 epoll 機制,并不會占用很高的 CPU 使用率,是以即使是無限循環,也不會導緻手機發燙啦…

總結

Handler 中持有 Looper,并通過 Looer 擷取 MessageQueue,然後發送消息就是将消息加入到 MessageQueue 中,并且在 Message 中将 Handler 本身攜帶過去,在 Looper 的 loop 循環中 通過 MessageQueue 的 next 方法 得到消息後,在 Message 中取出 target,即 Handler,然後回調 dispatchMessage,執行我們自定義的方法。

本篇對 Handler 的分析就到這裡,如果文中有錯誤的地方,希望在評論區給出指正!另外如果對 Handler 的底層想進行研究的話,建議看看老羅的文章。

參考

《安卓開發藝術探索》

Android應用程式消息處理機制(Looper、Handler)分析

Android 異步消息處理機制 讓你深入了解 Looper、Handler、Message三者關系