【Android】Handler機制源碼解析

前言

Handler機制是一套在Android端用於跨線程傳遞消息的機制。它內部維護了一個由根據消息觸發時間進行排列的消息隊列。並由一個死循環不斷的從消息隊列中獲取隊列最前面的消息,然後交由Handler進行消息分發。

通常,在Android中切換線程時,會使用到Handler機制。比如,需要在子線程更新UI時,我們會調用一個在主線程實例化的Handler去發送一條通知消息,然後在回調處收到通知消息後,去更新UI。

那麼,現在就有幾個問題了:

  1. Handler是如何做到線程切換的?
  2. Handler中的延時消息是如何實現的?
  3. 如何保證消息是按照時間順序到達?
  4. 所謂的同步消息屏障是什麼?有什麼用?

基礎知識

在開始源碼解析之前,需要先對幾個Handler機制涉及到的類進行一點簡單的瞭解。

整個機制包含以下4個主要的類:

  1. Handler,用於發送和處理消息。
  2. MessageQueue,用於對消息進行排列。
  3. Message,用於傳遞數據。內部是鏈式結構。
  4. Loop,用於讀取消息隊列並交由Handler分發。

OK,大概瞭解了每個類的作用之後,下面正式開始源碼解析。

實例化Handler

在使用Handler機制時,我們都會先new一個Handler對象出來,並傳入一個Callback或者重寫Handler的handleMessage()

那麼第一步,我們首先看看,當我們實例化Handler的時候,實際是幹了哪些事情。

	/**
	* 通常使用的構造函數
	*/
	public Handler() {
  	  this(null, false);
	}

	/**
	* 實際最後調用的構造函數
	*/
    public Handler(@Nullable Callback callback, boolean async) {
       ...省略部分無關代碼

        // 檢查是否調用過Looper.prepare()
        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對象,然後再獲取到Looper對象中的消息隊列mQueue。另外的mCallback在默認情況下是傳入的null,當然我們也可以傳入我們自己的Callback。async這個屬性在後面會用到,主要用來決定發送的消息是否是異步消息。

在這裏,我們看到了一個熟悉的報錯信息:Can't create handler insode ....。根據源碼可以知道,這是因爲Looper.myLooper()返回null導致的。提示我們,需要先調用Looper.prepare()才行。那麼先看看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");
    }
    // 向當前線程的map中放入Looper
    sThreadLocal.set(new Looper(quitAllowed));
}

首先調用sThreadLocal.get(),判斷是否爲null。根據錯誤信息,可以猜到sThreadLocal.get()返回的就是一個Looper對象。

然後,向sThreadLocal中放入了一個剛剛實例化的Looper。

sThreadLocal這個變量主要是保存一些當前線程的數據。所以這裏實際上就是爲我們實例化一個Looper並保存到當前線程。

到這裏,其實不難猜測,Looper.myLooper()實際上就是調用了sThreadLocal.get()去獲取一個當前線程的Looper對象。

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

果不其然。

到這裏,實例化Handler的源碼基本就解析完了。我們來總結一下,當我們實例化一個Handler對象時,實際上做了哪些操作:

  1. 調用Looper.myLooper()獲取一個當前線程的Looper,如果沒有的話會報錯提示先調用Looper.prepare()去新建一個Looper並保存到當前線程。
  2. 獲取Looper中的消息隊列mQueue

當實例化完Handler之後,下一步就是構造消息了。

構造消息Message

可能很多人都聽過,當我們要創建一條消息時,應該使用Message.obtain()而不是直接new Message(),那麼這是爲什麼呢?

public static Message obtain() {
    synchronized (sPoolSync) {
        if (sPool != null) {
            Message m = sPool;
            sPool = m.next;
            m.next = null;
            m.flags = 0; // clear in-use flag
            sPoolSize--;
            return m;
        }
    }
    return new Message();
}

通過源碼可以知道,obtain()實際上會進行一個複用的判斷,如果能複用的話就複用,而不是直接新建。這個複用機制可以減少新對象的創建,優化內存使用。

我們知道,Android的屏幕刷新時16ms一次的,而屏幕刷新就是使用的Handler機制實現的。那麼假如不進行Message複用的話,意味着每16ms就要創建一個Message對象。

