从Handler.postDelayed来看看Android怎么实现处理延时消息

前言

Android的消息机制之前有一篇文章有写,里面具体讲到了Handler怎么发送和处理消息的整个过程。感兴趣的同学可以先跳转过去看看 从Handler.post(Runnable r)再一次梳理Android的消息机制(以及handler的内存泄露)

延时操作

通常要实现延时操作有这几种方法:

  • TimerTask
  • Rxjava
  • Thread
  • Handler

这里我们主要来关注最后一种方法,使用Handler的postDelayed方法来处理延时:

new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                //延时到了的操作
            }
        }, 1000);//毫秒

这里就很简单的实现了一个1秒的延时,且不会阻塞主线程。

但是怎么来实现这不是要探讨的话题,关键是在Hadler的内部是怎么来处理这个延时的呢,我们来进一步看看源码。

Handler发送延时消息

来一步步看看postDelayed方法:

    public final boolean postDelayed(@NonNull Runnable r, long delayMillis) {
        return sendMessageDelayed(getPostMessage(r), delayMillis);
    }

这里将Runnable封装成一个消息

    private static Message getPostMessage(Runnable r) {
        Message m = Message.obtain();
        m.callback = r;
        return m;
    }

然后调用

sendMessageDelayed

    public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
        if (delayMillis < 0) {//判断延时时间
            delayMillis = 0;
        }
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }

在这个方法可以看到,发送延时消息,其实就是获取开机到当前的时间总数+延时时间,然后在指定的时间来发送消息,再来到:

sendMessageAtTime

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

最终将消息加到消息队列,同时指定时间

enqueueMessage

    private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
            long uptimeMillis) {
        msg.target = this;
        msg.workSourceUid = ThreadLocalWorkSource.getUid();

        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }

到这里为止,Handler里面的处理就完成了,接下来来到MessageQueue

MessageQueue的消息处理

插入消息

打开MessageQueue,找到

enqueueMessage

    boolean enqueueMessage(Message msg, long when) {
        ...省略部分代码...
        synchronized (this) {
            ...省略部分代码...

            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;//如果当前的队列是阻塞的状态,那么mBlocked就会为true
            } 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); //调用native方法,唤醒阻塞
            }
        }
        return true;
    }

在这里面,首先有一个判断,将现在队列最前面的消息跟现在将要入队的消息的执行时间做对比,如果现在的消息时间在前面或者队列没消息,那就需要将现在这条消息直接插入到最前面。

跟队首消息比对之后,执行时间大于队首消息,那就应该把消息插到后面。

在这里面有一个死循环,挨个来判断队列里面的消息执行时间与现在消息的执行时间,目的是将现在这条消息按照执行时间来插入到比他执行时间大的消息之前。

这里的needWake用来判断是否需要唤醒阻塞。

取出消息

到这里我们直接来到MessageQueue的next方法,具体为什么会调用这个方法,开头的文章链接(点击跳转)有详解,这里直接来看这个方法

next

    Message next() {
    ...省略部分代码...
            int pendingIdleHandlerCount = -1; // -1 only during first iteration
            int nextPollTimeoutMillis = 0;
            for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }

            nativePollOnce(ptr,nextPollTimeoutMillis);//调用naive方法阻塞管道,由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);
                    } else {//取出消息交给Handler处理
                        // 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;
                }
    ...省略部分代码...
    }

在这里首先会调用nativePollOnce阻塞线程,对应enqueueMessage方法里的nativeWake唤醒线程。然后也是一个死循环,在循环里面,首先会判断消息的执行时间,如果执行时间大于等于现在的时间就取出消息。

总结

其实从源码看下来,发现Handler在处理延时消息的时候,其实跟普通消息一样,只不过延时消息是在普通消息之上加上了我们设置的延时,所有的消息都是按照时间来取出处理的。

Handler文章关联:

从Handler.post(Runnable r)再一次梳理Android的消息机制(以及handler的内存泄露)

一个线程可以有几个Looper?几个Handler?从Looper.prepare()来看看关于Looper的一些问题

Thread、Handler和HandlerThread关系何在?

从Handler探究AsyncTask原理

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345