一个 Handler 面试题引发的血案!!!

栏目: Android · 发布时间: 6年前

内容简介:一位热心群友在面试时抛了一个问题:说下 handler 机制,Looper 通过 MessageQueue 取消息,消息队列是先进先出模式,那我延迟发两个消息,第一个消息延迟2个小时,第二个消息延迟1个小时,那么第二个消息需要等3个小时才能取到吗?鉴于这个血案,我们来翻翻案,一探究竟。

一位热心群友在面试时抛了一个问题:

说下 handler 机制,Looper 通过 MessageQueue 取消息,消息队列是先进先出模式,那我延迟发两个消息,第一个消息延迟2个小时,第二个消息延迟1个小时,那么第二个消息需要等3个小时才能取到吗?

鉴于这个血案,我们来翻翻案,一探究竟。

已知

  • Main Handler 在 ActivityThread 的时候就 Looper.loop
  • 所有的消息都是通过 Looper.loop 进行分发

  • Message 消息队列对于延迟消息是如何处理的?

解题步骤氛围两步来看:

  • 分发消息 sendMessageDelayed
  • 接收消息 dispatchMessage

分发消息

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

 public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
   ....
 }
复制代码

Handler 在发送消息时都会进入这一步,从这段代码中我们捋出几个重要点:

时间戳+延迟时间
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
}
复制代码

最终会调用到 enqueueMessage ,这里给几个信息:

  • msg.target 指当前创建的 Handler
  • mAsynchronous 默认为 false
  • 最终调用 MessageQueue.enqueueMessage

来看看 MessageQueue.enqueueMessage 干了啥:

MessageQueue.class
boolean enqueueMessage(Message msg, long when) {
    ...
    synchronized (this) {
    ... 
    msg.when = when;
    Message p = mMessages;
    boolean needWake;
    //① 如果进来的消息 when 比当前头节点 p.when 还小,就想该消息插入到表头
    if (p == null || when == 0 || when < p.when) {
        msg.next = p;
        mMessages = msg;
        needWake = mBlocked;
     } else {
         ...
         Message prev;
         for (;;) {
            prev = p;
            //遍历链表
            p = p.next;
            //②
            //p==null : 只有在遍历到链表尾的时候才会为 true
            //when < p.when : 上一个消息的延迟大于当前延迟,这个地方就可以回顾面试的那个问题
            //p.when 当做第一个延迟2小时,when 当做目前进来的延迟1小时,这个时候是为 true
            if (p == null || when < p.when) {
                   break;
            }
            ...
          }
         //③
         msg.next = p;
         prev.next = msg;
     }
 }
复制代码

继续捋关键点:

  • 时间戳+延迟时间  在这个地方变成了 when  ,并且赋值给了 Message
  • 其他解释看标记处

这个地方需要重点讲解 ③ 处,这个地方要分类去讨论,我们给出两个假设和例子:

假设一: p==null 为 true

p==null 为 true 的话,也就意味着链表遍历到了链尾,并且  when < p.when 一直都为 false,也就是说进来的消息延迟都是大于当前节点的延迟,这个地方我们来举个满足条件例子:

  • 原消息链:0s -> 0s -> 1s -> 4s
  • 进来延迟消息为 10s

最后的代码就是意思就是 10s.next=null4s.next=10s ,最终链表为:

  • 0s -> 0s -> 1s -> 4s -> 10s

假设二: when < p.when 为 true

也就是说,链表还没有遍历到链尾发现进来的消息延迟小于当前节点的延迟,然后break了循环体,这个地方也来举一个满足条件的例子:

  • 原消息链:0s -> 0s -> 1s -> 4s
  • 进来延迟消息为 2s

遍历到 4s 的时候,发现 2s < 4s,break,当前 p 节点指向的是节点 4s,则最后代码的意思就是 2s.next=4s1s.next=2s ,最终链表为:

  • 0s -> 0s -> 1s -> 2s -> 4s

总结

Handler 会根据延迟消息整理链表,最终构建出一个时间从小到大的序列

接收消息

Looper.class
public static void loop() {
    final MessageQueue queue = me.mQueue;
    for (;;) {
       Message msg = queue.next(); // might block
       ...
        try {
             msg.target.dispatchMessage(msg);
        }catch()
    }
   ...
 }
复制代码

loop 会一直循环去遍历 MessageQueue 的消息,拿到 msg 消息后,会将消息 dispatchMessage 发送出去,那么,me.next() 取消息就显得尤为重要了,我们进来看看。

MessageQueue.class
Message next() {
        ...
        int nextPollTimeoutMillis = 0;
        for (;;) {
            ...
            synchronized (this) {
                // Try to retrieve the next message.  Return if found.
                //①、获取当前的时间戳
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                ...
                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;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }
            ...
            nextPollTimeoutMillis = 0;
        }
   }
复制代码

详细解释下:

②标识:

还记得 msg.when 是由什么构成的嘛? 时间戳+delay ,每次循环都会更新 now 的时间戳,也就是说,当前for循环会一直去执行,直到 now 大于 时间戳+delay 就可以去取消息了。

④标识:

因为消息的存取都是按时间从小到大排列的,每次取到的消息都是链表头部,这时候链头需要脱离整个链表,则设置 next=null。知道最后这个用完的消息去哪了嘛?还记得 obtainMessage 复用消息吗?

总结

延迟消息的发送是通过循环遍历,不停的获取当前时间戳来与 msg.when 比较,直到小于当前时间戳为止。那通过这段代码我们也是可以发现,通过 Handler.delay 去延迟多少秒是非常不精确的,因为相减会发生偏差


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

The Shallows

The Shallows

Nicholas Carr / W. W. Norton & Company / 2010-6-15 / USD 26.95

"Is Google making us stupid?" When Nicholas Carr posed that question, in a celebrated Atlantic Monthly cover story, he tapped into a well of anxiety about how the Internet is changing us. He also crys......一起来看看 《The Shallows》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

随机密码生成器
随机密码生成器

多种字符组合密码

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具