知道如何創建Message之後,我們再來看看,Message內部有哪些比較關鍵的東西。
Message

asynchronous:標記該消息是否爲異步消息。

callback:當該消息被分發的時候會執行的動作。

data:該消息攜帶的數據。

inUse:標記該消息是否正在被使用。

target:將會由哪個Handler處理該消息。

whta:標記該消息。

when:該消息何時被分發

總結一下:

構造消息時,儘量使用Message.obtain(),而不是去new Message(),這樣可以減少新對象的創建。

一個Message,必不可少的就是target,因爲它決定了這條消息是由誰處理的。

發送消息Handler.sendMessage()

​ 消息構造好之後,就是將消息發送出去了。此時我們只需要調用Handler.sendMessage()就可以了,然後就可以在Handler.handleMessage()或者Callback中去接收發送的消息了。這樣就完成了線程之間的切換。那麼這一套操作的內部邏輯是怎麼樣子的呢?

public final boolean sendMessage(@NonNull Message msg) {
    return sendMessageDelayed(msg, 0);
}

public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}

public boolean sendMessageAtTime(@NonNull 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);
}

可以看到,當我們調用sendMessage()的時候,實際上還是調用到了enqueueMessage()。根據方法名可以猜測,這個方法應該是將Message放入到消息隊列中去了。

/**
 * 將消息Message入隊MessageQueue
 * @param queue
 * @param msg
 * @param uptimeMillis 該消息觸發的時間,系統自啓動運行的時間+傳遞消息時設置的延時時間
 * @return
 */
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
        long uptimeMillis) {
    msg.target = this;// 這裏重刷了一次msg的target對象
    msg.workSourceUid = ThreadLocalWorkSource.getUid();

    if (mAsynchronous) {// 是否異步消息,默認的使用是false
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}

這裏需要注意一下,message.target在這裏被設置成了當前的Handler。也就意味着假如在當初構造消息的時候,我們設置了target,那麼其實是無效的。Message由哪個Handler發送,那麼它也就被哪個Handler處理。

這裏設置完target之後,會根據當初構造Handler的時候傳入的參數判斷是否要將消息設置爲異步消息。

然後調用消息隊列queueenqueueMessage()方法。所以實際上,消息入隊的操作還是在MessageQueue中進行的,在Handler中僅僅只是對Message進行一些改變而已。

由於MessageQueue.enqueueMessage()代碼過長,這裏我將一段一段的進行分析,就不一次性貼全部的代碼了。

首先是消息的完整性檢查:

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

target一般來說不會出現爲null的情況,因爲我們用Handler.sendMessage()的時候,會重新設置一次target。主要是第二個檢測,檢測消息是否正在已經被使用過了。

​ 然後,是使用了一個同步代碼塊(synchronized)去進行正在的消息入隊操作。

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;
    }
    ...省略部分代碼
}

判斷當前消息隊列是否正在退出,根據源碼跟進發現,mQuitting下面這個調用鏈下會變爲true:

MessageQueue.quit()
---------Looper.quit()

即當Looper退出後,消息隊列也將標記爲退出狀態。

做完上述的檢測性動作後,下面將正在的開始進行消息入隊的操作,整個入隊操作都是在synchronized的修飾之下的。

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 {
    // 非頭部消息
    ...省略部分代碼
}

首先將消息標記爲已使用的狀態,再把消息的觸發時間給賦值。如果你有仔細看之前的代碼,可以知道,消息的觸發時間=系統開機以來的時間+消息延遲的時間。

注意這裏使用到了一個變量mMessages,這個變量表示當前消息隊列中,最前面即頭部的那條消息。在最開始的基礎知識部分我提到過,Message的內部是使用的鏈式結構,那麼對於消息隊列而言,只需要知道頭部是誰即可。

另外還有個布爾變量needWake,它用於決定,是否要喚醒該消息隊列。關於消息隊列的阻塞和喚醒,在後面獲取消息的時候再仔細講,這裏只分析什麼情況下,將會進行喚醒。

