sendMessageDelayed延时发送详解

最近面试问到了这个问题,感觉自己答得不是很好,特此写一下总结,如果想快速了解原理直接拖到总结就行。

1.下载源码

直接进入handle源码,发现其内部就一行throw new RuntimeException(“Stub!”),但是实际运行中并没有抛出该错误,该方法也并没有语法报错。因此可能是系统设计者故意隐藏此部分的实现源码。

使用的Android Studio或者其他IDE看jar包的源码的时候,编译工具只不让你看方法的实现,原因有下:
1.Android SDK自带的Source源码包很小,并没有包括所有的Android Framework的源码,仅仅提供给应用开发参考用,一些比较少用的系统类的源码并没有给出,所以有时候你会看到throw new RuntimeException(“Stub!”)。
2.出于安全或者某些原因,这些API不能暴露给应用层的开发者,当然,这些API在ROM中是实际存在的,有些开发者发现了一些可以修改系统行为的隐藏API,在应用层通过反射的方式强行调用这些API执行系统功能,这种手段也是一种HACK。

我们直接下载就好了。
在这里插入图片描述

2.sendMessageDelayed (Message msg, long delayMillis)

我们知道要发送延时消息,用的是 boolean sendMessageDelayed (Message msg, long delayMillis) 这个接口,API文档的描述其实已经给了答案:
在这里插入图片描述
翻译一下就是
调用这个方法时会将这个message根据delayMillis插入到MessageQueue的合适地方。
sendMessageAtTime最终会调用到queue.enqueueMessage。函数如下
在这里插入图片描述

3.queue.enqueueMessage

我们再来看一下queue.enqueueMessage
有点长,首先是一些属性的非空判断,比如我们需要通过target值来判断是哪个handler发过来的消息的。

PS:(handler的同步屏障就是一个target为空的msg,用来优先执行异步方法的)

同步屏障有一个很重要的使用场所就是接受垂直同步Vsync信号,用来刷新页面view的。因为为了保证view的流畅度,所以每次刷新信号到来的时候,要把其他的任务先放一放,优先刷新页面。

 boolean enqueueMessage(Message msg, long when) {
        if (msg.target == null) {
            throw new IllegalArgumentException("Message must have a target.");
        }

        synchronized (this) {
            if (msg.isInUse()) {
                throw new IllegalStateException(msg + " This message is already in use.");
            }

            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;
            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;
                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;
    }

when参数的含义是:分发当前Message的具体时间
根据when然后把消息加入到MessageQueue的消息队列里面,分2种情况处理

1.队列为空,直接插入并返回
2.队列非空,加入队列,接下来主要就是将这个msg根据实际执行时间进行排序插入到queue里面(看里面的for循环)。
好了,现在queue也构建完成了,假设我现在第一条消息就是要延迟10秒,怎么办呢。

如果我现在是looper,我要遍历这个messageQueue,那肯定要调用next方法。

next()方法比较长,我只贴关于延时消息的核心部分。

  for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }

            nativePollOnce(ptr, nextPollTimeoutMillis);

            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 && msg.target == null) {
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                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);
                    }
                   // ……………

可以看到这里也是一个for循环遍历队列**,核心变量就是nextPollTimeoutMillis**。可以看到,计算出nextPollTimeoutMillis后,此时调用,nativePollOnce(long ptr, int timeoutMillis),入参nextPollTimeoutMillis即为需要延迟的时间,等待延迟时间后在触发获取Message,其中是具体是调用了epoll_wait这个方法。

4.epoll_wait

这是linux的一个I/O多路复用机制,

Linux里的I/O多路复用机制:举个例子就是我们钓鱼的时候,为了保证可以最短的时间钓到最多的鱼,我们同一时间摆放多个鱼竿,同时钓鱼。然后哪个鱼竿有鱼儿咬钩了,我们就把哪个鱼竿上面的鱼钓起来。这里就是把这些全部message放到这个机制里面,那个time到了,就执行那个message。
我们来看看epoll_wait的声明。

