Handler
在Android
中的地位沒必要說了,學習安卓開發必然跳不過Handler
,講解Handler
的文章也是很是的多,這裏我對我所瞭解的Handler
這種Android
中多線程間的通訊方式的相關知識作一個總結。面試
Handler
做爲線程間通訊的方式,最常使用的地方就是子線程更新UI
。由於Android
的UI
控件不是線程安全的,若是在多線程下併發訪問可能會致使UI
控件處於不可預期的狀態。因此在子線程想要更新UI
的時候會使用handler.sendMessage(Message msg)
方法通知主線程更新。
關於Handler
的經常使用方法,除了sendMessage
系列方法,handler
還有一個post
系列方法,能夠在子線程中經過handler.post(Runnable r)
方法進行一些在主線程的操做。
sendMessage系列方法:安全
public final boolean sendMessage(Message msg)
public final boolean sendEmptyMessage(int what)
public final boolean sendEmptyMessageDelayed(int what, long delayMillis)
public final boolean sendEmptyMessageAtTime(int what, long uptimeMillis)
public final boolean sendMessageDelayed(Message msg, long delayMillis)
public final boolean sendMessageAtFrontOfQueue(Message msg)
public boolean sendMessageAtTime(Message msg, long uptimeMillis)
複製代碼
post系列方法:bash
public final boolean post(Runnable r)
public final boolean postAtTime(Runnable r, long uptimeMillis)
public final boolean postAtTime(Runnable r, Object token, long uptimeMillis)
public final boolean postDelayed(Runnable r, long delayMillis)
public final boolean postDelayed(Runnable r, Object token, long delayMillis)
public final boolean postAtFrontOfQueue(Runnable r)
複製代碼
簡單運用:網絡
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_SEND:
mTitleText.setText((String) msg.obj);
break;
}
}
};
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.button:
new Thread(new Runnable() {
@Override
public void run() {
try {
//子線程耗時操做
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
handler.post(new Runnable() {
@Override
public void run() {
mTitleText.setText("post");
}
});
}
}).start();
break;
case R.id.button2:
new Thread(new Runnable() {
@Override
public void run() {
try {
//子線程耗時操做
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//發送消息
Message message = Message.obtain();
message.what = MESSAGE_SEND;
message.obj = "sendMessage";
handler.sendMessage(message);
}
}).start();
break;
}
}
複製代碼
Handler
的基本工做流程。過程當中主要涉及瞭如下四個類:
接下來首先就從Handler
看起。多線程
使用Handler
第一步就是建立一個Handler
對象,從而首先調用的就是Handler
的構造方法。固然Handler
構造方法有不少的不一樣參數的重載,這裏只看最主要的兩個。併發
public Handler(Looper looper, Callback callback, boolean async) {
mLooper = looper;
mQueue = looper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
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 " + Thread.currentThread()
+ " that has not called Looper.prepare()");
}
//給Handler中的成員變量初始化
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
複製代碼
第一個是傳參帶有Looper
的所調用的方法,其中就是作了些初始化的操做,調用這個方法建立的Handler
的Looper
就是做爲參數傳入的Looper
。
第二個構造方法中第一個if
判斷當前Handler
類是否有內存泄漏的隱患,主要是檢驗當前類是不是匿名類、成員類、局部類是否靜態修飾等。接着經過Looper.myLooper()
方法獲取當前線程中的Looper
對象。接下來判斷若是這個Looper
爲空,說明當前Handler
初始化所在線程沒有Looper
,會拋出Exception
。這裏就決定了Handler
初始化所在線程必須有Looper
,因此在子線程中建立Handler
以前先要經過Looper.prepare()
方法建立Looper
。接着就是對Handler
中一些成員變量進行初始化,將Looper
中的消息隊列引用傳遞給mQueue
,將構造中傳入的callback
初始化等。
來看下Looper
的myLooper()
方法:app
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
複製代碼
這裏是從ThreadLocal
中獲取到一個Looper
。關於ThreadLocal
的相關知識點能夠看Android進階知識:ThreadLocal。less
Handler
建立以後,在須要進行主線程操做的時候,咱們會使用handler
的sendMessage
系列方法,或者post
系列方法。這裏一樣有不少重載,具體的方法在前文中已經列舉。這裏先看post
方法:異步
public final boolean post(Runnable r){
return sendMessageDelayed(getPostMessage(r), 0);
}
複製代碼
能夠看到無論哪一個post
方法中,都是經過getPostMessage()
方法構建一個Message
最終仍是調用對應的sendMessage
方法。async
private static Message getPostMessage(Runnable r) {
Message m = Message.obtain();
m.callback = r;
return m;
}
複製代碼
getPostMessage
方法中經過Message.obtain()
獲取一個Message
將傳入的Runnable
賦給Message
中的callback
,接着返回這個Message
。
由於post
方法的最後又都調用了對應的sendMessage
方法,因此接下來看sendMessage
方法的實現:
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);
}
複製代碼
這裏看到通過層層調用最終執行了enqueueMessage
方法。這裏要注意的是sendMessageDelayed
方法中設置的延遲時間是經過SystemClock.uptimeMillis()
+ 延遲時間來計算的。
SystemClock.uptimeMillis()
方法是獲取從開機到如今的毫秒數,與System.currentTimeMillis()
獲取從1970年1月1日到如今的毫秒數不一樣,後者會受到手機系統時間影響,而系統時間能夠手動修改。sendMessageAtTime
方法中對MessageQueue
進行是否爲null
的判斷,爲null
拋出異常,這裏的MessageQueue
就是在Handler
構造函數中Looper
中的Queue
。
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
的引用傳遞給了Message
中的target
。接着調用了MessageQueue
的enqueueMessage
方法。接下來進入MessageQueue
。
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; } 複製代碼
MessageQueue
的enqueueMessage
方法中,傳入的msg
就是要插入到消息隊列的新消息,when
是這個任務的延時時間。方法裏首先對消息裏的target
也就是Handler
進行空判斷,爲空拋出異常。接着判斷了這個消息是否被使用和消息隊列是否退出等。看到msg.when = when
;這一行將延遲時間傳遞保存到消息內部,下一行定義了一個臨時指針p
用來指向mMessage
,這個mMessage
就是消息隊列的首結點,接下來的這個if-else
作的就是將新消息根據他的when
大小,將他按順序加入到隊列中合適位置上。這裏能夠看出這個消息隊列其實是個鏈表,每一個Message
是一個結點,結點中有一個next
指針存放下一個結點位置。這裏的新消息的添加,就是向這個鏈表中插入一個節點。
先看if
中判斷,p==null
即首結點爲null
,when=0
及延時時間爲0,當即執行,when<p.when
即新節點的延時時間小於當前鏈表首結點的延時時間,這三種狀況下直接將新消息節點插到鏈表頭部,即msg.next=p;mMessages=msg
這兩行的操做,而後喚醒消息線程處理新消息。else
就要將新節點根據when
的大小插入到鏈表中合適位置,這裏又定義了一個臨時指針prev
,指向p
指向的前一個節點,看到for (;;)
循環中,將p
指針不斷向後移,直到p
等於null
即鏈表結尾或者新結點的when<p.when
的時候,即這個鏈表是按照節點when
的從小到大的順序排列插入的。此時break
出循環,將新節點插入到此處,即msg.next=p;prev.next=msg
。到此消息發送加入消息隊列的過程節結束了。
有往消息隊列里加消息,就有從消息隊列取消息。誰來取呢?就是Looper
,以前看到在Handler
的構造方法裏,經過Looper.myLooper()
方法獲取到當前線程(handler
建立所在線程)的Looper
對象。並且還知道了建立Handler
的線程必須存在一個Looper
對象不然會拋出異常。這也是咱們不能在子線程裏直接建立使用Handler
的緣由。那麼爲何主線程能夠直接建立Handler
呢?是由於主線程中有Looper
。那麼主線程的Looper
又是哪來的呢?這須要看到ActivityThread
類裏的代碼。
public static void main(String[] args) {
......
Looper.prepareMainLooper();
......
ActivityThread thread = new ActivityThread();
thread.attach(false, startSeq);
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}
if (false) {
Looper.myLooper().setMessageLogging(new
LogPrinter(Log.DEBUG, "ActivityThread"));
}
// End of event ActivityThreadMain.
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
Looper.loop();
......
}
複製代碼
ActivityThread
中main
方法在app
啓動時調用,這裏省去了一些無關代碼,只看與主線程Looper
相關的。能夠看到在main
方法裏調用了Looper.prepareMainLooper()
方法,以後獲取了一個主線程的Handler
,接着調用了Looper.loop()
方法。一個方法一個方法來看,先是prepareMainLooper()
方法:
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));
}
public static void prepareMainLooper() {
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
sMainLooper = myLooper();
}
}
複製代碼
prepareMainLooper()
方法中調了prepare(false)
方法,這裏quitAllowed
傳的是false
,這個標記會傳遞到MessageQueue
中,這裏說明主線程的消息隊列是不容許退出的。prepare()
方法裏初始化了new
了一個Looper
對象,並將它添加到當前線程的ThreadLocal
中。接着到Looper
的構造函數中看看:
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
複製代碼
Looper
的構造函數很簡單,建立了MessageQueue
消息隊列保存了一個當前線程的對象。從這裏看出來建立一個Handler
須要傳入建立線程的Looper
,而建立Looper
又對應建立了一個MessageQueue
。下面回到main
方法中看thread.getHandler()
這個獲取主線程Handler
方法:
final Handler getHandler() {
return mH;
}
複製代碼
這裏直接返回mH
這個Handler
,那麼這個Handler
是在哪裏建立的呢?
final H mH = new H();
複製代碼
跟蹤下去發現這個mH
是ActivityThread
類的成員變量,而且直接初始化。因此這個Handler
就是在main
方法中建立ActivityThread
對象時就初始化了。最後調用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);
dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
} finally {
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
......
}
}
複製代碼
這裏一樣省略了部分代碼,來看主要的流程,首先仍是獲取當前線程Looper
對空作了校驗。而後從Looper
中獲取到MessageQueue
,接着進入for
循環,調用queue.next()
從消息隊列中取出消息,根據註釋,這個方法有可能會阻塞,若是返回的msg爲null
,說明消息隊列正在退出。接着在try
代碼塊中調用了msg.target.dispatchMessage(msg)
方法,這個msg.target
就是在前面enqueueMessage
方法中設置的發送消息的Handler
,因此這裏調用了Handler的dispatchMessage(msg)
方法。
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
複製代碼
dispatchMessage
方法中對callback
進行了判斷,這裏有兩個callback,msg.callback
是對應Handler
中的post
方法,將傳入的Runnable
存入Message
的callback
中,若是調用post
方法msg.callback
不爲空調用handleCallback
方法,最終會執行Runnable
的run
方法,開始執行post
時傳進來的任務。
private static void handleCallback(Message message) {
message.callback.run();
}
複製代碼
第二個mCallback
,對應的是建立Handler
時的傳參,若是不爲空會執行mCallback.handleMessage
方法。若是初始化時沒傳mCallback
,就會執行handleMessage(msg)
方法:
/**
* Subclasses must implement this to receive messages.
*/
public void handleMessage(Message msg) {
}
複製代碼
這個方法就是咱們本身須要實現的處理消息的方法,也是咱們最經常使用重寫的方法。至此UI
主線程中建立Handler
,Looper
,而且Looper
開啓輪詢到調用了Handler
的dispatchMessage
處理消息的過程就結束了。
回到上面說的,這是主線程中Handler
、Looper
的初始化,那麼要在子線程使用Handler
該怎麼作呢?
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
Handler handler = new Handler(Looper.myLooper()) {
@Override
public void handleMessage(Message msg) {
//處理消息
}
};
Looper.loop();
}
}).start();
複製代碼
其實和主線程同樣,由於子線程中沒有Looper
因此須要咱們本身建立Looper
而且調用Looper.loop()
方法開始輪詢。這裏的Looper.prepare()
方法和prepareMainLooper()
方法同樣最終會調用prepare(boolean quitAllowed)
方法,這時傳入的quitAllowed爲true
,表示消息隊列能夠退出。
至此Handler
機制相關類Handler
、Looper
、MessageQueue
的主要方法源碼都看完了,他們之間的工做流程相互關係也都清楚了。
其實還剩一個Message
消息類,Message
類中主要看一個obtain()
方法,Message
除了能夠經過new
來建立,還能夠經過obtain()
方法來得到,而且obtain()
方法是從全局池中獲取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();
}
複製代碼
這裏看到只要sPool
不等於null
,就從sPool
頭上去一個消息節點返回。因此使用obtain
方法,相對直接new
一個Message
能減小內存分配。
A1:主要涉及到Handler
、Looper
、MessageQueue
、Message
這四個類。
Handler:發送消息到消息隊列。
Looper:從消息隊列中取出消息,交給Handler的dispatchMessage方法處理。
MessageQueue:消息隊列存儲管理消息插入取出。
Message:消息類,攜帶着消息數據。
A2:經過以前的源碼閱讀知道,是有順序的,是根據Message.when
這個相對時間排列的。
A3:一樣從源碼中能夠知道,子線程中不能直接建立Handler
,Handler
建立須要指定一個Looper
,子線程中沒有Looper
,須要先建立Looper
,調用Looper.loop
方法。
A4:不是,內部實現實際上是一個單鏈表。
A5:quit
方法會清空消息隊列中的全部消息,quitSafely
方法只會清除全部延遲消息,非延遲消息仍是分發出去交給Handler
處理。具體仍是看源碼:
public void quit() {
mQueue.quit(false);
}
public void quitSafely() {
mQueue.quit(true);
}
複製代碼
這裏其實是調用了MessageQueue
的quit
方法:
void quit(boolean safe) {
if (!mQuitAllowed) {
throw new IllegalStateException("Main thread not allowed to quit.");
}
synchronized (this) {
if (mQuitting) {
return;
}
mQuitting = true;
if (safe) {
removeAllFutureMessagesLocked();
} else {
removeAllMessagesLocked();
}
// We can assume mPtr != 0 because mQuitting was previously false.
nativeWake(mPtr);
}
}
複製代碼
quit
方法傳入的是false
調用的是removeAllMessagesLocked()
方法,quitSafely
傳入的是true
調用的是removeAllFutureMessagesLocked
方法。
private void removeAllMessagesLocked() {
Message p = mMessages;
while (p != null) {
Message n = p.next;
p.recycleUnchecked();
p = n;
}
mMessages = null;
}
複製代碼
removeAllMessagesLocked
方法中直接將全部消息清空。
private void removeAllFutureMessagesLocked() {
final long now = SystemClock.uptimeMillis();
Message p = mMessages;
if (p != null) {
if (p.when > now) {
removeAllMessagesLocked();
} else {
Message n;
for (;;) {
n = p.next;
if (n == null) {
return;
}
if (n.when > now) {
break;
}
p = n;
}
p.next = null;
do {
p = n;
n = p.next;
p.recycleUnchecked();
} while (n != null);
}
}
}
複製代碼
removeAllFutureMessagesLocked
方法中先作判斷若是首節點when
大於當前時間說明全是延遲消息,就一樣調用removeAllMessagesLocked
處理所有清空,不然循環找到隊列中when
大於now
也就是大於當前時間的節點位置,將該節點消息同其後的全部消息清空。
A6:這個涉及到Linux
的Epoll
機制。簡單來講就是Android
應用程序的主線程在進入消息循環過程前,會在內部建立一個Linux
管道(Pipe
),這個管道的做用是使得Android
應用程序主線程在消息隊列爲空時能夠進入空閒等待狀態,而且使得當應用程序的消息隊列有消息須要處理時喚醒應用程序的主線程。
具體解釋:Android中爲何主線程不會由於Looper.loop()裏的死循環卡死?
Handler
致使的內存泄露,是平時寫代碼不注意很是容易出現的問題,並且內存泄露多了對應用性能影響較大,因此單獨研究下。
通常咱們使用Handler
更新UI
都是這樣的:
public class HandlerActivity extends Activity {
private TextView mTextView;
private final int MESSAGE_SEND = 0x01;
private MyHandler handler = new MyHandler();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler);
mTextView = findViewById(R.id.textView);
Message obtain = Message.obtain();
obtain.what = MESSAGE_SEND;
obtain.obj = "文字";
handler.sendMessageDelayed(obtain, 1000 * 60 * 10);
}
class MyHandler extends Handler {
@Override
public void handleMessage(Message msg) {
if (msg.what == MESSAGE_SEND) {
mTextView.setText((String) msg.obj);
}
}
}
}
複製代碼
這裏看到進入Activity
就發送了一個延遲消息,現實開發中多是網絡有延遲又或者進入一個界面後馬上離開這時數據還沒加載好,只要是耗時任務尚未完成,當前的Activity
又須要銷燬,這時候由於此時MyHander
,它隱式持有外部類的引用,當Activity
銷燬時,此時異步耗時任務尚未結束,仍然持有Activity
的引用,使得Activity
沒法回收,形成內存泄漏。 經過集成LeakCanary
能夠檢測到內存泄漏,以下圖:
AndroidStudio Profiler
能夠查看到內存泄露:
經過屢次打開關閉
HandlerActivity
,而後觀察內存狀況,能夠發現即便在我手動
GC
屢次後,仍然存在多個實例沒有被回收的現象。
內存泄漏解決方法:
Handler
定義爲靜態,靜態內部類不會持有外部類的引用。Handler
中沒法訪問外部類的成員,須要用一個外部類的弱引用來訪問外部成員,又由於是弱引用,在GC
時能夠將其回收,不會形成內存泄露。Activity
的onDestory
方法中調用removeCallbacksAndMessages
方法清除消息隊列。解決內存泄漏:
public class WeakHandlerActivity extends Activity {
private TextView mTextView;
private static final int MESSAGE_SEND = 1;
private MyHandler handler = new MyHandler(this);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler);
mTextView = findViewById(R.id.textView);
Message obtain = Message.obtain();
obtain.what = MESSAGE_SEND;
obtain.obj = "文字";
handler.sendMessageDelayed(obtain, 1000 * 60 * 10);
}
//靜態內部類
static class MyHandler extends Handler {
private final WeakReference<WeakHandlerActivity> mActivty;
public MyHandler(WeakHandlerActivity activity) {
//初始化Activity的弱引用
mActivty = new WeakReference<WeakHandlerActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
WeakHandlerActivity activity = mActivty.get();
if (activity != null) {
if (msg.what == MESSAGE_SEND) {
activity.mTextView.setText((String) msg.obj);
}
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
//清除消息隊列
handler.removeCallbacksAndMessages(null);
}
}
複製代碼
經過這三個方法結合使用,就能夠解決Handler
致使的內存泄漏的問題。此次再經過Profiler
來查看內存狀況:
WeakHandlerActivity
實例。
可是在
GC
事後內存中的
WeakHandlerActivity
已經被所有回收,不會繼續佔用內存,形成泄漏。
Handler是Android提供的一種線程間通訊方式。由於Android中UI控件不是線程安全的,多線程併發訪問會出現同步問題,因此若是子線程想更新UI一般經過Handler來完成線程間通訊。
Handler的工做流程主要是由Handler發送消息,將消息添加到消息隊列MessageQueue中,再經過輪詢器Looper從消息隊列中取出消息,交給Handler去分發處理消息對應任務。
Handler使用時容易發生內存泄露,記得經過靜態內部類+弱引用的方式使用Handler,而且在Activity的onDestory方法裏記得清除消息隊列。