Android 关于Handler面试的坑

目录

一、问题

二、Handler机制和原理

1. Handler中包含的对象

2.运行机制

三、Message 消息队列对于延迟消息的处理

1. 先看一下发送消息

2.之后看一下消息队列

2.1重点讲解 3 处,这个地方要分类去讨论

3、总结

四、接收消息

总结

回顾问题


一、问题

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

 

当然不是,其实Message Queue内部维护了一个消息链表,对于msg有一个字段when,when的值是通过进栈时间戳+delay组成的,而这个链表就是根据when字段从小到大排列的,每次循环都判断当前的时间戳,是否大于链表顶部msg的when字段,直到大于 when就可以取出链表顶部的msg消息,进行发送了。

二、Handler机制和原理

1. Handler中包含的对象

首先是Handler,它负责Message的发送和执行处理,是Message的主要处理者

那么何为Message呢,可以理解为线程的交流信息

Message Queue,用来存放通过Handler发布的消息,按照先进先出执行(此处引出了上面的面试问题,下面会讲解)

Looper ,是每条线程里的Message Queue的管家

2.运行机制

首先在主线程创建一个Handler对象 ,并重写handleMessage方法。

然后当在子线程中需要进行更新UI的操作,我们就创建一个Message对象,并通过handler发送这条消息出去。

之后这条消息被加入到MessageQueue队列中等待被处理,通过Looper对象会一直尝试从Message Queue中取出待处理的

消息,最后分发会Handler的handlerMessage方法中,然后进行相应的处理。

三、Message 消息队列对于延迟消息的处理

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

1. 先看一下发送消息

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 在发送消息时都会进入这一步,从这段代码中我们捋出几个重要点:

 

  • delay 设置的延迟时间低于0时默认为0
  • uptimeMillis 为当前 时间戳 + 延迟时间(注意,这里后面需要用上)

2.之后看一下消息队列

 

Handler.class
    
    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;
            //1. 如果进来的消息 when 比当前头节点 p.when 还小,就把该消息插入到表头
            if (p == || when == 0 || when < p.when) {
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                ...
                Message prev;
                for (; ; ) {
                    prev = p;
                    //遍历链表 p = p.next;
                    //2. p== : 只有在遍历到链表尾的时候才会为 true
                    //when < p.when : 上一个消息的延迟大于当前延迟,这个地方就可以回顾面试的那个问题
                    //p.when 当做第一个延迟2小时,when 当做目前进来的延迟1小时,这个时候是为 true
                    if (p == || when < p.when) {
                        break;
                    }
                    ...
                }
                //3.下面讲解
                msg.next = p;
                prev.next = msg;
            }
        }

继续捋关键点:

  • uptimeMillis(时间戳+延迟时间)在这个地方变成了 when,并且赋值给了 Message

  • 如果进来的消息 when 比当前头节点 p.when(头节点上的Message的when) 还小或相等,就把该消息插入到表头

  • 如果大于p.when 将会遍历链表,只有在遍历到链表尾的时候 p== :  才会为true

2.1重点讲解 3 处,这个地方要分类去讨论

我们给出两个假设和例子:

1、假设一: p== 为 true

p==为 true 的话,也就意味着链表遍历到了链尾,

并且 when < p.when一直都为 false,

也就是说进来的消息延迟都是大于当前节点的延迟,

这个地方我们来举个满足条件例子:

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

最后的代码就是意思就是 10s.next=、4s.next=10s

最终链表为:0s -> 0s -> 1s -> 4s -> 10s

2、假设二: when < p.when 为 true

也就是说,链表还没有遍历到链尾发现进来的消息延迟小于当前节点的延迟,然后break了循环体,

这个地方也来举一个满足条件的例子:

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

遍历到 4s 的时候,发现 2s < 4s,break,

当前 p 节点指向的是节点 4s,则最后代码的意思就是 2s.next=4s、1s.next=2s,

最终链表为:0s -> 0s -> 1s -> 2s -> 4s

3、总结

  • 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) {
                // 试图获取下一条消息。如果发现返回
                //1、获取当前的时间戳
                final long now = SystemClock.uptimeMillis;
                Message prevMsg = ;
                Message msg = mMessages;
                ...
                if (msg !=) {
                    //2,如果当前时间戳小于所取延迟消息,则以他们的时间差返回
                    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 {
                        //3、延迟时间到了就可以拿到消息,直接返回了
                        // Got a message.
                        mBlocked = false;
                        if (prevMsg !=) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        //4、下面解释
                        msg.next =;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse;
                        return msg;
                    }
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }
                ...
                nextPollTimeoutMillis = 0;
            }
        }
    }

详细解释下:

2标识:

还记得 msg.when是由什么构成的嘛?时间戳+delay,

每次循环都会更新 now的时间戳,

也就是说,当前for循环会一直去执行,直到 now大于 时间戳+delay就可以去取消息了。

3标识:

因为消息的存取都是按时间从小到大排列的,每次取到的消息都是链表头部,

这时候链头需要脱离整个链表,则设置 next=。

知道最后这个用完的消息去哪了嘛?还记得 obtainMessage 复用消息吗?

总结

延迟消息的发送是通过循环遍历,不停的获取当前时间戳来与 msg.when 比较,直到小于当前时间戳为止。

那通过这段代码我们也是可以发现,通过 Handler.delay 去延迟多少秒是非常不精确的,因为相减会发生偏差

回顾问题

  1. MessageQueue 的实现不是队列,不要被名称迷惑,他是一个链表
  2. 每次发送消息都会按照 delay 从小到大进行重排
  3. 所有的 delay 消息都是并行的,不是串行的
  4. 第一个延迟2个小时,第二个延迟1小时,会优先执行第二个,再过1小时执行第一个

参考:https://www.toutiao.com/i6667297908389315083/

 

 

你可能感兴趣的:(android,进阶)