【Android Handler系列二】 MessageQueue

754 阅读6分钟

【Handler系列二】 MessageQueue

前言

上篇文章我们分析了Android消息机制中的Message,对Message不了解的请先移步到上篇文章Message;本文我们主要来讲用于存储消息的MessageQueue消息队列。

MessageQueue(消息队列):见名知义,消息队列就是用来存储消息、取出消息和移除消息的;他既不是用List来存储,也不是用Map来存储,他是用Message的内部链表结构来存储的。那么接下来我们就来研究研究,MessageQueue到底是如何做到对消息的存储、取出和移除的。

解析enqueueMessage(Message msg, long when) 将消息添加到消息队列

首先该方法就是用来将消息插入到我们的消息队列中去,废话不多说,来直接上代码

boolean enqueueMessage(Message msg, long when) {

    //首先插入到消息中的target对象(即Handler对象)不能为空
    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) {
	......
	//省略部分代码

        msg.markInUse();
        msg.when = when;
        Message p = mMessages;
        boolean needWake;
        
        //当前队列为空或者when==0(消息优先级最高)或者该消息优先级高于队列第一个消息的优先级
        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 {
            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;
                }
            }
            
            //将需要插入的消息插入到消息prev和p中间
            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;
}
  1. 代码很简洁,存储主要分为两种情况

  2. 第一种情况:当当前消息队列为空或者when=0或者when小于当前队列第一个消息的when;这里的when到底指的是什么呢?主要是用来区分消息的优先级用的,越小优先级越高。

    这种情况下直接将当前消息插入到消息队列的第一位

  3. 第二种情况即为正常情况,需要循环遍历当前队列,将该消息插入到队列中间去(详细分析见代码注释)

  4. 最后需要调用native方法来唤醒(Linux的epoll,有兴趣的自行百度)。这里不做过多讲解(其实是笔者也不是很懂,嘿嘿嘿嘿)

现在已经明白了将消息添加到队列中的逻辑了,那么接下来我们就来分析下是如何从消息队列中获取消息

解析next() 从队列取出消息

大家可以猜想一下,如果让你设计出队列中取数据,该如何来取呢?这个就很简单了,肯定是循环呗,然后return

那么我们就先来看看next()方法里的return的地方

情况一

final long ptr = mPtr;
if (ptr == 0) {
    return null;
}

当mPtr为0则返回null。那么mPtr是什么?值为0又意味着什么?

image-20210608113532731.png

从上图可以看出,在MessageQueue的构造方法中调用了nativeInit()方法返回了mPtr值;然后又在dispose()方法中将mPtr置为0了,并且调用了nativeDestroy(mPtr)方法;大概猜想就是当MessageQueue被释放以后,将mPtr置为0。因此就可以理解当mPtr==0的时候返回null了

情况二

if (mQuitting) {
    dispose();
    return null;
}

这种情况也很简单理解,就是当该消息队列退出的时候返回null,并且还调用了dispose方法,则队列直接被释放了。

情况三

int nextPollTimeoutMillis = 0;
for (;;) {
		nativePollOnce(ptr, nextPollTimeoutMillis);
    synchronized (this) {
        final long now = SystemClock.uptimeMillis();
        Message prevMsg = null;
        Message msg = mMessages;
        
        //开启同步屏障,获取异步消息
        if (msg != null && msg.target == null) {
            // Stalled by a barrier.  Find the next asynchronous message in the queue.
            do {
                prevMsg = msg;
                msg = msg.next;
            } while (msg != null && !msg.isAsynchronous());
        }
        
        if (msg != null) {
            if (now < msg.when) {
                // Next message is not ready.  Set a timeout to wake up when it is ready.
                nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
            } else {
                // Got a message.
                mBlocked = false;
                if (prevMsg != null) {
                    prevMsg.next = msg.next;
                } else {
                    mMessages = msg.next;
                }
                msg.next = null;
                msg.markInUse();
                return msg;
            }
        } else {
            
            //队列中已经没有消息了
            nextPollTimeoutMillis = -1;
        }
    }
}

大致思路:如果msg.target(即handler)== null的时候,则开启了同步屏障,则取出异步消息来进行处理,否则取出第一个同步消息来处理;当该消息的when小于当前now的时候,则返回该msg对象;否则就会计算当前时间到when还有多久并保存到nextPollTimeoutMillis中,然后调用nativePollOnce()来延时唤醒(Linux的epoll,有兴趣的自行百度),唤醒之后,继续按照循环来获取message。

这里存在两个问题:

  1. 为什么msg.target == null的时候就是异步消息呢?
  2. msg.target又是什么时候赋值的呢?

这两个问题暂时不多做处理,下次我们来写Handler的发送处理消息的时候再来进行解密

解析 removeMessages(Handler h, Runnable r, Object object) 从消息队列中移除消息

看过源码的都知道移除消息有几个方法,但是内部实现基本一致,这里就以removeMessages(Handler h, Runnable r, Object object)为例子进行分析。来来来,小二,上代码

void removeMessages(Handler h, Runnable r, Object object) {
    //handler对象为空或Runnable为空的时候直接返回,这里就不多做说明了,dddd
    if (h == null || r == null) {
        return;
    }

    synchronized (this) {
        Message p = mMessages;

        // Remove all messages at front.
        // 移除掉前边满足条件的消息,不带头结点
        while (p != null && p.target == h && p.callback == r
               && (object == null || p.obj == object)) {
            Message n = p.next;
            mMessages = n;
            p.recycleUnchecked();
            p = n;
        }

        // Remove all messages after front.
        // 移除掉后边满足条件的消息,肯定带头结点了
        while (p != null) {
            Message n = p.next;
            if (n != null) {
                if (n.target == h && n.callback == r
                    && (object == null || n.obj == object)) {
                    Message nn = n.next;
                    n.recycleUnchecked();
                    p.next = nn;
                    continue;
                }
            }
            p = n;
        }
    }
}

分析:这里为啥会出现两次循环呢?了解链表的都知道链表分为带头结点链表和不带头结点链表,很显然MessageQueue是不带头结点的链表。不带头结点的链表的循环,第一个元素需要单独处理,然后才能将后续部分当作带头结点的链表进行循环遍历。MessageQueue遍历过程中还需要删除掉结点,因此不仅第一个元素需要单独处理,而是第一组符合删除条件的结点。

第一个while循环

遍历前边满足条件的不带头结点的消息队列,不满足循环条件后,新生成消息队列p变成了带头结点的队列了

第二个while循环

就如正常的带头结点的遍历,满足条件移除,然后取下一个message进行判断,直至没有更多的消息

总结

插入同步消息

  1. msg.target不能为null
  2. 按照消息优先级when来插入到消息队列中去

从消息队列中取出消息

  1. mPtr为0到时候,返回null,代表的时候消息队列被回收了
  2. 当消息队列退出的时候也返回null
  3. 取出第一个消息后,进行now和when的判断,如果when<now,则直接返回;否则计算now到when的时间间隔,然后调用nativePollOnce()来延时唤醒Linux的epoll

从消息队列中移除消息

  1. 先将第一个不满足移除条件前边的消息按照不带头结点的链表循环方式进行循环移除
  2. 其次得到后边部分的带头结点的链表,循环满足条件移除