Looper、Handler和HandlerThread

Looper、Handler和HandlerThread:保留第四版正文机制,以所有者—状态—结果合同、事件轨迹和章专属故障完成可重放验收。

学习目标

  • 能沿“Looper、Handler和HandlerThread”的用户事件解释Android组件、状态所有者、线程与销毁边界。
  • 能围绕“深入理解 Android 消息队列机制,学会用 Handler 在任意两个线程之间传递消息,并掌握 Handler 内存泄漏的经典解法。”改出一个可运行结果,并用前后状态而非组件数量验收。
  • 能在旋转、进程重建、拒权、离线或无效输入中选择适用反例,定位首个状态分叉。
  • 能用请求或消息时间线、取消、乱序与失败恢复独立重放结论,并标明第四版机制与现代targetSdk政策的边界。

为什么 Android 需要一套"消息队列"?

想象你是工厂的前台文员(主线程),忙得不可开交——接电话、接待客户、整理文件。每隔 30 秒还得去仓库跑一趟取货。你跑仓库的这 5 分钟里,电话没人接、客户干等着——整个前台停摆。老板让你改进:在仓库那边安排一个搬运工(后台线程),需要取货就把"取货申请单"塞进一个筐子。搬运工从筐子里一份份拿出来处理,货到了放回前台——前台照常运转,拿到货后才展示给客户。

这,就是 Android 消息队列机制的直觉模型。筐子(MessageQueue)是线程间通信的中转站,搬运工(Looper)不停地从筐子里取任务处理,发申请单的(Handler)负责往筐子里投递消息。

没有这套机制,Android 的多线程通信会陷入混乱——你不知道该用什么方式安全地把数据从后台传到前台。

消息队列三件套:Looper、MessageQueue、Handler

一条消息的完整生命

一条消息(Message)从发送到被处理,经历以下路径:

   Handler.sendMessage(msg)
        │
        ▼
   MessageQueue.enqueueMessage()      ← 消息入队,按时间戳排序
        │
        ▼
   Looper.loop()                      ← 无限循环,不停从队列取消息
        │
        ▼
   msg.target.dispatchMessage(msg)    ← target 就是发送它的 Handler
        │
        ▼
   Handler.handleMessage(msg)         ← 你在 handleMessage() 中写处理逻辑
可交互
一个线程(自带 Looper + MessageQueue)MessageQueue(消息队列 · 按时间戳排序)队头(先处理)队尾(新入队)↻ Looperloop()⏳ 队空 · 阻塞等待Handlerpost() / handleMessage()Messagewhat=1Messagewhat=2Messagewhat=3

第 1 / 5 步 · ① Handler.post(msg):Handler 把一条消息投进队列,从队尾入队(MessageQueue.enqueueMessage)

点击播放,看一条消息走完循环:Handler 入队 → 排队 → Looper 从队头取出 → 分发给 Handler.handleMessage 处理 → 队空则 Looper 阻塞等待。可暂停、单步、拖进度逐帧观察。

消息循环三件套:Handler 往 MessageQueue 队尾投消息,Looper.loop() 无限循环从队头取出、分发给 Handler.handleMessage 处理,队空则阻塞等待。主线程天生有 Looper(ActivityThread.main 里 prepareMainLooper + loop),所以 Handler 常用于工作线程 post 回主线程更新 UI(跨线程通信)。

每个角色各司其职:

  • :一个按时间戳排序的优先队列。只有放消息和取消息两个操作,不负责执行。每个有 Looper 的线程自动带一个。

  • :线程的消息循环引擎。Looper.prepare() 初始化消息队列,Looper.loop() 进入无限循环——不停地从消息队列里取消息,分发给目标 Handler。

  • :消息发送和处理的门面。你通过它往关联线程的消息队列投递消息,消息到达时 handleMessage() 也在那个线程执行。

主线程为什么天然支持消息队列

你在主线程创建 Handler 不需要任何准备工作——因为 Android 在主线程启动时已经帮你调了 Looper.prepareMainLooper()。这也是为什么主线程永远不会退出:Looper.loop() 在无限循环,一旦退出,App 就死了。

