Loading...
墨滴

牛角尖

2021/03/22  阅读:18  主题:默认主题

Android 老生常谈之消息机制

前言

我们都知道,GUI 应用程序是消息(事件)驱动的,Android 应用程序也是如此。消息的传递要依赖于应用框架提供的消息机制。

实际上 Android 系统提供了两种消息机制:

组件间消息:Intent 机制

线程间消息:Message 机制

而我们通常说的 “Android 消息机制”,单指 “Message 机制”,这篇文章,我们要探讨的依旧是这个老生常谈的 Message 机制。

概述

在开发中,大家常打交道的是 Handler,我们常用 Handler 来更新 UI。

实际上更确切的讲,Handler 是用来切换线程的。

比如我们常在子线程中进行 “请求网络、存取文件、操作数据库” 等耗时操作,操作完成后通过 Handler 切换到主线程对 UI 进行一些改变;当然也会出现在子线程中再创建子线程,Handler 用于子线程和子线程的子线程间的切换也是可以的。

所以我们说 Android 的消息机制,基本可以认为就是 Handler 的工作机制。

Handler 最重要的工作就是收发 Message ;Handler 的工作需要 MessageQueueLooper 的支持;Looper 是运行在创建 Handler 的线程中的,而以线程为作用域的数据存取需要用到 ThreadLocal 这个数据存储类。

这几个蓝色类就是我们讨论 Android 消息机制要关注的,我们来看一下他们的 UML 类图。 image.png

关于消息机制的工作流程,也贴一张图,我们有个大体印象。

image.png
image.png

消息机制的工作流程分三步进行:

1、Handler 调用 sendMessage() 将 Message 入队到 MessageQueue 中;

2、Looper 类的 loop() 方法中无限循环,从 MessageQueue 中取出 Message ,再交回给 Handler ;

3、Handler 调用 dispatchMessage() 分发处理消息。

下面我们从 Message 开始下手,看一下消息机制到底是怎么工作的。

Message

“消息机制” 要传递的消息,就是这个 Message 了。

在其类描述中,官方有这么一句话:

While the constructor of Message is public, the best way to get one of these is to call Message.obtain() or one of the Handler.obtainMessage() methods, which will pull them from a pool of recycled objects.

简单翻译一下:

当需要获取一个 Message 对象时,不建议通过构造方法进行实例化,尽管构造方法是 public 的,建议通过 Message.obtain() 或者 Handler.obtainMessage() 这两种方式,他们会从消息池中拉取一个消息复用。

消息与消息池,就相当于线程与线程池,从消息池中复用已经处理完毕的消息,要比直接通过构造方法 new 新的消息更节省资源。

当消息池为空时,系统会自动 new 一个新的消息返回给调用者。

另外,消息池的最大容量是 50。

Message 类中有一个属性 int FLAG_IN_USE

    /** If set message is in use.
     * This flag is set when the message is enqueued and remains set while it
     * is delivered and afterwards when it is recycled.  The flag is only cleared
     * when a new message is created or obtained since that is the only time that
     * applications are allowed to modify the contents of the message.
     *
     * It is an error to attempt to enqueue or recycle a message that is already in use.
     */

    /*package*/ static final int FLAG_IN_USE = 1 << 0;

这个属性用来标示消息是否在用。

当消息入队或者被回收, FLAG_IN_USE 会被置为 1,表示消息在用,此时如果调用 mHandler.sendMessage(msg) 企图将消息再次入队,会报异常 This message is already in use

Message 类中有一个属性 Message next,这个属性与 MessageQueue 息息相关。

MessageQueue

MessageQueue 管理着一个 Message 队列,Handler 向其中添加消息,Looper 从其中取消息。

MessageQueue 虽然叫做消息队列,但其内部的数据结构是单链表,也就是说,是用单链表实现的队列,如下图所示。

image.png
image.png

MessageQueue 中的属性 Message mMessages 指向第一个消息节点 m1 ;每个消息节点通过上述的 Message next 属性,依次持有其后一个消息节点的引用。这就是单链表实现的消息队列。

MessageQueue 有几个方法我们要关注一下。

1、enqueueMessage() 入队,由 Handler 调用,会将新消息添加到队尾。

2、next() 出队,由 Looper 调用,会从队首取出要立即处理的消息。当队列中没有消息时,next() 方法会阻塞。

3、quit(boolean safe) 队列退出,由 Looper 调用。从入参可明显看出,队列的退出分为非安全退出和安全退出,其内部分别调用 removeAllMessagesLocked()removeAllFutureMessagesLocked() 两个方法。

当队列已退出,还通过 Handler 向队列中添加消息的话,会报异常 sending message to a Handler on a dead thread

4、removeAllMessagesLocked() 非安全退出队列,将消息队列中所有消息全部回收,不再处理。

5、removeAllFutureMessagesLocked() 安全退出队列,遍历消息队列,当找到第一个延迟消息时,将其与其后的所有消息回收。