現在我們看到if語句,判斷了3個條件滿足一個即可:

  1. 當前消息隊列沒有頭部消息,即消息隊列爲空。
  2. 將要入隊的消息,觸發時間爲0,即立刻觸發。
  3. 將要入隊的消息觸發時間小於當前的頭部消息的觸發時間。

三選一,只要滿足一個,那麼該消息將被插入到消息隊列的頭部中,那麼之前的頭部則將作爲該條消息的next。這裏的needWake的值由mBlock覺得,即如果消息隊列阻塞中,那麼將喚醒消息隊列。

現在來看else的代碼塊。根據前面的條件可以知道,如果走到else代碼塊,那麼意味着這條入隊的消息,觸發時間是大於當前的頭部消息的。那麼我們就需要從頭部消息開始向下尋找,找到第一個觸發時間大於當前消息的。

// 不是頭部消息,那麼需要遍歷去找到觸發時間最相近的一條消息
    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;

由於我們不知道這條鏈有多長,所以使用的是一個死循環去遍歷。

首先看到第一行代碼,是用於決定是否要喚醒消息隊列的,需要同時滿足以下三個條件:

  1. 當前消息隊列已被阻塞。
  2. 當前頭部消息的target爲null。
  3. 將要入隊的消息是異步消息。

這裏你可能有疑問了,爲什麼頭部消息的target可以爲null呢?明明在enqueueMessage()這個方法的一開始就判斷了message.target是否爲null啊?這裏涉及到一個名詞,叫:同步消息屏障。這裏將不展開,你只需要知道,只要消息的target爲null,那麼它就會被認爲是一條屏障消息,即所謂的同步消息屏障。

繼續看我們的後續代碼。

邏輯很簡單,判斷下一條消息是否爲null或者觸發時間是否大於當前消息的觸發時間,如果滿足一個,那麼就將當前消息插入到這條消息的前面。

這裏又出現一個有意思的地方了:

if (needWake && p.isAsynchronous()) {
            needWake = false;
}

爲什麼在前面明明已經滿足喚醒隊列的情況下,如果發現消息隊列中某條消息是異步消息,又不喚醒消息隊列了呢?這個問題,留到後面的輪詢消息的時候我將會解密。

到這裏,我們的發送消息流程就結束了。其實,雖然對於調用者而言,調用的是sendMessage(),實際上內部叫做enqueueMessage(),整個流程叫消息入隊更貼切。

現在我們總結一下:

當調用Handler.sendMessage()的時候,實際上最終是調用到MessageQueue.enqueueMessage()。所謂的發送消息,其實只是將消息加入到了消息隊列中去等待被分發而已。

另外,我們通過分析入隊的代碼,可以知道,整個Handler機制是如何保證消息按觸發時間進行消息分發的了。因爲在消息入隊的時候,會將消息按照觸發時間的順序進行插入,這樣,在後續獲取消息的時候,只需要從消息隊列的頭部,一條接一條的獲取即可。這就解答了在本文最開頭問道的問題:

  1. Handler中的延時消息是如何實現的?
  2. 如何保證消息是按照時間順序到達?
  3. 同步消息屏障是什麼?就是message.target爲null的消息。

但是還有一個問題沒有解決,那就是到底是如何做到線程切換的呢?這個問題將在下面的,獲取消息中進行解答。

獲取消息MessageQueue.next()

首先你可能有個疑惑,在使用Handler的時候,我明明只需要發送消息就可以了啊,不用去調用MessageQueue.next()去獲取消息啊?的確,因爲這個動作是機制內部完成的。那麼這裏就有個問題,到底是誰在幫我們完成獲取消息這個動作呢?沒錯,就是Looper這個類。

還記得,如果我們在子線程中去實例化一個Handler的時候,我們需要怎麼做嗎?

Looper.prepare();
handler = new Handler();
Looper.loop();

Looper.prepare()我們已經看了,就是新建一個Looper並保存到當前線程中。

那麼Looper.loop()是幹什麼的呢?可能很多人都遇到過,在子線程中使用Handler發送消息明明沒有報錯,一切正常,但是就是收不到消息。這個時候,你去百度一下,很多人都會讓你加上這麼一句話:Looper.loop()。神奇的事情發生了,消息能正常的獲取到了。