// 主线程直接创建 Handler,不需要 prepare
val mainHandler = Handler(Looper.getMainLooper()) { msg ->
    when (msg.what) {
        1 -> textView.text = msg.obj as String
    }
    true
}

动手看:一条消息从"发出"到"被执行"

猜一猜:在后台线程创建一个不加 Looper 的 Handler,会怎么样?切到最后一步看答案。

分步1 / 5

① 在后台线程准备消息循环

val backgroundThread = HandlerThread("BackgroundWorker").apply { start() }

HandlerThread 是 Android 帮你封装好的——自带 Looper 和 MessageQueue 的后台线程。start() 后线程进入 Looper.loop() 循环,等待消息到来。

代码逐段拆解:跨线程通信的完整案例

场景:后台下载图片,主线程显示

class DownloadActivity : AppCompatActivity() {
    private lateinit var imageView: ImageView
    private lateinit var downloadThread: HandlerThread
    private lateinit var downloadHandler: Handler
    private val mainHandler = Handler(Looper.getMainLooper())
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_download)
        imageView = findViewById(R.id.imageView)
 
        // 1. 创建并启动带 Looper 的后台线程
        downloadThread = HandlerThread("ImageDownloader").apply { start() }
        downloadHandler = Handler(downloadThread.looper) { msg ->
            if (msg.what == MSG_DOWNLOAD) {
                val url = msg.obj as String
                val bitmap = downloadImage(url)      // 后台线程下载
                // 2. 下载完切回主线程更新 UI
                mainHandler.post {
                    imageView.setImageBitmap(bitmap)
                }
            }
            true
        }
    }
 
    private fun downloadImage(url: String): Bitmap {
        val client = OkHttpClient()
        val request = Request.Builder().url(url).build()
        val bytes = client.newCall(request).execute().body?.bytes()
        return BitmapFactory.decodeByteArray(bytes, 0, bytes?.size ?: 0)
    }
 
    companion object {
        private const val MSG_DOWNLOAD = 1
    }
 
    override fun onDestroy() {
        super.onDestroy()
        downloadThread.quitSafely()  // 3. 退出消息循环
    }
}

这段代码展示了经典的"双 Handler 模式":

  • downloadHandler 挂载在 HandlerThread,负责后台下载
  • mainHandler 挂载在主线程,负责 UI 更新
  • 两个 Handler 通过消息投递完成线程切换——这就是 Android 多线程通信的基本范式

容易踩的坑

小结

  • Android 消息队列三件套:Looper(无限循环取消息)、MessageQueue(按时间排序存消息)、Handler(发送 + 处理消息)
  • 主线程自带 Looper(prepareMainLooper),通过 Handler 可以实现任意线程之间的消息传递
  • HandlerThread 是自带 Looper 的后台线程封装,适合需要排队执行的串行后台任务
  • static Handler + WeakReference<Activity> 是解决 Handler 内存泄漏的经典范式
  • handleMessage() 里做耗时操作会阻塞整个消息队列——所有消息排队等,包括 UI 绘制

练习

问题 1:“第25章 Loopers, Handlers, and HandlerThread”覆盖哪些正式节点和项目主线?

问题 2:怎样建立本页最小可执行实验?

问题 3:为什么只在正常点击路径运行不能证明完成?

问题 4:怎样设计能推翻当前实现的反例?

问题 5:从第4版语境迁移到现代目标SDK时如何控制变量?

问题 6:本页达到独立交接标准需要什么?

名词解释

名词解释

本章出现的专业名词,用大白话再讲一遍。

MessageQueue(消息队列)

每个有 Looper 的线程内部的消息存储结构。它是一个按时间戳排序的优先队列——一条消息什么时候该被执行,取决于它的 when 字段。只负责存和取,不负责执行。详见本章"消息队列三件套"一节。

Looper(消息泵)

线程的消息循环引擎。Looper.prepare() 创建 MessageQueue 并绑定当前线程,Looper.loop() 进入无限循环——不停从队列取消息,交给目标 Handler 处理。主线程的 Looper 由系统创建且永不退出。详见本章"消息队列三件套"一节。

Handler(消息收发器)

挂载在某个线程 Looper 上的消息发送和处理器。sendMessage() 往关联线程的消息队列投 Message,handleMessage() 在关联线程上执行。一个线程可以有多个 Handler。详见本章"消息队列三件套"一节。

