简单实现Android中的消息循环机制

做安卓的同学对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)必须是按时间排序的,以便在正确的时间被方便的取出。
  • 生产者向缓冲区投递消息的行为可能会打扰到正在休眠的消费者,因为每当消息入队时会唤醒阻塞的消费者线程,而此时消费者线程并不急于处理消息(时间没到)。
  • 生产者线程在处理消息/休眠时,应安全的退出消息循环
从以上几个问题出发,我们如此编写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,完成消息的处理。


好了,到此为止就是这套消息循环机制的全部代码,需要的小伙伴就麻烦自己整理下源码吧,就这样。

以上。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值