【Android】消息机制

首先要清楚的是,Android 的 UI 线程(也就是主线程)不能执行长耗时操作,否则会造成 ANR 程序无法响应异常,在子线程也无法直接更新 UI 元素,所以要多线程机制的参与,Android 提供了消息机制,其上层接口就是 Handler。

消息机制的 Handler 体系主要包括 Handler、Looper 和 MessageQueue,Handler 是消息的生产者和消费者,是消息的提交和处理入口;Looper 是消息的循环者,负责从 MessageQueue 取出 Message 消息,调用消息目标的 Handler 去处理;MessageQueue 则是消息队列,负责消息的自然排序存储。

1 个线程至多持有 1 个 Looper,每个 Looper 实例持有 1 个 MessageQueue,所以 Thread、Looper 和 MessageQueue 是对应的映射关系,不同的 Message 应该有不同的处理逻辑,所以 MessageQueue 允许注册多个 Handler。

Android 的消息机制概述

普通线程 Thread 初始化后不持有 Looper 实例,要通过调用 Looper.prepare 方法来获得 Looper 实例,并调用 Looper.loop 方法使 Looper 进入轮询循环,不断从 MessageQueue 获取信息。

java 复制代码
new Thread(v -> {
    Looper.prepare();
    Looper.loop();
}).start();

Handler 的构造方法有 3 种,分别是

  1. 空参构造,内部会使用 Looper.myLooper 方法来获取当前线程的 Looper 并绑定
  2. Handler(Looper looper),明确指定消息的执行线程
  3. 带 Callback 构造,可以自定义消息处理逻辑,不用重写 handlerMessage 方法

所以使用原始 Thread 的消息队列构造应该是

java 复制代码
Handler handler;
new Thread(v -> {
    Looper.prepare();
    handler = new Handler() {
        @Override
        boolean handleMessage(Message msg) {
            // 消息处理逻辑
        }
    }
    Looper.loop();
}).start();

这种初始化方法的缺陷是外部 handler 的生命周期依赖于匿名线程,handler 在 Looper.prepare 前是空指针,更优雅的方式是使用 HandlerThread

java 复制代码
HandlerThread handlerThread = new HandlerThread("handlerThread"); // 线程名称
handlerThread.start();  // 启动 HandlerThread

Handler handler = new Handler(handlerThread.getLooper()) {
    @Override
    public void handleMessage(Message msg) {
        // 消息处理逻辑
    }
};

初始化消息循环后,可以调用 handler 实例的 sendMessage 或 post 来提交任务,前者提交 Message 实例,执行并返回结果;后者直接提交 Runnable 直接执行逻辑,不关心消息标识。提交任务相当于投递到 MessageQueue,由 Looper 线程循环处理,调度执行提交 Message 的 Handler 的 dispatchMessage 方法,最后可能由 Runnable 执行,或根据构造 Handler 时提供的 Callback 执行,如果二者都为 null,则执行 handlerMessage 逻辑。Handler 消息机制默认是 Callback 的,也就是不支持返回值,可以通过回调接口或回复 Message 来获取执行结果。

持有关系上,Thread 持有 Looper 实例,Looper 持有外部 Thread 引用和 MessageQueue 实例,Message 实体类持有 int 类的消息类型句柄 what、Object 类的数据 obj、发送自身的 Handler 实例和可指定的处理方式 Callback,Handler 持有 Looper 和 MessageQueue 的引用。

Android 的消息机制分析

ThreadLocal 的工作原理

ThreadLocal 是线程内部的数据存储类,只有在指定线程中才可以获取到存储的数据,类似 MINECRAFT 的末影箱,当某些数据是以线程为作用域且不同线程具有不同的数据副本的时候,就可以考虑采用 ThreadLocal。

java 复制代码
public void set(T value) {
    Thread currenThread = Thread.currentThread();
    Values values = values(currentThread);
    if (values == null) {
        values = initializeValues(currentThread);
    }
    values.put(this, value);
}

Thread 类内部有专门存储 ThreadLocal 数据的 ThreadLocal.Values localValues,在 localValues 内部有 Object 类型的数组 table,ThreadLocal 的值就存在 table 数组中。在现代 Android 中,存储 ThreadLocal 数据的字段变成 ThreadLocalMap threadLocals,本质是映射表,key 是 ThreadLocal 对象本身的弱引用,value 是该线程存储的具体对象,所以代码块中最后有向 Map 加键值对的 put 方法,覆盖具体对象 value 变成新值。因为 set 方法局限在独立线程内,所以 Thread 存储的 ThreadLocal 数据往往只存储 1 个键值对,也就是当前 ThreadLocal 弱引用和该线程的存储数据。

