App下载 微信公众号

Handler.sendMessageDelayed()怎么实现延迟的?

简答题 /  作者【吾非言】/ 发布于2020-9-17/ 988次浏览
2020 9/17 8:7

首先看第一下,Handler.sendMessageDelayed()的源码,如下:

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

可见该方法最终走到sendMessageAtTime方法中,该方法有两个参数,一个是Message消息,一个是消息被处理的时间。而消息被处理的时间采用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()方法最终会调用Handler.enqueueMessage方法进入MessageQueue的enqueueMessage方法中:

public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
         MessageQueue queue = mQueue;
         if (queue == null) {
             RuntimeException e = new RuntimeException(
                     this + " sendMessageAtTime() called with no mQueue");
             Log.w("Looper", e.getMessage(), e);
             return false;
         }
         return enqueueMessage(queue, msg, uptimeMillis);
     }
 
 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 从队列中取出消息。

先分析MessageQueue.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,这样防止了多个线程对同一个队列的同时操作。

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

感谢您使用伴职平台,如有侵权,请投诉删除!

全部评价

最新
查看更多评论 加载