HandlerThread

Android 封装好的自带 Looper + MessageQueue 的后台线程。不用手动 Looper.prepare() / Looper.loop()——start() 即进入消息循环。适合需要串行排队执行的后台任务。详见本章"HandlerThread"一节。

“Looper、Handler和HandlerThread”不使用未获授权的纸书正文;InformIT出版信息授权电子版完整目录只用于确认第四版32章、269个正式目录节点和时代语境,第四版官方勘误用于识别工具链变更。下列中文解释、图示、交互、代码与练习均为独立教学重写,平台行为再以Android Developers的一手文档复核。

为什么“Looper、Handler和HandlerThread”必须回到可观察状态

“Looper、Handler和HandlerThread”的学习结果不是记住类名,而是能预测“深入理解 Android 消息队列机制,学会用 Handler 在任意两个线程之间传递消息,并掌握 Handler 内存泄漏的经典解法。”在一次输入、一次重建和一次失败中的不同状态,并指出哪条Android合同产生差异。

第四版机制逐项深读

25. Loopers, Handlers, and HandlerThread

在“Looper、Handler和HandlerThread”中,“25. Loopers, Handlers, and HandlerThread”的线程安全不由Handler名称保证;共享状态、关闭顺序和回调引用仍需明确所有权。

Preparing RecyclerView to Display Images

在“Looper、Handler和HandlerThread”中,“Preparing RecyclerView to Display Images”的性能由帧时间、绑定分配与diff范围共同决定,不以单次滑动的主观流畅度验收。

Preparing to Download Bytes from a URL

在“Looper、Handler和HandlerThread”中,验证“Preparing to Download Bytes from a URL”只改变一个生命周期或外部条件,保存操作、原始日志、状态快照和用户可见断言。

Downloading Lots of Small Things

在“Looper、Handler和HandlerThread”中,“Downloading Lots of Small Things”若依赖第四版时期API,应分开说明原书机制与现代平台政策,并用Android官方文档核对迁移边界。

Assembling a Background Thread

在“Looper、Handler和HandlerThread”中,验证“Assembling a Background Thread”用快速滚动、旋转和重复请求制造乱序,确认旧任务被取消、缓存键正确且主线程无磁盘网络。

Messages and Message Handlers

在“Looper、Handler和HandlerThread”中,分析“Messages and Message Handlers”记录post线程、执行线程、队列时刻和回传目标,后台线程不能直接触碰已销毁View。

Listening to the View Lifecycle

在“Looper、Handler和HandlerThread”中,“Listening to the View Lifecycle”服务于“深入理解 Android 消息队列机制,学会用 Handler 在任意两个线程之间传递消息,并掌握 Handler 内存泄漏的经典解法。”;解释要落到用户事件、Android所有者、状态变化、线程和可观察结果,并给出一个会推翻实现的反例。

Retained Fragments

在“Looper、Handler和HandlerThread”中,“Retained Fragments”的容器替换不是简单换画面;状态保存后提交、重复tag与嵌套管理器都会改变恢复结果。

For the More Curious: Solving the Image Downloading Problem

在“Looper、Handler和HandlerThread”中,“For the More Curious: Solving the Image Downloading Problem”用于推翻“深入理解 Android 消息队列机制,学会用 Handler 在任意两个线程之间传递消息,并掌握 Handler 内存泄漏的经典解法。”的顺利路径:先写预期,再引入一个边界输入、重建或平台差异,并用状态、日志与用户结果解释首个分叉。

For the More Curious: StrictMode

在“Looper、Handler和HandlerThread”中,分析“For the More Curious: StrictMode”记录post线程、执行线程、队列时刻和回传目标,后台线程不能直接触碰已销毁View。

Challenge: Observing View LifecycleOwner LiveData

在“Looper、Handler和HandlerThread”中,“Challenge: Observing View LifecycleOwner LiveData”把Entity模式、DAO合同和SQLite文件连接成单一事实源;编译期SQL校验不能替代迁移和真实数据反例。

Challenge: Improving ThumbnailDownloader's Lifecycle Awareness

