Android Handler消息机制及消息类型(同步消息、异步消息、消息屏障)

Android 消息机制作为系统运行的机制之一,在大一点的厂子面试被问到的概率比较大,可见它的重要性。下面将分两部分,首先介绍消息的整体机制,接着聊聊消息的类型。

一、消息机制

在消息机制中,有下面几个角色:

  • a. Message: 消息实体
  • b. MessageQueue: 消息队列,存放Message,以链表的方式实现
  • c. Looper: 对MessageQueue进行循环,获取Message给Handler处理
  • d. Handler: 对Message进行处理
    下面从源码的角度分析它们之间是怎么协作的
Looper:
public final class Looper {
    ...

    /** Initialize the current thread as a looper.
      * This gives you a chance to create handlers that then reference
      * this looper, before actually starting the loop. Be sure to call
      * {@link #loop()} after calling this method, and end it by calling
      * {@link #quit()}.
      */
    public static void prepare() {
        prepare(true);
    }

    private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));
    }

    /**
     * Initialize the current thread as a looper, marking it as an
     * application's main looper. The main looper for your application
     * is created by the Android environment, so you should never need
     * to call this function yourself.  See also: {@link #prepare()}
     */
    public static void prepareMainLooper() {
        prepare(false);
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();
        }
    }
    ...
}

从上述源码可知,Looper提供了两个方法来创建Looper对象,并将创建的对象保存在sThreadLocal。
从prepare方法可以看到,每个线程只允许创建一个Looper对象,否则会抛异常。
而我们在主线程创建Handler时,则不用自己创建Looper,那主线程的Looper是在哪里被创建的呢?我们看下prepareMainLooper()方法的注释
在 Looper 的 prepareMainLooper() 方法注释中可以看到这样一句话:

Initialize the current thread as a looper, marking it as an application's main looper.
The main looper for your application is created by the Android environment, so 
you should never need to call this function yourself.  See also: {@link #prepare()}

意思是说:将当前线程初始化为looper,将其标记为应用程序的主循环。您的应用程序的主要循环器是由Android环境创建的,永远不应该自己调用这个函数。
由此可知,是系统运行时就帮我们创建了Looper了,可以看下ActivityThread的main方法:

public static void main(String[] args) {
    ...
    Looper.prepareMainLooper();
    ...
    Looper.loop();
}

印证了上述的注释。
接下来看下里面loop()方法:

    /**
     * Run the message queue in this thread. Be sure to call
     * {@link #quit()} to end the loop.
     */
    public static void loop() {
        ...
        for (;;) {
            // 从消息队列中取得消息,具体的在下面MessageQueue进行解析
            Message msg = queue.next(); 
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
            ...
            try {
                // target是Handler,由Handler进行处理,具体看下面Handler的解析
                msg.target.dispatchMessage(msg);
                dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
            ...
        }
    }
Handler:

首先是send系列方法,拿一个出来看:

    public final boolean sendMessage(Message msg)
    {
        return sendMessageDelayed(msg, 0);
    }
    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) {
        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);
    }

可见最后是调用了sendMessageAtTime方法,其它的方法包括post系列的方法也一样,最终都是调用了sendMessageAtTime,不同的是最后传入的uptimeMillis。
然后再看下Looper调用Handler的dispatchMessage方法

    /**
     * Handle system messages here.
     * 处理消息
     */
    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }
MessageQueue:

接下来看下sendMessageAtTime里的enqueueMessage方法,此方法为将一个消息入队。进入MessageQueue类

    // msg-上面说的消息实体, when-需要延时执行的时长
    boolean enqueueMessage(Message msg, long when) {
        // 参数检查省略
        synchronized (this) {
            // 检查Looper及相应的队列是否已经终止,是的话对msg进行回收并退出
            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;  // mMessages指向队列头部
            boolean needWake;
            if (p == null || when == 0 || when < p.when) {
                // New head, wake up the event queue if blocked.
                // 假设队列为空 或 msg不需要延时 或 
                // msg需要延时的时长小于队头消息的延时时长,
                // 则将msg插入对头
                msg.next = p;
                mMessages = msg;
                // 标记是否需要唤醒,mBlocked表示现在消息队列是否处于阻塞状态
                // 阻塞的原因在下面next()方法获取消息时再进行说明
                needWake = mBlocked; 
            } else {
                // 将msg插入在队列的中部,插入的位置
                // 根据when得到,when小的msg会在前面
                // 这样方便保证之后先出队msg都是需要
                // 先执行的,从而保证在delay的时候过后执行msg
                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;
            }
            // 当在next()获取msg时,假设获取到的when大于0,
            // 说明此时需要延时when后才能
            // 执行这个msg,因此进行了阻塞,mBlocked=true。
            // 但是这个时候有新的消息入队并处于队头位置,
            // 因此先于上一个队头消息执行,所醒
            // 以此时需要唤醒队列,才能保证后来者需要先执行
            // 的不会因为阻塞而中断
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }

接下来看看上面提到的出队的方法next():

    Message next() {
        // 条件检查,省略
        // 第一次循环的时候为-1,其它情况不会时-1
        int pendingIdleHandlerCount = -1; 
        int nextPollTimeoutMillis = 0; // 需要进行阻塞的时间
        for (;;) { // 死循环,不断获取msg
            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) {
                        // 下一个消息还未到执行时间,因此设置一个时间进行阻塞,
                        // 过了这个时间,将唤醒
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        // 此时有消息到执行时间了,则设置队列处于不阻塞状态,
                        // 将队头出队,并返回
                        mBlocked = false; 
                        if (prevMsg != null) { // 有消息屏障的情况下,将链表的前部分的同步消息连接到后面
                            prevMsg.next = msg.next;
                        } else { // 否则,直接将mMessages指向下一个消息即可
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    // 如果没有消息,则设置阻塞时长为无限,直到被唤醒
                    nextPollTimeoutMillis = -1;
                }

                // Process the quit message now that all pending messages have been handled.
                if (mQuitting) {
                    dispose();
                    return null;
                }

                // 第一次循环 且 (消息队列为空 或 
                // 消息队列的第一个消息的触发时间还没有到)时,
                // 表示处于空闲状态
                // 获取到 IdleHandler 数量
                if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
                if (pendingIdleHandlerCount <= 0) {
                    // 没有 IdleHandler 需要运行,循环并等待
                    mBlocked = true; // 设置阻塞状态为 true
                    mBlocked = true;
                    continue;
                }

                if (mPendingIdleHandlers == null) {
                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                }
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
            }

            // 运行 IdleHandler,只有第一次循环时才会运行
            for (int i = 0; i < pendingIdleHandlerCount; i++) {
                final IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null; // release the reference to the handler

                boolean keep = false;
                try {
                    keep = idler.queueIdle();
                } catch (Throwable t) {
                    Log.wtf(TAG, "IdleHandler threw exception", t);
                }

                if (!keep) {
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }

            // 重置 IdleHandler 的数量为 0,确保不会重复运行
            // pendingIdleHandlerCount 置为 0 后,上面可以通
            // 过 pendingIdleHandlerCount < 0 判断是否是第一次循环,
            // 不是第一次循环则 pendingIdleHandlerCount 的值不会变,始终为 0。
            pendingIdleHandlerCount = 0;

            // 在执行 IdleHandler 后,可能有新的消息插
            // 入或消息队列中的消息到了触发时间,
            // 所以将 nextPollTimeoutMillis 置为 0,表示不
            // 需要阻塞,重新检查消息队列。
            nextPollTimeoutMillis = 0;
        }
    }
经过上面两个方法,这里总结下消息机制的总体流程:

消息在入队(即有新消息加入)的时候,会根据delay的时间,在队列找到合适的位置入队,从而保证整个队列的顺序是以延迟时间从小到大排序。

  • a. 当入队的消息的delay时间比原先队头消息短的时候,或者队列为空的时候,则消息会入队在队头,并且当此时列表处于阻塞状态时,则唤醒队列;
  • b. 否则入队的位置为非队头。

这个特性方便后续获取消息即出队的时候,直接出队头消息,即是最优先需要执行的消息。出队时

  • a. 若队列为空,则无限长时间进行阻塞;
  • b. 出队的消息要是到达执行时间了,则出队;
  • c. 出队的消息还没到执行时间,则进行对应时间的阻塞。

二、消息类型(同步消息、异步消息、消息屏障)

上面已经介绍完了Handler的机制,代码注释中提及了消息屏障、异步消息,下面具体介绍下这部分内容。

Hander的消息类型有三种,分别是同步消息、异步消息、消息屏障,下面先看看使用方式。

1. 同步消息

我们平常使用Handler发送的消息基本都是同步消息,例如下面的代码:

        Handler handler = new Handler(Looper.getMainLooper());

        // 使用方式1
        handler.post(new Runnable() {
            @Override
            public void run() {
                // do something
            }
        });

        // 使用方式2
        handler.sendEmptyMessage(0);

        // 使用方式3
        Message msg = Message.obtain();
        msg.what = 1;
        handler.sendMessage(msg);

        ...
2. 异步消息

使用方式如下:

        Message msg = Message.obtain();
        msg.what = 2;
        msg.setAsynchronous(true); // 设置消息为异步消息
        handler.sendMessage(msg);
3. 消息屏障

添加消息屏障的API如下:

    /**
     * Posts a synchronization barrier to the Looper's message queue.
     *
     * @return A token that uniquely identifies the barrier.  This token must be
     * @hide
     */
    public int postSyncBarrier() {
        return postSyncBarrier(SystemClock.uptimeMillis());
    }

该方法是@hide的,因此需要自行反射调用,添加完消息屏障后,会返回这个消息的token,移除消息屏障时,需要用到这个token,具体API如下(同样需要反射调用):

    /**
     * Removes a synchronization barrier.
     *
     * @param token The synchronization barrier token that was returned by
     * {@link #postSyncBarrier}.
     *
     * @throws IllegalStateException if the barrier was not found.
     *
     * @hide
     */
    public void removeSyncBarrier(int token) {
    }

上面介绍了三种消息类型的大体使用方式,那它们有什么作用,或者有什么区别呢?

下面回到MessageQueue#next()的源码中:

    Message next() {
        for (;;) {
            synchronized (this) {
                Message prevMsg = null;
                Message msg = mMessages;
                if (msg != null && msg.target == null) { // 判断是否有消息屏障
                    // 有消息屏障的话,取出后面第一条异步消息
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                if (msg != null) {
                    if (now < msg.when) {
                    } else {
                        if (prevMsg != null) { // 有消息屏障的情况下,将链表的前部分的同步消息连接到后面
                            prevMsg.next = msg.next;
                        } else { // 否则,直接将mMessages指向下一个消息即可
                            mMessages = msg.next;
                        }
                    }
                }
            }
        }
    }

从上面的代码可以看出,消息屏障的作用是来阻塞消息队列后面的同步消息,而异步消息不受消息屏障影响。在无消息屏障的情况下,同步消息与异步消息无本质上的区别。

转载请注明出处:https://www.jianshu.com/p/9ecccd6d4506

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