因此我們不難判斷,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);
            ...
        } catch (Exception exception) {
            ...
            throw exception;
        } finally {
            ...
        }
        ...
        msg.recycleUnchecked();
    }
}

整個方法,我刪除了一些非功能性的代碼,只留下主要的獲取消息以及分發消息的代碼。

那麼整個邏輯就很簡單了,死循環一直調用MessageQueue.next()獲取消息,如果返回null的話表示消息隊列正在退出,那麼Looper也將退出工作,不再循環,否則將消息調用message.target.dispatchMessage()傳遞給目標Handler處理。

你可能有個疑問,似乎沒有看到延時的代碼,那麼這樣一直死循環的去讀取消息隊列,不會造成卡頓嗎?莫急莫急,馬上你就知道了。我們來看看MessageQueue.next()

由於源碼太長,因此我將一段段的貼代碼並進行講解:

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

關於mPtr這個變量,是底層的native方法中的一個指針,由於我不會C,所以我也不太清楚這個指針什麼情況下將會爲0。所以這段代碼我們眼熟一下就好,關鍵還是後面的。

int nextPollTimeoutMillis = 0;
for (;;) {
    nativePollOnce(ptr, nextPollTimeoutMillis);

    synchronized (this) {
       // 找到下一條需要分發的消息
    }
}

又見for死循環,爲什麼我這裏會省略掉尋找下一條消息的代碼呢?因爲我想提一提這個nativePollOnce()底層函數。它會根據第二個參數nextPollTimeoutMillis去進行阻塞,有以下2種情況:

  1. -1表示一直阻塞,直到被喚醒。
  2. 大於等於0表示阻塞nextPollTimeoutMillis這麼久。

我找了很久,在底層源碼中找到了下面的代碼,位於/system/core/include/utils/Condition.h

inline status_t Condition::wait(Mutex& mutex) {
    return -pthread_cond_wait(&mCond, &mutex.mMutex);
}

inline status_t Condition::waitRelative(Mutex& mutex, nsecs_t reltime) {
    struct timespec ts;
    ...省略部分代碼
    return -pthread_cond_timedwait(&mCond, &mutex.mMutex, &ts);
}

主要關注這兩個方法pthread_cond_wait()pthread_cond_timedwait(),這兩個方法是Linux系統的方法。查閱資料知道他們的作用是這樣的:

pthread_cond_wait 線程等待信號觸發,如果沒有信號觸發,無限期等待下去。
pthread_cond_timedwait 線程等待一定的時間,如果超時或有信號觸發,線程喚醒。

這裏我提供一下整個native層的調用順序。

  1. /frameworks/base/core/jni/android_os_MessageQueue.cpp:188
  2. /frameworks/base/core/jni/android_os_MessageQueue.cpp:107
  3. /frameworks/base/native/android/looper.cpp:52
  4. /frameworks/hardware/interfaces/sensorservice/libsensorndkbridge/ALooper.cpp:43
  5. /system/core/include/utils/Condition.h:117

講完了這個方法,現在你應該知道爲什麼在Looper.loop()中死循環還不用延時都不會卡頓的原因了吧?就是因爲消息隊列會進行阻塞,這樣Looper中也就一同被阻塞住了。

現在,假設消息隊列被喚醒了,我們要去看看現在的消息隊列中是否有滿足觸發條件的消息去分發還是繼續阻塞。

 		// 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());
        }
		...省略部分代碼

首先進來一個判斷,當前的頭部消息不爲null且頭部消息的target爲null。還記得我前面說的嗎?target爲null的消息就是屏障消息,這裏,我們就可以看到所謂的屏障消息到底有何作用了:如果檢測到屏障消息,那麼下一條消息將不再按照正常的順序,而是優先向下尋找最近的一條異步消息。

所謂的異步消息,從代碼看上來,其實只是優先級比普通消息高而已。因爲我們看到,如果在出現屏障的情況下,所有同步消息都會被阻擋住,只有異步消息能正常進行分發。