ThreadLocal 在消息机制中起到什么作用,让我们看 Looper 的 prepare 简化方法流程,其中的 sThreadLocal 就是 ThreadLocal 实例,Looper 内部使用它来保存线程对应的 Looper,创建 Looper 后将其绑定到当前线程的 ThreadLocal,Looper.myLooper 方法也是通过访问 ThreadLocal 来获得当前线程的 Looper。

java 复制代码
private static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<>();

public static void prepare() {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper per thread");
    }
    sThreadLocal.set(new Looper());
}

消息队列的工作原理

消息队列即 MessageQueue,内部实现是单链表,首先我们要理解消息队列的特性,消息按时间顺序发送和处理,也就是 FIFO 先进先出,消息随时可能被插入或删除,因此消息队列的长度是动态变化的。

Android 的 MessageQueue 没有单独维护尾指针,所以插入普通消息要遍历至链表尾插入,时间复杂度 O(n),插入延时消息 MessageQueue 会从链表头指针开始按时间比较和遍历,找到延时消息的插入位置,取消息直接取链表头节点即可,所以是常数时间复杂度。

Handler 的工作原理

在概述部分我们知道,提交的消息事务会经过 Handler 的 dispatchMessage 方法,Message 消息本身如果通过 post 方法提交会带有 Runnable callback 执行体,也就是代码块的 handlerCallback 方法,Handler 会优先处理 post 消息,其次 Handler 会处理构造方法中若提供的 Handler.Callback mCallback 的 handleMessage 方法,这种方式避免重写 Handler 本身的 handleMessage 方法,方便复用,最后才会执行 Handler 本身的 handleMessage 方法。

值得注意的是,mCallback 的 handleMessage 方法会返回 boolean 来表示是否已经完全处理消息,类似 View 的事件分发机制,如果返回 false 则 dispatchMessage 方法会继续将消息传播到 Handler 本身的 handleMessage 来执行。

java 复制代码
public void dispatchMessage(@NonNull Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}
private static void handleCallback(Message message) {
    message.callback.run();
}

主线程的消息循环

Android 的主线程就是 ActivityThread,主线程的入口方法是 main,该方法内系统会通过 Looper.prepareMainLooper 来创建主线程的 Looper 和消息队列,并通过 Looper.loop 开启消息循环。用户点击等事件和我们在 Activity 类代码中设定的任务都会作为消息事务进入主线程的消息队列,再由 Looper 不断分发交付处理。

java 复制代码
public static void main(String[] args) {
    ...
    Process.setArgV0("<pre-initialized>");
    
    Looper.prepareMainLooper();
    
    ActivityThread thread = new ActivityThread();
    thread.attach(false);
    
    if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler();
    }
    
    AsyncTask.init();
    
    if (false) {
        Looper.myLooper().setMessageLogging(new LogPrinter(Log.DEBUG, "ActivityThread"));
    }
    
    Looper.loop();
    
    throw new RuntimeException("Main thread loop unexpectedly exited");
}
相关推荐
电商API_180079052473 小时前
第三方淘宝商品详情 API 全维度调用指南:从技术对接到生产落地
java·大数据·前端·数据库·人工智能·网络爬虫
晓晓莺歌3 小时前
vue3某一个路由切换,导致所有路由页面均变成空白页
前端·vue.js
一点程序3 小时前
基于SpringBoot的选课调查系统
java·spring boot·后端·选课调查系统
C雨后彩虹3 小时前
计算疫情扩散时间
java·数据结构·算法·华为·面试
2601_949809594 小时前
flutter_for_openharmony家庭相册app实战+我的Tab实现
java·javascript·flutter
Up九五小庞4 小时前
开源埋点分析平台 ClkLog 本地部署 + Web JS 埋点测试实战--九五小庞
前端·javascript·开源
快点好好学习吧4 小时前
phpize 依赖 php-config 获取 PHP 信息的庖丁解牛
android·开发语言·php
是誰萆微了承諾4 小时前
php 对接deepseek
android·开发语言·php
vx_BS813304 小时前
【直接可用源码免费送】计算机毕业设计精选项目03574基于Python的网上商城管理系统设计与实现:Java/PHP/Python/C#小程序、单片机、成品+文档源码支持定制
java·python·课程设计
2601_949868364 小时前
Flutter for OpenHarmony 电子合同签署App实战 - 已签合同实现
java·开发语言·flutter