那什么是延迟消息?

比如说通过 mHandler.postDelayed(Runnable r, long delayMillis) 等方式向消息队列中添加的消息,设置了消息的延迟处理时间,这样的消息就是延迟消息。

可以发现,我们一般不会直接操作 MessageQueue,就连 MessageQueue 的实例化,都是在 Looper 的构造中完成的。

在分析 Looper 之前,我们要先了解下 ThreadLocal ,这有助于我们更好的理解 Looper。

ThreadLocal

ThreadLocal 是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,数据存储以后,只有在指定线程中可以获取到存储的数据,对于其他线程来说则无法获取到数据。 ——《Android 开发艺术探索》

ThreadLocal 可以将线程的数据存储起来,且只有相应线程可以访问和获取。

Looper Looper 负责消息调度,即负责从 MessageQueue 中取出消息,交给 Handler 进行处理。

一个线程只能有一个 Looper,要通过 Looper.prepare() 创建一个 Looper ,之后再调用 Looper.loop() 开始循环从 MessageQueue 中取 Message 进行处理。

并且,Handler 的工作需要有 Looper,没有 Looper 会报错 Can't create handler inside thread that has not called Looper.prepare()

在 Looper 的类描述中,官方提供了一段代码示例:

    class LooperThread extends Thread {
        public Handler mHandler;
  
        public void run() {
            Looper.prepare();
  
            mHandler = new Handler() {
                public void handleMessage(Message msg) {
                    // process incoming messages here
                }
            };
  
            Looper.loop();
        }
    }

我们可能会有这样的疑问:

1、我们创建的子线程,大多数情况下并没有调用 Looper.prepare()Looper.loop() 啊?

这是因为我们的这些子线程,并没有使用 Handler 。上文提到 “Handler 的工作需要有 Looper”,不用 Handler,自然就不需要 Looper 了。

2、UI 主线程中,我们用到了 Handler,但我们也没有调用 Looper.prepare()Looper.loop() 啊?

这是因为在程序的 main() 函数中,系统已经帮我们调用了 Looper.prepare()Looper.loop()main() 函数位于 ActivityThread 类中。

Looper 中还有两个重要的方法 quit()quitSafely() ,其内部实现极其简单。

    public void quit() {
        mQueue.quit(false);
    }

    public void quitSafely() {
        mQueue.quit(true);
    }

这两个方法是通过直接调用 MessageQueue 的退出方法,将消息队列给退出了。

那 Looper 的无限循环方法 loop() 怎么退出呢?

 public static void loop(){
 
  // ……
  
        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
            // ……
         }
         // ……  
 }

很简单,当从消息队列中拿不到消息时,loop() 方法就会退出。

那从消息队列拿不到消息,一定是消息队列退出了,没有可能是因为消息队列中的消息处理完了吗?

没有可能。

因为如果是因为消息队列中的消息处理完了,queue.next() 会阻塞,不会出现拿不到消息的情况的。

所以,如果子线程中用了 Looper,一定要调用 Looper.quit() 方法进行退出,否则 queue.next() 会一直阻塞,消息队列和 Looper 都无法回收,会造成内存泄漏。

Handler

我们知道,Handler 发送消息可以通过 sendXxx() 系列方法和 postXxx() 系列方法。

postXxx() 系列方法,内部还是将入参的 Runnable 包装成 Message ,再通过 sendXxx() 系列方法,最终调用 MessageQueue 的 enqueueMessage() 将消息入队的。

至于 Handler 消息处理的回调函数,有如下三种重写方式:

第一种:

    Handler mHandler = new Handler();
    mHandler.post(new Runnable() {
        @Override
        public void run() {

        }
    });

第二种:

    Handler mHandler = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            return false;
        }
    });

第三种:

    Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };

我们上文说过,Looper 的 loop() 方法拿到消息后,会交给 handler 进行处理:

    msg.target.dispatchMessage(msg)

我们看下 dispatchMessage(msg) 源码:

    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {
            handleCallback(msg);
        } else {
            if (mCallback != null) {
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);
        }
    }

这段代码中的 第 4 行 ,调用的就是上面的回调方法重写的第一种方式;

这段代码中的 第 7 行 ,调用的就是上面的回调方法重写的第二种方式;

这段代码中的 第 11 行 ,调用的就是上面的回调方法重写的第三种方式。

好了,Handler 做的工作就这么多,可以发现,它就干两件事:调用消息队列将消息入队,调用回调方法处理消息

后记

经过上文对各个类的梳理,我们再来看看下面这张图,会发现 Android 消息机制的工作流程并不复杂。

image.png
image.png

本文首发于 公众号:牛角尖尖上起舞,ID:niujiaojianhi,欢迎关注。

牛角尖

2021/03/22  阅读:18  主题:默认主题

作者介绍

牛角尖