簡單實現Android中的消息循環機制

作安卓的同窗對Handler並不陌生,經過它能夠方便的實現線程切換及定時任務,是線程通訊的利器。安卓應用自己就是事件驅動的消息輪詢模型,系統中的各個組件的正常工做也離不開Handler的支持。其背後的Message、Looper、MessageQueue爲這套機制提供了充足的保障。相信你們已經看過很多消息循環機制的源碼分析了,出於學習目的,此次讓咱們一塊兒實現一個簡單的消息循環機制。
java

咱們但願在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,循環不斷處理消息,直至手動退出。bash

因此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,沒有容量限制。綜上所述,雖然咱們並不須要處理生產者線程的喚醒問題,但咱們仍需考慮如下幾點:oop

  • 緩衝區(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爲鏈表表頭引用,初始狀況下爲nullpost

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方法處理:ui

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對象,用於阻塞消費者線程,定義以下:this

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,完成消息的處理。


好了,到此爲止就是這套消息循環機制的所有代碼,須要的小夥伴就麻煩本身整理下源碼吧,就這樣。

以上。

相關文章
相關標籤/搜索