在“Looper、Handler和HandlerThread”中,“Challenge: Improving ThumbnailDownloader's Lifecycle Awareness”通过FileProvider URI把写入能力临时交给相机应用,再按目标尺寸解码;验证要覆盖无相机、取消、零字节文件、旋转和内存上限。

Challenge: Preloading and Caching

在“Looper、Handler和HandlerThread”中,验证“Challenge: Preloading and Caching”用快速滚动、旋转和重复请求制造乱序,确认旧任务被取消、缓存键正确且主线程无磁盘网络。

“Looper、Handler和HandlerThread”验收回顾

“Looper、Handler和HandlerThread”只有在请求或消息时间线、取消、乱序与失败恢复能够从相同基线再次得到相同断言时才通过;第四版机制与现代平台政策分别记录,不用新API名称掩盖旧行为。

← 上一页:HTTP与后台任务 · 下一页:搜索 →

章专属可重放状态实验

先预测“入队、后台下载、主线程回传、滚动复用与 View 销毁”发生后,ThumbnailDownloader、工作 Looper 与 viewLifecycleOwner应怎样改变请求队列、token、目标 holder、bitmap 和取消状态;再操作三个实验。第四版示例与当前 Android 政策分别记录,实验不把新 API 名称倒填为原书内容。

实验一:所有者—状态—结果合同

选择任一正式目录节点和正常/边界场景,检查它是否真的进入本章状态合同。目录标题只有同时出现在解释、可视状态和交付证据中才算覆盖。

Owner · state · observable result

Looper、Handler和HandlerThread:状态合同

用 Looper、Handler 与 HandlerThread 串行下载缩略图并安全回传主线程

验证场景

第四版正式目录节点

looper-handler · 正常任务

25. Loopers, Handlers, and HandlerThread固定 SDK、设备配置和初始状态,触发“入队、后台下载、主线程回传、滚动复用与 View 销毁”

状态所有者ThumbnailDownloader、工作 Looper 与 viewLifecycleOwner
受控状态请求队列、token、目标 holder、bitmap 和取消状态
触发事件入队、后台下载、主线程回传、滚动复用与 View 销毁

冻结入口:25. Loopers, Handlers, and HandlerThread

记录ThumbnailDownloader、工作 Looper 与 viewLifecycleOwner的初始请求队列、token、目标 holder、bitmap 和取消状态

观察:post/execute 线程、token、URL、holder ID、取消与绑定日志中的“25. Loopers, Handlers, and HandlerThread”轨迹

预期:由ThumbnailDownloader、工作 Looper 与 viewLifecycleOwner提交请求队列、token、目标 holder、bitmap 和取消状态,并持续满足“回调只更新仍绑定同一请求的可见 View,销毁后队列可取消”

实验二:事件与生命周期轨迹

沿五次转换逐步执行“入队、后台下载、主线程回传、滚动复用与 View 销毁”。每一步只允许ThumbnailDownloader、工作 Looper 与 viewLifecycleOwner按职责提交状态,并持续核对“回调只更新仍绑定同一请求的可见 View,销毁后队列可取消”。

Deterministic event replay

Looper、Handler和HandlerThread:事件轨迹

选择一次状态转换1 / 5

不变量:回调只更新仍绑定同一请求的可见 View,销毁后队列可取消

交付证据:post/execute 线程、token、URL、holder ID、取消与绑定日志

实验三:章专属反例与同输入恢复

注入“Holder 已复用给新 URL,旧下载回调把错误图片写入当前行”,保存第一个偏离点;撤销后以完全相同的 SDK、设备状态和用户事件重放。只有post/execute 线程、token、URL、holder ID、取消与绑定日志一起恢复才算修复。

Fault · cancel · restore

Looper、Handler和HandlerThread:反例与恢复

故障:Holder 已复用给新 URL,旧下载回调把错误图片写入当前行

1. 冻结输入一致

第 1 次使用相同 SDK、设备配置、初始状态与用户事件

2. 注入边界一致

保持正常输入不变,仅注入“Holder 已复用给新 URL,旧下载回调把错误图片写入当前行”

3. 检查所有者一致

回调只更新仍绑定同一请求的可见 View,销毁后队列可取消

4. 核对结果一致

post/execute 线程、token、URL、holder ID、取消与绑定日志

讨论

评论区加载中…