跳過這一段,我們看看正常情況下的消息是如何尋找並返回給Looper的。

    if (msg != null) {
        ...有消息
    } else {
        // 無消息
        // No more messages.
        nextPollTimeoutMillis = -1;
    }

    // Process the quit message now that all pending messages have been handled.
    // 翻譯:現在已處理所有掛起的消息,請處理退出消息。
    if (mQuitting) {
        dispose();
        return null;
    }

	if (pendingIdleHandlerCount <= 0) {
        // No idle handlers to run.  Loop and wait some more.
        // 翻譯:沒有空閒的處理程序可以運行。 循環並等待更多。
        mBlocked = true;
        continue;
    }

	// We only ever reach this code block during the first iteration.
	// 翻譯:我們只會在第一次迭代時到達此代碼塊。
	...省略部分代碼
    
    // Reset the idle handler count to 0 so we do not run them again.
    // 翻譯:將空閒處理程序計數重置爲0,這樣我們就不再運行它們。
    pendingIdleHandlerCount = 0;

...省略部分代碼

代碼並不多,並且官方還提供了註釋。首先是檢測是否存在頭部消息,如果頭部消息都沒有的話,那就意味着整個消息隊列目前是沒有任何消息的,此時將nextPollTimeoutMillis設置爲-1,然後走到下面的判斷,根據註釋我們可以知道將會繼續循環。

如果有消息:

    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;// 結束死循環並將消息返回給Looper
    }

判斷當前時間是否小於消息觸發時間,如果小於,則將nextPollTimeoutMillis設置爲觸發事件。需要注意的是,這裏表明了,nextPollTimeoutMillis的最大值爲Integer.MAX_VALUE

如果當前事件大於等於消息觸發時間,那麼就將該消息從隊列中移除,並將改消息返回給Looper。

到這裏,獲取消息的源碼解析完畢。

我們來總結一下:Looper.loop()使用一個死循環不斷的調用MessageQueue.next()獲取消息,而在MessageQueue.next()中,會調用底層方法去阻塞當前消息隊列,直到返回消息或者消息隊列退出。然後Looper.loop()將調用Message.target.dispatchMessage()將消息傳遞給Handler去處理。

而且我們需要注意,在Looper.loop()時,是運行在調用Looper.prepare()的這個線程的,也就意味着Message.target.dispatchMessage()也運行在同一個線程。也就是我們實例化Handler的線程。注意到了嗎?不知不覺中,就完成了線程的切換。Handler.sendMessage()可以在任何線程調用,最終都消息都會去到實例化Handler時的線程。

下一步,我們看看最後的消息是如何回調到我們Callback.handleMessage()或者Handler.handleMessage()的。

處理消息Handler.dispatchMessage()

當Looper調用Message.target.dispatchMessage()時,就將消息傳遞到了我們的Handler這裏。

/**
 * Handle system messages here.
 */
public void dispatchMessage(@NonNull Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        // 有callbacl就用callback處理消息,沒有則用當前handler處理
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        // 默認爲空實現,需要自行重寫
        handleMessage(msg);
    }
}

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

非常簡單的幾行代碼,如果消息有本身設置的callback,那麼就調用callback.run()

如果消息沒有設置callback,那麼將根據是否有爲Handler設置callback進行消息的分發。有Handler.callback則調用callback.handleMessage(),否則調用Handler本身的handleMessage()

總結

到此,我們已經基本把Handler這套機制給大體上理清楚了。現在我們來回顧最開始提到的幾個問題:

  1. Handler是如何做到線程切換的?

答:使用到了Looper,而Looper是根據線程進行保存的,並且分發消息也是由Looper完成的。

  1. Handler中的延時消息是如何實現的?

答:在消息隊列中,會在底層阻塞消息隊列獲取下一條消息,直到到達設定的時間點。

  1. 如何保證消息是按照時間順序到達?

答:在每條消息入隊的時候,會找到最近的一條觸發時間大於該消息的觸發時間的消息,然後將要入隊的消息插入到這條消息的前面。這樣,就保證整個消息隊列是按照觸發時間,從小到大進行排序的。

  1. 所謂的同步消息屏障是什麼?有什麼用?

答:當Message.target爲null的時候,就是同步消息屏障,也叫屏障消息。當獲取消息的時候,遇到屏障消息時,將不再按照正常的邏輯去向下獲取消息隊列,而是隻獲取異步消息。這樣,相當於提高了異步消息的處理優先級。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章