美文网首页程序员程序员
深入理解MessageQueue

深入理解MessageQueue

作者: lizb | 来源:发表于2019-01-07 14:40 被阅读0次

    在上一篇文章中我们分析了Handler 、Looper、 MessageQueue 、线程之间的关系,简单的说就是:一个线程绑定一个Looper,一个Looper维护一个MessageQueue队列,而一个线程可以对应多个Handler。而在Handler的消息机制中,MessageQueue可能算是最重要的,今天我们就来分析这个类。
    在分析之前,先提出两个问题:
    1.Handler.sendMessageDelayed()怎么实现延迟的?
    2.Looper.loop是一个死循环,拿不到需要处理的Message就会阻塞,那在UI线程中为什么不会导致ANR?

    现在,我们带着这两个问题进入MessageQueue的分析中。首先看第一个,Handler.sendMessageDelayed()的源码如下:

    public final boolean sendMessageDelayed(Message msg, long delayMillis)
        {
            if (delayMillis < 0) {
                delayMillis = 0;
            }
            return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
        }
    

    可以看出 : 消息被处理的时间 = 当前时间+延迟的时间

    至于这个地方为什么要用SystemClock.uptimeMillis() 而不用SystemClock. currentTimeMillis(),这里可以看两者的区别(摘自网上):

    System.currentTimeMillis() 方法产生一个标准的自1970年1月1号0时0分0秒所差的毫秒数。该时间可以通过调用setCurrentTimeMillis(long)方法来手动设置,也可以通过网络来自动获取。这个方法得到的毫秒数为“1970年1月1号0时0分0秒 到 当前手机系统的时间”的差。因此如果在执行时间间隔的值期间用户更改了手机系统的时间,那么得到的结果是不可预料的。因此它不适合用在需要时间间隔的地方,如Thread.sleep, Object.wait等,因为它的值可能会被改变。

    SystemClock.uptimeMillis()方法用来计算自开机启动到目前的毫秒数。如果系统进入了深度睡眠状态(CPU停止运行、显示器息屏、等待外部输入设备)该时钟会停止计时,但是该方法并不会受时钟刻度、时钟闲置时间亦或其它节能机制的影响。因此SystemClock.uptimeMillis()方法也成为了计算间隔的基本依据,比如Thread.sleep()、Object.wait()、System.nanoTime()以及Handler都是用SystemClock.uptimeMillis()方法。这个时钟是保证单调性,适用于计算不跨越设备的时间间隔。

    Handler.sendMessageDelayed()方法最终会调用enqueueMessage方法进入MessageQueue的enqueueMessage方法中:

    private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
            msg.target = this;
            if (mAsynchronous) {
                msg.setAsynchronous(true);
            }
            return queue.enqueueMessage(msg, uptimeMillis);
        }
    

    MessageQueue中最重要的就是两个方法:
    1.enqueueMessage向队列中插入消息
    2.next 从队列中取出消息

    先分析enqueueMessage:

    boolean enqueueMessage(Message msg, long when) {
            if (msg.target == null) {//msg.target就是发送此消息的Handler
                throw new IllegalArgumentException("Message must have a target.");
            }
            if (msg.isInUse()) {//表示此消息正在被使用
                throw new IllegalStateException(msg + " This message is already in use.");
            }
    
            synchronized (this) {
                if (mQuitting) {//表示此消息队列已经被放弃了
                    IllegalStateException e = new IllegalStateException(
                            msg.target + " sending message to a Handler on a dead thread");
                    Log.w(TAG, e.getMessage(), e);
                    msg.recycle();
                    return false;
                }
    
                msg.markInUse();
                msg.when = when;//将延迟时间封装到msg内部
                Message p = mMessages;//消息队列的第一个元素
                boolean needWake;
                if (p == null || when == 0 || when < p.when) {
                   //如果此队列中头部元素是null(空的队列,一般是第一次),或者此消息不是延时的消息,则此消息需要被立即处理,此时会将这个消息作为新的头部元素,并将此消息的next指向旧的头部元素,然后判断如果Looper获取消息的线程如果是阻塞状态则唤醒它,让它立刻去拿消息处理
              
                    msg.next = p;
                    mMessages = msg;
                    needWake = mBlocked;
                } else {
                    //如果此消息是延时的消息,则将其添加到队列中,原理就是链表的添加新元素,按照when,也就是延迟的时间来插入的,延迟的时间越长,越靠后,这样就得到一条有序的延时消息链表,取出消息的时候,延迟时间越小的,就被先获取了。插入延时消息不需要唤醒Looper线程。
      
                    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;
                        }
                    }
                    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;
        }
    

    源码中主要的地方我给了注释,可以参考参考。
    由此可以看出:
    MessageQueue中enqueueMessage方法的目的有两个:
    1.插入消息到消息队列
    2.唤醒Looper中等待的线程(如果是及时消息并且线程是阻塞状态)
    同时我们知道了MessageQueue的底层数据结构是单向链表,MessageQueue中的成员变量mMessages指向的就是该链表的头部元素。

    接下来我们再来分析一下取出消息的方法next():

    next()方法代码比较多,下面是主要部分,后面省略了一部分IdleHandler的处理逻辑,用于空闲的时候处理不紧急事件用的,有兴趣的自行分析。

    Message next() {
        
            final long ptr = mPtr;
            if (ptr == 0) {
               //从注释可以看出,只有looper被放弃的时候(调用了quit方法)才返回null,mPtr是MessageQueue的一个long型成员变量,关联的是一个在C++层的MessageQueue,阻塞操作就是通过底层的这个MessageQueue来操作的;当队列被放弃的时候其变为0。
                return null;
            }
    
            int pendingIdleHandlerCount = -1; // -1 only during first iteration
            int nextPollTimeoutMillis = 0;
            for (;;) {
                if (nextPollTimeoutMillis != 0) {
                    Binder.flushPendingCommands();
                }
    
                //阻塞方法,主要是通过native层的epoll监听文件描述符的写入事件来实现的。
               //如果nextPollTimeoutMillis=-1,一直阻塞不会超时。
               //如果nextPollTimeoutMillis=0,不会阻塞,立即返回。
               //如果nextPollTimeoutMillis>0,最长阻塞nextPollTimeoutMillis毫秒(超时),如果期间有程序唤醒会立即返回。
                nativePollOnce(ptr, nextPollTimeoutMillis);
    
                synchronized (this) {
               
                    final long now = SystemClock.uptimeMillis();
                    Message prevMsg = null;
                    Message msg = mMessages;
                    if (msg != null && msg.target == null) {
                        //msg.target == null表示此消息为消息屏障(通过postSyncBarrier方法发送来的)
                        //如果发现了一个消息屏障,会循环找出第一个异步消息(如果有异步消息的话),所有同步消息都将忽略(平常发送的一般都是同步消息)
                        do {
                            prevMsg = msg;
                            msg = msg.next;
                        } while (msg != null && !msg.isAsynchronous());
                    }
                    if (msg != null) {
                        if (now < msg.when) {
                            // 如果消息此刻还没有到时间,设置一下阻塞时间nextPollTimeoutMillis,进入下次循环的时候会调用nativePollOnce(ptr, nextPollTimeoutMillis)进行阻塞;
                            nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                        } else {
                            //正常取出消息
                            //设置mBlocked = false代表目前没有阻塞
                            mBlocked = false;
                            if (prevMsg != null) {
                                prevMsg.next = msg.next;
                            } else {
                                mMessages = msg.next;
                            }
                            msg.next = null;
                            msg.markInUse();
                            return msg;
                        }
                    } else {
                        //没有消息,会一直阻塞,直到被唤醒
                        nextPollTimeoutMillis = -1;
                    }
    
                    if (mQuitting) {
                        dispose();
                        return null;
                    }
    
                pendingIdleHandlerCount = 0;
                nextPollTimeoutMillis = 0;
            }
        }
    
    
    

    由此可以看出:
    1.当首次进入或所有消息队列已经处理完成,由于此刻队列中没有消息(mMessages为null),这时nextPollTimeoutMillis = -1 ,然后会处理一些不紧急的任务(IdleHandler),之后线程会一直阻塞,直到被主动唤醒(插入消息后根据消息类型决定是否需要唤醒)。
    2.读取列表中的消息,如果发现消息屏障,则跳过后面的同步消息。
    3.如果拿到的消息还没有到时间,则重新赋值nextPollTimeoutMillis = 延时的时间,线程会阻塞,直到时间到后自动唤醒
    4.如果消息是及时消息或延时消息的时间到了,则会返回此消息给looper处理。

    通过enqueueMessage和next两个方法的分析我们不难得出:
    消息的入列和出列是一个生产-消费者模式,Looper.loop()在一个线程中调用next()不断的取出消息,另外一个线程则通过enqueueMessage向队列中插入消息,所以在这两个方法中使用了synchronized (this) {}同步机制,其中this为MessageQueue对象,不管在哪个线程,这个对象都是同一个,因为Handler中的mQueue指向的是Looper中的mQueue,这样防止了多个线程对同一个队列的同时操作。

    现在,我们对开篇的第一个问题做个回答:
    Handler.sendMessageDelayed()怎么实现延迟的?
    前面我们分析了如果拿到的消息还没有到时间,则会重新设置超时时间并赋值给nextPollTimeoutMillis,然后调用nativePollOnce(ptr, nextPollTimeoutMillis)进行阻塞,这是一个本地方法,会调用底层C++代码,C++代码最终会通过Linux的epoll监听文件描述符的写入事件来实现延迟的。

    对于第二个问题:
    Looper.loop是一个死循环,拿不到需要处理的Message就会阻塞,那在UI线程中为什么不会导致ANR?

    首先我们来看造成ANR的原因:
    1.当前的事件没有机会得到处理(即主线程正在处理前一个事件,没有及时的完成或者looper被某种原因阻塞住了)
    2.当前的事件正在处理,但没有及时完成

    我们再来看一下APP的入口ActivityThread的main方法:

    public static void main(String[] args) {
      
            ...
    
            Looper.prepareMainLooper();
    
            ActivityThread thread = new ActivityThread();
            thread.attach(false);
    
            if (sMainThreadHandler == null) {
                sMainThreadHandler = thread.getHandler();
            }
    
            Looper.loop();
    
            throw new RuntimeException("Main thread loop unexpectedly exited");
        }
    
    

    显而易见的,如果main方法中没有looper进行死循环,那么主线程一运行完毕就会退出,会导致直接崩溃,还玩什么!

    现在我们知道了消息循环的必要性,那为什么这个死循环不会造成ANR异常呢?

    我们知道Android 的是由事件驱动的,looper.loop() 不断地接收事件、处理事件,每一个点击触摸或者说Activity的生命周期都是运行在 Looper的控制之下,如果它停止了,应用也就停止了。只能是某一个消息或者说对消息的处理阻塞了 Looper.loop(),而不是 Looper.loop() 阻塞它,这也就是我们为什么不能在UI线程中处理耗时操作的原因。
    主线程Looper从消息队列读取消息,当读完所有消息时,主线程阻塞。子线程往消息队列发送消息,唤醒主线程,主线程被唤醒只是为了读取消息,当消息读取完毕,再次睡眠。因此loop的循环并不会对CPU性能有过多的消耗。

    相关文章

      网友评论

        本文标题:深入理解MessageQueue

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