做安卓的同學對Handler並不陌生,通過它可以方便的實現執行緒切換及定時任務,是執行緒通訊的利器。安卓應用本身就是事件驅動的訊息輪詢模型,系統中的各個元件的正常工作也離不開Handler的支援。其背後的Message、Looper、MessageQueue為這套機制提供了充足的保障。相信大家已經看過不少訊息迴圈機制的原始碼分析了,出於學習目的,這次讓我們一起實現一個簡單的訊息迴圈機制。
我們希望在javaSE上實現一個跨平臺的訊息迴圈機制,可以做到執行緒間通訊並支援傳送延時訊息。執行緒同步方面選用jdk提供的重入鎖(非必須,可選其他)配合Condition完成執行緒的休眠與喚醒。與AndroidSDK類似,通過MessageQueue物件實現訊息佇列,通過Handler物件實現訊息的傳送與處理。但Message與Looper都相應做了簡化,只實現核心部分。放一張類圖鎮鎮場子:
訊息佇列組織為連結串列形式,通過MessageQueue物件維護。Handler建立一條訊息,將其插入到MessageQueue維護的連結串列中。因為有延時訊息的存在,佇列中的訊息並不一定要立即處理。MessageQueue的next方法會檢查訊息佇列第一條訊息的處理時間,若符合要求才將Message出隊,Looper會將出隊的Message派發給這個Message物件內的Handler(也就是建立這個Message的Handler)來處理。由於MessageQueue的next方法一次只會處理一條訊息,所以Looper的loop方法會無限呼叫next,迴圈不斷處理訊息,直至手動退出。
所以Message物件中有指向下一個節點的引用。when欄位記錄了訊息希望被處理的時間。Message中的其他欄位定義如下:
public class Message {
public Object obj; //你懂得
public int what; //你懂得
Handler target; //處理此Message的Handler(也是生成此Message的Handler)
Runnable callback;//訊息處理回撥
long when; //處理時間
Message next; //next節點
}複製程式碼
定義好Message物件後,就可以著手實現訊息佇列了。學過作業系統的同學都知道,這套訊息迴圈機制其實就是一個生產者消費者模型,也可以認為是它的一個變種。消費者(Looper)不斷消費Message,直到緩衝區中沒有訊息被阻塞。生產者(Handler)不斷向緩衝區生產Message,沒有容量限制。綜上所述,雖然我們並不需要處理生產者執行緒的喚醒問題,但我們仍需考慮以下幾點:
- 緩衝區(MessageQueue)中的資料(Message)必須是按時間排序的,以便在正確的時間被方便的取出。
- 生產者向緩衝區投遞訊息的行為可能會打擾到正在休眠的消費者,因為每當訊息入隊時會喚醒阻塞的消費者執行緒,而此時消費者執行緒並不急於處理訊息(時間沒到)。
- 生產者執行緒在處理訊息/休眠時,應安全的退出訊息迴圈
/**
* 從訊息佇列中取出一個Message 可能會引起執行緒的阻塞
* @return 需要處理的訊息
*/
public Message next() {
//需要休眠的時間 0代表緩衝區空時無限休眠 大於零代表實際的休眠時間 小於零代表不休眠
long waitTimeMillis = 0;
while (true) {
try {
lock.lockInterruptibly();
//如果沒有需要馬上處理的訊息,此方法將在這行程式碼處阻塞
waitMessage(waitTimeMillis);
//quiting為布林值變數作為訊息迴圈退出的標誌
if (quitting) return null;
long now = System.currentTimeMillis();
Message msg = messages;
//如果緩衝區內有資料,則以隊首元素中的時間欄位為依據
//要麼取出訊息並返回,要麼計算等待時間重新休眠
if (msg != null) {
if (now < msg.when) {
waitTimeMillis = msg.when - now;
} else {
//隊首元素出隊
messages = messages.next;
msg.next = null;
return msg;
}
} else {
//緩衝區中沒資料,但執行緒被喚醒,說明訊息迴圈需要退出,將等待時間置為-1以便退出迴圈
waitTimeMillis = -1;
}
} catch (InterruptedException e) {
return null;
} finally {
lock.unlock();
}
}
}複製程式碼
多說一句,上文提到的緩衝區、訊息佇列、MessageQueue均指代這個由Message物件構成的連結串列,而消費者、訊息迴圈指代MessageQueue所在的執行緒。上文的程式碼中,lock物件為重入鎖,定義如下:
private ReentrantLock lock = new ReentrantLock();複製程式碼
messages為連結串列表頭引用,初始情況下為null
private Message messages;
複製程式碼
我們知道,每次呼叫next方法都會返回一個Message物件,如果訊息佇列中沒有合適的物件,此方法將阻塞,當訊息迴圈退出時,next方法將直接返回null,MessageQueue的使用者(Looper)迴圈不斷的通過next方法取出一條條訊息,根據Message為空與否,決定訊息迴圈的終止與執行。雖然next方法一次只返回一條訊息,但其主體是一個迴圈。因為我們需要處理延時訊息,當一條訊息入隊時,可能正在阻塞著的next方法將被迫排程起來繼續執行,但因為此時訊息的處理時間還沒到,while迴圈可以幫助我們在下一輪迴圈中繼續休眠。也就是說,waitMessage方法返回後,雖然會保證緩衝區非空,但不能保證隊首的Message可被立即處理,所以我們可以看到這段程式碼:
if (now < msg.when) {
waitTimeMillis = msg.when - now;
}複製程式碼
在隊首Message不能立即被處理的情況下,重新記錄休眠時間,經過while的下一輪迴圈,被記錄的休眠時間將被waitMessage方法處理:
public void waitMessage(long waitTimeMillis) throws InterruptedException {
if (waitTimeMillis < 0) return;
if (waitTimeMillis == 0) {
//緩衝區空則無限休眠,直到新的訊息到來喚醒此執行緒
while (messages == null) notEmpty.await();
} else {
//休眠指定時間
notEmpty.await(waitTimeMillis, TimeUnit.MILLISECONDS);
}
}複製程式碼
else分支中執行緒在notEmpty上等待waitTimeMillis毫秒。程式碼中的notEmpty為Condition物件,用於阻塞消費者執行緒,定義如下:
private Condition notEmpty = lock.newCondition();複製程式碼
回到waitMessage方法,當waitTimeMillis為-1時,函式直接返回,無需等待。這是因為-1代表著執行緒準備退出,此時直接返回意味著不再阻塞當前執行緒,程式碼繼續向下執行,遇到if (quitting) return null;之後next方法便返回了。
這樣,quit方法的負責修改quitting欄位的值,同時喚醒執行緒以便完成退出。
public void quit() {
lock.lock();
quitting = true;
notEmpty.signal();
lock.unlock();
}複製程式碼
剩下的事情就很明確了。enqueueMessage方法用於入隊訊息,定義如下:
public boolean enqueueMessage(Message message) {
try {
lock.lockInterruptibly();
if (quitting) return false;
//將message插入合適的位置
insertMessage(message);
//喚醒消費者執行緒
notEmpty.signal();
return true;
} catch (InterruptedException e) {
} finally {
lock.unlock();
}
return false;
}複製程式碼
程式碼很簡單,先將引數Message插入訊息佇列,然後喚醒消費者執行緒。我們直接來看一下insertMessage方法:
private void insertMessage(Message msg) {
Message now = messages;
if (messages == null || msg.when < now.when) {
msg.next = messages;
messages = msg;
return;
}
Message pre = now;
now = now.next;
while (now != null && now.when < msg.when) {
pre = now;
now = now.next;
}
msg.next = now;
pre.next = msg;
}複製程式碼
這裡首先處理的是緩衝區空或新訊息被插入到隊首的情況,頭插法返回之。其他情況下,找到第一個比msg晚的Message物件,將其插入到這個物件之前。這樣就可以保證入隊的訊息在緩衝區中按處理時間升序排列了。
以上就是MessageQueue的全部程式碼。現在我們假設有三條訊息在同一時間點被依次傳送,第一個訊息需要延時5s,第二個需要立刻處理,第三個需要延時2.5秒。初始狀態下緩衝區為空,消費者執行緒阻塞。第一個訊息的入隊使得消費者執行緒被喚醒,在next方法中檢查隊首元素發現需要延時5s處理,於是將waitTimeMillis置為5000,在下一輪while迴圈中呼叫notEmpty.await(waitTimeMillis, TimeUnit.MILLISECONDS);進行休眠。第二個訊息的到來又導致了消費者執行緒被喚醒,此時第二個訊息因為需要立即執行被插入緩衝區隊首。next方法取得隊首訊息發現需要立即處理,便將此訊息返回給Looper處理。Looper處理完後繼續呼叫next方法獲取訊息,由於此時緩衝區非空,無需阻塞,檢視隊首訊息(延時5s的那條訊息)發現時間未到,計算剩餘時間並休眠自己。隨著第三條訊息的到來,消費者執行緒又被喚醒,依然是檢查時間並休眠自己,注意,此時訊息佇列中存在兩條訊息,依次為延時2.5s訊息、延時5s訊息,這次的休眠時間也被重置為約2.5s。等時間一到,取出隊首元素返回給Looper,後面的動作與處理完無延時訊息後的動作別無二致了。當然上面的描述只是可能會出現的一種情況,具體的入隊與喚醒順序取決於作業系統對執行緒的排程,相信大家自己也能捋出來了。
有了MessageQueue物件,其他角色的工作就輕鬆許多。上文中多次提到Looper,他長這個樣子:
public class Looper {
private static ThreadLocal<Looper> localLooper = ThreadLocal.withInitial(Looper::new);
private MessageQueue queue;
private Looper() {
queue = new MessageQueue();
}
public static Looper myLooper() {
return localLooper.get();
}
public static void loop() {
Looper me = myLooper();
while (true) {
Message msg = me.queue.next();
if (msg == null) return;
msg.target.dispatchMessage(msg);
}
}
MessageQueue getMessageQueue() {
return queue;
}
public void quit() {
queue.quit();
}
}複製程式碼
為了簡化編寫,去掉了安卓Looper中的prepare方法,直接在宣告時初始化threadLocal。loop方法也非常的簡單粗暴,不斷呼叫MessageQueue的next方法,獲取訊息並分發之,在分發的時候,呼叫的是msg.target.dispatchMessage(msg)方法。這裡的target物件是一個Handler物件:
public class Handler {
private Looper looper;
private Callback callback;
public Handler(Looper looper) {
this(looper, null);
}
public Handler() {
this(Looper.myLooper(), null);
}
public Handler(Looper looper, Callback callback) {
this.looper = looper;
this.callback = callback;
}
protected void handleMessage(Message message) {
}
void dispatchMessage(Message message) {
if (message.callback != null) {
message.callback.run();
return;
} else {
if (callback != null) {
if (callback.handleMessage(message)) {
return;
}
}
handleMessage(message);
}
}
public void sendMessage(Message message) {
if (looper == null) return;
MessageQueue queue = looper.getMessageQueue();
message.target = this;
queue.enqueueMessage(message);
}
public void postDelay(Runnable runnable, long delay) {
Message message = new Message();
message.when = System.currentTimeMillis() + delay;
message.callback = runnable;
sendMessage(message);
}
public void sendMessageDelay(Message message, long delay) {
message.when = System.currentTimeMillis() + delay;
sendMessage(message);
}
public void post(Runnable runnable) {
postDelay(runnable, 0);
}
public interface Callback {
boolean handleMessage(Message message);
}
}
複製程式碼
dispatchMessage方法的實現與官方的思路一致,分發順序也儘量復刻原版程式碼。Handler會通過建構函式獲取Looper,或者通過引數傳入,或是直接通過Looper的靜態方法獲取。有了looper物件,在傳送訊息的時候我們就可以向looper內的MessageQueue插入訊息了。摘出兩段有代表性的傳送訊息的程式碼:
public void sendMessageDelay(Message message, long delay) {
message.when = System.currentTimeMillis() + delay;
sendMessage(message);
}
複製程式碼
public void sendMessage(Message message) {
if (looper == null) return;
MessageQueue queue = looper.getMessageQueue();
message.target = this;
queue.enqueueMessage(message);
}複製程式碼
在Handler傳送訊息之前,將自己繫結到message物件的target欄位上,這樣looper就可以將取出的message物件重新派發回建立它的handler,完成訊息的處理。
好了,到此為止就是這套訊息迴圈機制的全部程式碼,需要的小夥伴就麻煩自己整理下原始碼吧,就這樣。
以上。