簡單實現Android中的訊息迴圈機制

叢逍遙發表於2017-11-30

做安卓的同學對Handler並不陌生,通過它可以方便的實現執行緒切換及定時任務,是執行緒通訊的利器。安卓應用本身就是事件驅動的訊息輪詢模型,系統中的各個元件的正常工作也離不開Handler的支援。其背後的Message、Looper、MessageQueue為這套機制提供了充足的保障。相信大家已經看過不少訊息迴圈機制的原始碼分析了,出於學習目的,這次讓我們一起實現一個簡單的訊息迴圈機制。

我們希望在javaSE上實現一個跨平臺的訊息迴圈機制,可以做到執行緒間通訊並支援傳送延時訊息。執行緒同步方面選用jdk提供的重入鎖(非必須,可選其他)配合Condition完成執行緒的休眠與喚醒。與AndroidSDK類似,通過MessageQueue物件實現訊息佇列,通過Handler物件實現訊息的傳送與處理。但Message與Looper都相應做了簡化,只實現核心部分。放一張類圖鎮鎮場子:

簡單實現Android中的訊息迴圈機制

訊息佇列組織為連結串列形式,通過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)必須是按時間排序的,以便在正確的時間被方便的取出。
  • 生產者向緩衝區投遞訊息的行為可能會打擾到正在休眠的消費者,因為每當訊息入隊時會喚醒阻塞的消費者執行緒,而此時消費者執行緒並不急於處理訊息(時間沒到)。
  • 生產者執行緒在處理訊息/休眠時,應安全的退出訊息迴圈
從以上幾個問題出發,我們如此編寫MessageQueue的next方法:

/**
 * 從訊息佇列中取出一個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,完成訊息的處理。


好了,到此為止就是這套訊息迴圈機制的全部程式碼,需要的小夥伴就麻煩自己整理下原始碼吧,就這樣。

以上。


相關文章