10. 线程与消息机制

2025/2/22 Android 开发基础

前面章节写过不少"点击按钮 → 弹 Toast"的代码,但一直没有回答一个基础问题:耗时操作(网络请求、文件读写、数据库查询)应该在哪里执行? 答案是:不能在主线程执行,必须在工作线程。而工作线程做完事,又要把结果送回主线程更新 UI。这一来一回,就构成了 Android 异步编程的核心命题。

本章(异步篇)是 Android 基础的最后一块重要拼图,共四节:

  • 10.00 索引与线程体系概述(本文):主线程与工作线程的职责边界、为什么 UI 只能主线程更新、异步方案全景图
  • 10.01 Handler 消息机制原理:Looper / MessageQueue / Message / Handler 四大角色,消息的流转、阻塞与唤醒机制,典型陷阱
  • 10.02 线程与线程池:Thread 基础、线程池的参数与工作流程、为什么不推荐 Executors、最佳实践
  • 10.03 ANR 与主线程规则:ANR 的触发条件与定位方法、主线程操作清单、耗时任务的正确处理

# 一、Android 线程体系总览

Android 线程体系总览示意图

两类线程,两种职责:

线程 创建时机 职责 红线
主线程 / UI 线程 进程启动时由系统创建 UI 绘制、事件分发(08.01)、生命周期回调 禁止耗时操作(会触发 ANR,见 10.03)
工作线程 开发者按需创建 网络请求、磁盘 IO、数据计算、图片解码 禁止直接更新 UI

一个进程只有一个主线程,Activity.onCreate、onDraw、dispatchTouchEvent 等全部在它上面执行。主线程默认持有唯一的 Looper(消息循环),这也是 Handler 机制能"送结果回主线程"的基础。

# 二、为什么 UI 只能主线程更新

Android 的 UI 工具包(ViewRootImpl 及各类控件)不是线程安全的。多线程并发修改同一棵 View 树,会产生不可预期的竞态(如绘制到一半布局被另一个线程改掉)。Android 的解法不是给每个控件加锁(加锁开销大、易死锁),而是单线程模型:所有 UI 操作都收敛到主线程,天然串行,无需加锁。

这套约束在代码里被强制检查:

void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}

例外:ProgressBar 等明确文档标注线程安全的控件除外;SurfaceView 的绘制发生在独立的 Surface 上,可在子线程绘制(相机预览、视频渲染场景,见 16.04 视频播放与录制)。

所以异步编程的固定范式是:工作线程干耗时活 → Handler 切回主线程 → 主线程更新 UI。

# 三、异步方案全景

方案 定位 适用场景 本系列态度
Thread + Handler 最原始的组合 学习原理、一次性简单任务 ✅ 必须掌握(是其他方案的底层)
HandlerThread 自带 Looper 的线程 串行后台任务(如相机操作队列) ✅ 基础掌握
ThreadPoolExecutor 线程复用、并发控制 绝大多数后台任务 ✅ 日常首选(10.02 详讲)
AsyncTask 已废弃的封装 不再使用 ❌ 只需知道它已废弃及原因
IntentService 已废弃(API 30+) 不再使用,用 WorkManager 替代 ❌ 了解即可
协程(Coroutines) Kotlin 的挂起式并发 Kotlin 项目的现代方案 ⏳ 20 Kotlin 基础 章节展开

本章路线:先把 Handler 机制(10.01)和线程池(10.02)的原理吃透,它们是 AsyncTask、协程调度、图片加载库内部(Glide 的线程池)等一切上层异步方案的共同地基。

# 结束

本节是异步篇的索引与总览:明确了主线程/工作线程的职责边界(耗时去工作线程、更新 UI 回主线程)、UI 单线程模型的原因(View 体系非线程安全,用单线程收敛替代加锁)、异步方案全景与学习路线。下一节进入本章核心:10.01 Handler 消息机制原理。