美文网首页Android开发经验谈Android开发Android技术知识
回转寿司你一定吃过!——Android消息机制(分发)

回转寿司你一定吃过!——Android消息机制(分发)

作者: 唐子玄 | 来源:发表于2019-02-02 11:28 被阅读6次

    这是“Android消息机制”系列的第二篇文章,系列文章目录如下:

    1. 回转寿司你一定吃过!——Android消息机制(构造)
    2. 回转寿司你一定吃过!——Android消息机制(分发)

    消息机制的故事


    寿司陈放在寿司碟上,寿司碟按先后顺序被排成队列送上传送带传送带被启动后,寿司挨个呈现到你面前,你可以选择吃或者不吃。

    将Android概念带入后,就变成了Android消息机制的故事:
    寿司碟 ---> 消息(Message)
    队列 ---> 消息队列(MessageQueue)
    传送带 ---> 消息泵 (Looper)
    寿司 ---> 你关心的数据

    暂未找到 Handler 在此场景中对应的实体。它是一个更抽象的概念,它即可以生产寿司,又把寿司送上传送带,还定义了怎么享用寿司。暂且称它为消息处理器吧。

    如果打算自己开一家回转寿司店,下面的问题很关键:

    1. 如何生产寿司(如何构造消息)
    2. 如何分发寿司(如何分发消息)

    关于如何构造消息可以移步上一篇博客回转寿司你一定吃过!——Android消息机制(构造)。这一篇从源码角度分析下“如何分发消息”。

    分发要解决的问题是如何将寿司从厨师运送到消费者。回转寿司系统是这样做的:将寿司挨个排好放在传送带上,然后让传送带滚动起来。对应的,在Android消息系统中也有类似的两个步骤:1. 消息入队 2. 消息泵
    (ps: 下文中的 粗斜体字 表示引导源码阅读的内心戏)

    1. 消息入队


    关于入队需要提两个基本问题:(1)什么时候入队(2)怎么入队。第二个问题其实是在问“消息队列的数据结构是什么?”。特定数据结构对应特定插入方法。
    对于消息队列一无所知的我完全没有了头绪,这源码该从哪里开始读起?没有思路的时候我们还可以YY(YY是人类特有的强大技能)。凭借着对数据结构残存的记忆,我隐约觉得“入队”应该是队列提供的基本操作,那就先从MessageQueue开始读吧~

    /**
     * Low-level class holding the list of messages to be dispatched by a
     * {@link Looper}.  “Messages are not added directly to a MessageQueue,
     * but rather through {@link Handler} objects associated with the Looper.”
     * <p>
     * <p>You can retrieve the MessageQueue for the current thread with
     * {@link Looper#myQueue() Looper.myQueue()}.
     */
    public final class MessageQueue
    {
        ...
    }
    

    还好注释中的每个单词都看得懂,其中带双引号的那句话非常关键,它说“消息不是直接加到消息队列中的,而是通过Handler对象”。不急着去看Handler,先找一下MessageQueue是否有“入队操作”。

    //省略了一些非关键代码    
    boolean enqueueMessage(Message msg,
                               long when)
        {
            ...
            synchronized (this)
            {
                ...
                msg.markInUse();
                msg.when = when;
                //p指向消息队列头结点
                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
                {
                    ...
                    //从消息队列队头开始寻找合适的位置将消息插入
                    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;
                }
            }
            ...
            return true;
        }
    
    • 不出所料,果然有一个入队函数,消息队列的数据结构和消息池一模一样(消息池的介绍可以点击这里),都是链表。
    • 看到这里第二个问题基本解决了:消息是通过链表的插入操作进入消息队列的。让我们思考的再深入一点:新消息插入到链表的什么位置? 可以看到源码中有一个大大的if-else,判断条件是传入的参数when,沿着调用链往上搜索,在Handler中会发现如下函数:
        /**
         * “Enqueue a message into the message queue after all pending messages
         * before the absolute time (in milliseconds) <var>uptimeMillis</var>.”
         * <b>The time-base is {@link android.os.SystemClock#uptimeMillis}.</b>
         * Time spent in deep sleep will add an additional delay to execution.
         * You will receive it in {@link #handleMessage}, in the thread attached
         * to this handler.
         * 
         * @param uptimeMillis “The absolute time at which the message should be
         *         delivered, using the
         *         {@link android.os.SystemClock#uptimeMillis} time-base.”
         *         ...
         */
        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);
        }
    
    • 注释中带引号的那句话很关键:“将消息插入到消息队列中,并且排在所有uptimeMillis之前产生的消息后面”。uptimeMillis表示消息被发送的时间。这么看来,消息是按时间先后顺序排列的,最旧的消息在队头,最新的消息在队尾。那消息入队就分两种情况:1. 尾插入 2.中间插入。其中尾插入表示最新的消息插入队尾。回头再看一遍MessageQueue.enqueueMessage(),那个大大的if-else就实现了这两种情况。
    • Handler.sendMessageAtTime()沿着调用链继续往上搜索,就会找到下面这个熟悉的方法:
        public final boolean sendMessage(Message msg)
        {
            return sendMessageDelayed(msg, 0);
        }
    
    • 这不就是我们用来发消息的Handler.sendMessage()吗!至此,让我们总结一下“消息入队”:Handler发送消息就是将消息按时间顺序插入到消息队列,消息队列是链表结构,链头是最旧的消息,链尾是最新的消息

    2. 消息泵


    寿司已经按时间顺序排列好了,是时候按下按钮启动传送带让寿司循环起来了。对于Android消息机制来说,让消息循环起来就表现为不断从消息队列中拿消息。MessageQueue中有入队操作,必然有出队操作

    //省略大量非关键代码
       Message next() {
            ...
            for (;;) {
                ...
                synchronized (this) {
                    // Try to retrieve the next message.  Return if found.
                    final long now = SystemClock.uptimeMillis();
                    Message prevMsg = null;
                    //msg指向消息队列队头
                    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 {
                            // 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;
                    }
                    ...
                }
            }
      }
    
    • 这个函数很长,省略了一些和主题不相关的细节,比如:队列空闲等待,异步消息。去掉了这些特殊情况后,出队操作就是取消息队列的头(队列头是最旧的消息,队列尾是最新的消息),这符合队列先进先出的特性,越早的消息越先被分发。
    • 必然有一个循环会不停的调用 MessageQueue.next(),从消息队列中不断的取消息进行分发,经过一顿搜索,果然在 Looper中找到了:
        public static void loop()
        {
            //获得当前线程的消息泵
            final Looper me = myLooper();
            if (me == null)
            {
                throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
            }
            //获得当前线程的消息队列
            final MessageQueue queue = me.mQueue;
    
            // Make sure the identity of this thread is that of the local process,
            // and keep track of what that identity token actually is.
            //nandian
            Binder.clearCallingIdentity();
            final long ident = Binder.clearCallingIdentity();
    
            //取消息的无限循环
            for (; ; )
            {
                //从队头取出消息
                Message msg = queue.next(); // might block
                //没有消息则退出循环
                if (msg == null)
                {
                    // No message indicates that the message queue is quitting.
                    return;
                }
    
                // This must be in a local variable, in case a UI event sets the logger
                Printer logging = me.mLogging;
                if (logging != null)
                {
                    logging.println(">>>>> Dispatching to " + msg.target + " " +
                                            msg.callback + ": " + msg.what);
                }
    
                //分发消息
                msg.target.dispatchMessage(msg);
    
                if (logging != null)
                {
                    logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
                }
    
                // Make sure that during the course of dispatching the
                // identity of the thread wasn't corrupted.
                //nandian
                final long newIdent = Binder.clearCallingIdentity();
                if (ident != newIdent)
                {
                    Log.wtf(TAG, "Thread identity changed from 0x" + Long.toHexString(ident) + " to 0x" + Long.toHexString(newIdent) + " while dispatching to " + msg.target.getClass()
                                                                                                                                                                            .getName() + " " + msg.callback + " what=" + msg.what);
                }
    
                //回收消息
                msg.recycleUnchecked();
            }
        
    
    • 这个函数是Android消息机制中构造并分发消息的终点,处理消息的起点。Looper通过无限循环从消息队列中取出最旧的消息,并分发给消息对应的消息处理器,最后回收消息。至此,上一篇文章回转寿司你一定吃过!——Android消息机制(构造)中留下的疑问就解决了:消息是在被分发后立马回收的。
    • Looper.loop()什么时候会被调用?我们都知道主线程自带Looper,虽然这是一个很好的切入点,但其中牵涉到太多和主题无关的内容。所以换一个更纯粹的切入点,那就是HandlerThread
    /**
     * Handy class for starting a new thread that has a looper. The looper can then be 
     * used to create handler classes. Note that start() must still be called.
     */
    public class HandlerThread extends Thread {
        int mPriority;
        int mTid = -1;
        Looper mLooper;
        private @Nullable Handler mHandler;
    
        @Override
        public void run() {
            mTid = Process.myTid();
            //1.准备Looper
            Looper.prepare();
            synchronized (this) {
                mLooper = Looper.myLooper();
                notifyAll();
            }
            Process.setThreadPriority(mPriority);
            onLooperPrepared();
            //2. Looper开始循环
            Looper.loop();
            mTid = -1;
        }
    }
    
    • 注释又一次给了我们很多提示:“该类用于创建带有Looper的线程”。难道并不是所有的线程都带有Looper?(想想也是:线程是一个Java概念,Looper是一个Android概念)。所以我们需要在线程启动的时候特意做些什么才能得到带有Looper的线程。在Thread.run()中看到了两个关键方法,其中Looper.loop()已经分析过了,在它之前还有一个Looper.prepare(),点进去看看:
    /**
      * “Class used to run a message loop for a thread.  Threads by default do
      * not have a message loop associated with them; to create one, call
      * {@link #prepare} in the thread that is to run the loop, and then
      * {@link #loop} to have it process messages until the loop is stopped.”
      */
    public final class Looper {
        static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
        final MessageQueue mQueue;
    
         /** 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));
        }
    }
    
    • 一脸茫然的时候就看注释,带引号的注释揭露了关键真相:Looper用于为线程创建消息循环系统,默认情况下线程没有和它相关联的消息循环系统。可以通过在线程中调用Looper.prepare()来启动一个消息循环系统,接着调用Looper.prepare()来循环处理消息
    • prepare()中,新建了Looper实例,并且设置给ThreadLocal对象,这个类用于保证线程对象和自定义类型对象一对一的关系。这个一个很大的主题,就不展开了。当下只要知道Looper通过它将自己的实例和某一个线程绑定,即一个线程只有一个Looper对象。所以Android消息系统的层级结构是这样的:1个Thread 对应 1个Looper,1个Looper有1个MessageQueue,1个MessageQueue有若干Message

    总结


    Android消息机制中的“分发消息”部分讲完了,总结一下:发送消息时,消息按时间先后顺序插入到消息队列中,Looper遍历消息队列取出消息分发给对应的Handler处理

    故事还没有结束,下一篇会继续讲解“处理消息”。

    相关文章

      网友评论

        本文标题:回转寿司你一定吃过!——Android消息机制(分发)

        本文链接:https://www.haomeiwen.com/subject/sjoksqtx.html