美文网首页
Handler源码学习(三)MessageQueue入队插队

Handler源码学习(三)MessageQueue入队插队

作者: 黑丫山上小旋风 | 来源:发表于2016-07-20 17:54 被阅读490次

    Handler源码学习(一)流程
    Handler源码学习(二)Message对象池
    Handler源码学习(三)MessageQueue入队插队

    1.消息入队

    消息队列与Message对象池的结构很像,也是通过对象之间通过next指向形成链表结构

    这时候加入一个msg消息,先来看如果消息队列为空的情况

    //判断消息队列为空时,会直接将这个msg赋值给mMessage,并将p赋值给msg.next,这时next当然时null
    msg.markInUse();
    msg.when = when;
    Message p = mMessages;
    boolean needWake;
    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 {
        // Inserted within the middle of the queue.  Usually we don't have to wake
        // up the event queue unless there is a barrier at the head of the queue
        // and the message is the earliest asynchronous message in the queue.
        needWake = mBlocked && p.target == null && msg.isAsynchronous();
        //首先声明了一个message对象prev,从字面意思理解是前一个message的意思
        Message prev;
        for (;;) {
          //接着进入一个死循环,将p赋值给prev,看看前面的代码可以知道,p指向的是mMessages,所以这里
          //是将prev指向了mMessage,再下一次循环的时候,prev则是指向了第二个message,依次类推
            prev = p;
          //接着将p指向p.next也就是mMessages.next,也就是消息链表中的第二个message
            p = p.next;
          //接下来判断两件事
          //1.p==null,说明没有下一个消息了,跳出循环
          //2.p!=null,并且当前需要入队的这个message的执行时间是小于队列中这个任务的执行时间的
          //也就是说这个入队的message需要比队列中的这个message先执行,也跳出循环
          //3.如果这两个条件都不满足的话,则继续跟队列中的下一个消息进行对比,直到满足条件,或者到
          //队列的末尾
            if (p == null || when < p.when) {
                break;
            }
            if (needWake && p.isAsynchronous()) {
                needWake = false;
            }
        }
      //跳出循环后做了两件事情
      // 1.将入队的这个消息的next指向循环中获取到的应该排在这个消息之后的message
        msg.next = p; // invariant: p == prev.next
      //将msg前面那个message.next指向了msg
        prev.next = msg;
      //到这里就将一个message完成了入队
      //入队的过程是线程安全的
    }
    
    MessageQueue入队.jpg MessageQueue入队(2).jpg

    2.取出消息

    这里贴的不是完整代码,而是取出message的核心逻辑代码,这里其实分了两个部分,第一个部分是消息插队,这个在第三节叙述,第二个部分是正常的消息,其实取出消息的部分比较简单,注释也比较清晰

    synchronized (this) {
        // Try to retrieve the next message.  Return if found.
        final long now = SystemClock.uptimeMillis();
        Message prevMsg = null;
      //队列的第一个message
        Message msg = mMessages;
      //正常取出消息
        if (msg != null) {
          //1.首先判断当前时间是否小于了msg的执行时间,
            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;
                if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                msg.markInUse();
                return msg;
            }
        } else {
            // No more messages.
            nextPollTimeoutMillis = -1;
        }
    

    3.消息插队

    首先需要知道的是整个android系统其实主要是依赖消息机制来处理事件,比如说点击事件等等,都是通过handler发送消息进行处理,但是这些系统消息相较于程序自己发送的消息,应该要优先执行,所以就涉及到了消息插队

    • isAsynchronous() — 如何将系统消息和程序发送的消息区分开来

    在阅读Hanlder的源码的时候,可以看到一个@hide的构造方法,传入一个布尔值,从字面意思理解是异步

    public Handler(boolean async) {
        this(null, async);
    }
    mAsynchronous = async;//赋值给mAsychronous
    //消息入队的时候,msg.setAsynchronous(true);将这个msg标记为异步
    private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
            msg.target = this;
            if (mAsynchronous) {
                msg.setAsynchronous(true);
            }
            return queue.enqueueMessage(msg, uptimeMillis);
        }
    

    为什么要标记为异步呢?是要新开一个线程来执行么?可是最后也是发送到looper所绑定的消息队列中啊,貌似这里并没有异步,接着看上一节提到的消息插队的源码,在这里看到调用了一个方法msg.isAsynchronous(),貌似这个布尔值并不是跟异步有关系,而是将这个消息做了一个标记而已

    前面的消息入队分析可以知道,虽然被标记了系统消息,但是还是按照消息队列的规则去入队,那么如何做到优先取出这个任务呢,一步步看

    Message prevMsg = null;
    Message msg = mMessages;
    //1.判断当前第一个消息不为空
    //2.重点来了,msg.target == null,如果这个消息的target为空,看过handler源码可以知道,
    //只要是通过handler发送消息,就会将这个msg的target指向发送消息的handler,否则也无法处理这个消息
    //那么为什么这里会出现target为空呢?先看if代码块里面的代码
    if (msg != null && msg.target == null) {
        // Stalled by a barrier.  Find the next asynchronous message in the queue.
        //进入一个循环,注意while中的判断条件
        do {
          
          //不断地取下一个消息来匹配判断条件
            prevMsg = msg;
            msg = msg.next;
        } while (msg != null && !msg.isAsynchronous());
      //只有当msg不为空 并且当前的这个消息是异步的,也就是说是系统消息,则跳出循环。
      //跳出循环后就走到了正常取消息的代码中,取出的正是这个系统消息,发现插队就成功了
    }
    

    上面的分析已经插队成功,但是还有疑点,到底是怎么进入到if代码块中的

    // Stalled by a barrier. Find the next asynchronous message in the queue.

    作者在这里给了一条注释,通过一个阻塞块来停止正常的队列,找到队列中的第一个系统消息

    在MessageQueue中试图搜索barrier,发现一个方法,

    public int postSyncBarrier() {
            return postSyncBarrier(SystemClock.uptimeMillis());
        }
    private int postSyncBarrier(long when) {
        // Enqueue a new sync barrier token.
        // We don't need to wake the queue because the purpose of a barrier is to stall it.
        synchronized (this) {
            final int token = mNextBarrierToken++;
            final Message msg = Message.obtain();
            msg.markInUse();
            msg.when = when;
            msg.arg1 = token;
    
            Message prev = null;
            Message p = mMessages;
          //判断将这个消息插入到队列中的哪个位置
            if (when != 0) {
                while (p != null && p.when <= when) {
                    prev = p;
                    p = p.next;
                }
            }
            if (prev != null) { // invariant: p == prev.next
                msg.next = p;
                prev.next = msg;
            } else {
                msg.next = p;
                mMessages = msg;
            }
            return token;
        }
    }
    

    加入队列的代码已经很熟悉了,可以看到在这段代码中其实入队了一个没有target的消息,而最上面那个public方法中可以看到传入的when是当前的系统时间,也就是说如果调用这个方法会在消息队列的头部插入一个没有target的message,到这里思路就比较清晰了,但是这个消息肯定不能一直在队列中,否则整个队列的正常消息就永远无法处理,所以相对应还有一个remove。注释中说明,这个方法应该跟插入的方法匹配使用。

    public void removeSyncBarrier(int token) {
        // Remove a sync barrier token from the queue.
        // If the queue is no longer stalled by a barrier then wake it.
        synchronized (this) {
            Message prev = null;
            Message p = mMessages;
            while (p != null && (p.target != null || p.arg1 != token)) {
                prev = p;
                p = p.next;
            }
            if (p == null) {
                throw new IllegalStateException("The specified message queue synchronization "
                        + " barrier token has not been posted or has already been removed.");
            }
            final boolean needWake;
            if (prev != null) {
                prev.next = p.next;
                needWake = false;
            } else {
                mMessages = p.next;
                needWake = mMessages == null || mMessages.target != null;
            }
            p.recycleUnchecked();
    
            // If the loop is quitting then it is already awake.
            // We can assume mPtr != 0 when mQuitting is false.
            if (needWake && !mQuitting) {
                nativeWake(mPtr);
            }
        }
    }
    

    当发送系统消息时 会在消息队列中插入一个target为空的message,在取出消息时如果发现了这个消息,就跳过所有的正常消息,返回最近的一个系统消息,然后将这个标记消息从队列中remove。

    整个Handler源码学习系列笔记就完结了,当然现在也没有到特别深入的程度,但了解了整套Handler的消息机制后,相信对于源码的阅读能力和编程思路上也有挺大的提高。

    相关文章

      网友评论

          本文标题:Handler源码学习(三)MessageQueue入队插队

          本文链接:https://www.haomeiwen.com/subject/nrgvjttx.html