#include <sys / epoll.h>
 
int epoll_wait(int epfd,struct epoll_event * events, int maxevents,int timeout);

epoll_wait()系统调用等待文件描述符epfd引用的epoll实例上的事件。事件所指向的存储区域将包含可供调用者使用的事件。
epoll_wait()最多返回最大事件。
maxevents参数必须大于零。
timeout参数指定epoll_wait()将阻止的最小毫秒数。 (此间隔将四舍五入为系统时钟的粒度,并且内核调度延迟意味着阻塞间隔可能会少量溢出。)
指定超时值为-1会导致epoll_wait()无限期阻塞,而指定的超时时间等于零导致epoll_wait()立即返回,即使没有可用事件。

当插入一条消息后,会唤醒epoll_wait方法,从native层回到framework的next方法
我们最初在sendMessageDelayed里面设置的delayMillis
当delay的时间到了后,就和正常的消息处理流程完全一样了。
那如果我在这段时间又插入了一个新的message怎么办,所以handler每次插入message都会唤醒线程,重新计算插入后,再走一次这个休眠流程。

5.其他问题

1.如果队列中有延时消息和普通消息两种怎么办?
消息队列虽然叫队列,但是其实是一个单链表,Handler可以调用sendMessageAtFrontOfQueue(Messagemsg),postAtFrontOfQueue(Runnable r),将消息插入队头,最先取出,最先执行,之后再处理队列中的其他消息。
如果队列中只有延迟消息,此时发送一个普通消息,普通消息会插入队头,最先处理,而不会等延迟消息取出后,再取出普通消息。
只有当表头来了新消息,才会唤醒Loop来获取,Message要么立即执行,要么Loop刷新自我唤醒的定时器继续睡眠。
2.Handler发送消息的 Delay 可靠吗?
面试的时候我以为是要精确发送的,但是
答案是不靠谱的,引起不靠谱的原因有如下

1.发送的消息太多,Looper负载越高,任务越容易积压,进而导致卡顿
2.消息队列有一些消息处理非常耗时,导致后面的消息延时处理

3、当整个链表都是延迟执行的message时,如果此时插入的message也是延时执行的,是否一定要唤醒呢?

如果插入的message并非插入表头,说明拿的下一个message也不是自己,完全可以让线程继续休眠,没有必要唤醒,因为此时的定时器到期唤醒后拿到的正是待返回和执行的表头message。

总结:

1.传入延时时间,调用对应方法
sendMessageDelayed(Message msg, long delayMillis)最终调用的是enqueueMessage()将msg插入队列中。即不管发送的消息有没有延迟,都会先插入队列中,如果有延迟的话,则会在一个for的死循环中遍历消息队列并将传入消息msg插到单链表中合适的位置。
2.构造队列
事实上,消息队列是按照消息处理的时间when,按照从近到远的顺序排列的,最先要执行的任务放在消息队列的头部,依次排列。
3.唤醒线程
Looper.loop()通过MessageQueue中的next()去取消息,调用nativiePollOnce这个native方法,其内部会根据传入的nextPollTimeoutMillis,在延迟这么长时间之后唤醒线程从消息队列中读取消息,nativePollOnce函数内部会调用epoll_wait方法,设置超时时间为nextPollTimeoutMillis,epoll_wait在这个超时时间之后,就会唤醒线程,开始处理消息队列中的消息。

精简版总结:

消息队列在插入消息的时候是按照消息的触发时间顺序排序的,先执行的消息放在单链表的头部,最后执行的消息放在单链表的尾部;

在消息执行的过程中,通过native层设置epoll_wait的超时事件,使其在特定时间唤醒线程开始出现消息。

感谢网上大佬们的博客给我了足够的参考,如果有侵权请联系我删除,欢迎指出文章中的不足之处以及讨论,您的建议是我进步的动力,非常感谢您的阅读!

  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值