
最近看了一些线程相关的文章,有感而发,想顺着时间线(并不是严格意义上的语言特性时间线,更多的是开发者使用时间线)看看 Android 里线程相关的代码是怎么一步步演变过来的。于是就有了这篇"牢骚文"。
为什么标题是《发展shi》而不是《发展史》?
因为早年间 Android 对多线程后台任务的处理,说实话,就是一坨 shi。
不过往根上说,Android 线程问题本质上就围绕这么几个点:
- 不要阻塞主线程;
- 后台任务完成后要能回到主线程;
- 线程资源不能失控;
- 异步流程要能组织起来;
- 任务要能取消;
- 生命周期要能兜住。
甚至任何多线程系统解决的问题,也会包含如上几点。
早期我们用 Thread + Handler 做线程通信,后来有了 HandlerThread、AsyncTask、线程池。业务复杂以后,RxJava 开始流行。再到 Kotlin 成为主力语言,Coroutines 又把异步代码的写法重新整理了一遍。
我个人认为,只有从 RxJava 开始,才算真正解决了上面这些问题 ------ 不只是让线程能干活儿,而是能没有心理负担地干活儿,同时多线程产生的技术债务也少了很多。
好,下面正式开始,一起看看这些年 Android 线程技术是怎么一步步进化过来的。
一、Thread:疯狂原始人
Android 应用从一开始就有一个基本规则:不要在主线程执行耗时任务。
不过,你早期如果非要在主线程做耗时任务,其实也没什么大问题,只是有点卡而已,但 Android 后期不一样了,如果你在主线程进行 IO 操作,就会直接抛出异常了。
在 Android 的架构中,主线程负责界面绘制、事件分发和生命周期回调。
如果在主线程里做网络请求、文件读写、图片解码,很容易造成卡顿,严重时会触发 ANR。
而当时,我们想到的,最直接的解决方式就是开一个线程。
核心原理
Thread 的工作方式很简单:创建一个新的执行流,把耗时任务放到这个执行流里运行。
它解决的是:耗时任务不要阻塞主线程
但它没有解决:后台线程执行完以后,怎么安全地通知主线程?
Android 的 View 体系不是线程安全的,后台线程不能直接更新 UI。所以即便使用了 Thread,最终仍然需要某种方式把结果切回主线程。
关键代码
kotlin
private val mainHandler = Handler(Looper.getMainLooper())
private var workerThread: Thread? = null
fun startWork() {
workerThread = Thread {
val result = loadData()
// 后台线程执行完成后,切回主线程
mainHandler.post {
textView.text = result
}
}
workerThread?.start()
}
fun destroy() {
workerThread?.interrupt()
workerThread = null
mainHandler.removeCallbacksAndMessages(null)
}
private fun loadData(): String {
Thread.sleep(1000)
return "Result from Thread"
}
这段代码包含了几个关键点:
Thread创建后台任务Handler.post切回主线程interrupt尝试中断任务removeCallbacksAndMessages清理未执行的 UI 回调
短板
Thread 的问题很明显。
第一,每次任务都创建新线程,线程无法复用。
第二,没有任务队列。多个任务要排队执行时,需要自己维护。
第三,取消能力比较弱。interrupt() 只是发出中断信号,任务代码本身还要配合处理(实际上这个不仅仅是 Thread 的问题,在基于 JVM 的语言中,关于线程的取消,基本都是协作的,需要代码互相配合)。
第四,不感知 Android 生命周期。页面销毁后,线程可能还在跑。
所以 Thread 适合简单、一次性的后台任务,不适合复杂业务。
适用场景
一句话:Thread 解决了后台任务的问题。
现在直接使用 Thread 的场景已经不多了。它更适合简单 Demo、底层实验代码,或者非常明确的一次性后台任务。普通 Android 业务代码里,不建议直接使用 Thread 作为主要异步方案。
Thread 解决了"任务不要跑在主线程"的问题,但没有很好地解决"线程之间怎么通信"。于是,Handler 出场了。
二、Handler:消息机制的核心
如果单纯使用 Thread,很快会遇到一个实际问题:后台线程拿到了结果,怎么交给主线程?
Android 给出的标准方案是 Handler。
Handler 不是简单的线程切换工具,它是 Android 消息机制的一部分。它可以把消息或者任务投递到某个线程的消息队列里,由那个线程执行。
核心原理
一个 Handler 绑定一个 Looper。
一个 Looper 对应一个线程的消息循环。
主线程天然就有 Looper,所以我们经常这样写:
kotlin
val mainHandler = Handler(Looper.getMainLooper())
这表示创建一个绑定到主线程的 Handler。之后通过它发送的消息或者 Runnable,都会在主线程执行。
可以简单理解为:
Handler负责发送消息;MessageQueue负责保存消息;Looper负责取出消息;- 线程负责执行消息。
关键代码
使用 post:
kotlin
private val mainHandler = Handler(Looper.getMainLooper())
Thread {
val result = loadData()
mainHandler.post {
textView.text = result
}
}.start()
使用 Message:
kotlin
private val mainHandler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
when (msg.what) {
MSG_RESULT -> textView.text = msg.obj as String
}
}
}
Thread {
val result = loadData()
val message = Message.obtain().apply {
what = MSG_RESULT
obj = result
}
mainHandler.sendMessage(message)
}.start()
销毁时要清理消息:
kotlin
override fun onDestroy() {
super.onDestroy()
mainHandler.removeCallbacksAndMessages(null)
}
这里的线程通信过程是:
text
后台线程 sendMessage / post
↓
主线程 MessageQueue
↓
Looper 取出消息
↓
Handler 执行回调
↓
更新 UI
短板
Handler 解决了线程向主线程发送消息的问题,但它不是完整的异步框架。
它仍然有几个问题。
第一,Handler 不负责创建后台线程。后台任务怎么跑,还是要自己处理。
第二,容易引发内存泄漏。尤其是延迟消息还在队列里,而消息间接持有了 Activity。
第三,它只负责消息分发,不负责复杂任务编排。多个任务串行、并行、合并结果、错误处理,都要自己写。
适用场景
一句话:Handler 解决了后台任务向主线程发送消息的问题。
Handler 现在仍然有价值,尤其适合:
- 切回主线程;
- 发送延迟任务;
- 和
Looper/MessageQueue直接打交道; - Framework 或 SDK 内部实现。
但是在普通业务层,不建议把大量异步逻辑都写成 Handler.post。
Handler 解决了线程通信问题,但如果我们希望后台线程也能长期接收任务,就需要一个带消息队列的后台线程。这就是 HandlerThread。
三、HandlerThread:带 Looper 的后台线程
普通 Thread 执行完任务就结束了。
如果我们希望有一个长期存在的后台线程,能够不断接收任务,就需要自己写循环、队列和退出逻辑。
HandlerThread 帮我们封装了这件事。
它本质上是一个自带 Looper 的后台线程。启动以后,可以通过它的 Looper 创建 Handler,然后不断往这个后台线程投递任务。
核心原理
可以把 HandlerThread 理解为 Thread + Looper + MessageQueue。
它适合处理串行后台任务。
所有投递到它的任务,都会在同一个后台线程里按顺序执行。
关键代码
kotlin
private val mainHandler = Handler(Looper.getMainLooper())
private val workerThread = HandlerThread("worker-thread").apply {
start()
}
private val workerHandler = Handler(workerThread.looper)
fun startWork() {
workerHandler.post {
val result = loadData()
mainHandler.post {
textView.text = result
}
}
}
fun destroy() {
workerHandler.removeCallbacksAndMessages(null)
mainHandler.removeCallbacksAndMessages(null)
workerThread.quitSafely()
}
这段代码的核心流程是:
text
主线程提交任务
↓
workerHandler.post
↓
HandlerThread 后台线程执行
↓
mainHandler.post
↓
主线程更新 UI
短板
HandlerThread 比普通 Thread 更适合处理连续任务,但它仍然比较底层。
第一,它是单线程串行执行。如果某个任务耗时很长,后面的任务都会被堵住。
第二,它没有线程池能力,不适合大量并发任务。
第三,生命周期仍然要手动管理。页面销毁时,要主动移除消息并退出线程。
第四,业务代码写多以后,还是会出现大量 post 嵌套,可读性不好。
适用场景
一句话:HandlerThread 解决了 Thread 只能执行一次的问题,使用它,你能让 Thread 一直工作。
HandlerThread 适合一些需要后台串行队列的场景,比如:
- 相机模块;
- 蓝牙模块;
- 传感器处理;
- SDK 内部工作线程;
- 需要长期存在的后台串行任务。
它不适合作为普通页面业务的主要异步方案。
HandlerThread 让后台线程也有了消息队列,但写业务时仍然有不少样板代码。为了简化"后台执行 + 主线程回调"这个常见流程,Android 后来提供了 AsyncTask。
四、AsyncTask:官方简化方案
如果你在早期 Android 项目里大量的使用了 Thread + Handler 的模式,那么你会发现,很多异步代码都是固定套路:
- 任务开始前更新 UI;
- 后台线程执行任务;
- 执行过程中更新进度;
- 执行完成后回到主线程。
如果每次都用 Thread + Handler 写,除了具体的后台任务代码以外,剩下的全是模版代码。
AsyncTask 就是为了解决这个问题出现的。
它把一个异步任务拆成几个阶段:
onPreExecute:主线程,任务开始前;doInBackground:后台线程,执行耗时任务;onProgressUpdate:主线程,更新进度;onPostExecute:主线程,处理结果。
核心原理
AsyncTask 本质上是对线程池和 Handler 的封装。
开发者只需要实现几个回调方法,它内部负责后台执行和主线程回调。
当年它很适合入门教学,因为它把异步任务包装成了一个固定模板。
关键代码
kotlin
@Suppress("DEPRECATION")
class LoadTask : AsyncTask<Unit, Int, String>() {
override fun onPreExecute() {
textView.text = "Loading..."
}
override fun doInBackground(vararg params: Unit?): String {
for (i in 1..5) {
if (isCancelled) return "Cancelled"
Thread.sleep(300)
// 从后台线程通知主线程更新进度
publishProgress(i * 20)
}
return "Result from AsyncTask"
}
override fun onProgressUpdate(vararg values: Int?) {
progressBar.progress = values.first() ?: 0
}
override fun onPostExecute(result: String) {
textView.text = result
}
}
使用和销毁:
kotlin
private var task: LoadTask? = null
fun startWork() {
task = LoadTask()
task?.execute()
}
fun destroy() {
task?.cancel(true)
task = null
}
它的线程通信方式是:
text
doInBackground 执行后台任务
↓
publishProgress 通知进度
↓
onProgressUpdate 更新进度
doInBackground 返回结果
↓
onPostExecute 更新 UI
短板
AsyncTask 的问题在真实项目里非常明显。
第一,它和生命周期没有真正绑定。Activity 销毁以后,任务可能还在执行。
第二,它容易持有 Activity 引用,导致内存泄漏。
第三,它不适合复杂任务编排。多个任务串行、并行、合并结果,写起来都不自然。
第四,它的执行策略在 Android 历史上发生过变化,有过串行和并行行为差异,容易踩坑。
当然,现在是 2026 年了,我几乎可以肯定你不会再用 AsyncTask 了。

适用场景
一句话:AsyncTask 解决了样板代码问题。
现在 AsyncTask 的主要价值是阅读老代码,对,也就是让你懂得什么是 Read The Fucking Source 原则!
AsyncTask 没有真正解决线程资源管理和复杂任务调度问题。随着业务增长,开发者开始更认真地管理线程资源,于是线程池成为了更常见的方案。
五、Executors 线程池:资源可控

直接创建线程的成本很高。
如果一个页面里有图片解码、文件读写、数据库查询、日志上传,每个任务都创建一个新线程,线程数量很快就会失控。
线程池解决的就是这个问题:不要每次任务都创建线程,而是复用一组线程。
核心原理
线程池可以简单理解为:任务队列 + 一组工作线程。
提交任务后,任务进入队列。空闲线程从队列里取任务执行。执行完以后,线程不销毁,而是继续等待下一个任务。
它比直接使用 Thread 更适合管理资源。
关键代码
kotlin
private val mainHandler = Handler(Looper.getMainLooper())
private val executor = Executors.newFixedThreadPool(2)
private var future: Future<*>? = null
fun startWork() {
future = executor.submit {
val result = loadData()
mainHandler.post {
textView.text = result
}
}
}
fun destroy() {
future?.cancel(true)
executor.shutdownNow()
mainHandler.removeCallbacksAndMessages(null)
}
这里的职责划分很清楚:
ExecutorService负责后台任务执行;Handler负责切回主线程;Future负责取消任务;shutdownNow负责关闭线程池。
短板
线程池解决了线程复用问题,但没有解决异步代码组织问题。
第一,线程池只关心任务执行,不关心任务之间的关系。
第二,线程切换仍然要手动处理,通常还要配合 Handler。
第三,取消依然需要任务代码配合。
第四,复杂链路会变成回调嵌套。比如先请求用户信息,再请求订单,再写数据库,再更新 UI,用线程池写起来并不舒服。
适用场景
一句话:Executors 解决了线程复用问题。
Executors 到现在仍然有价值,尤其适合:
- Java 代码;
- SDK 内部线程管理;
- 自定义线程池;
- CPU 密集型任务隔离;
- IO 密集型任务隔离;
- 老项目维护。
但对于现代 Android 页面业务来说,线程池通常应该藏在底层实现里,而不是暴露到 UI 层。
线程池解决了"线程资源怎么管理"的问题,但没有解决"复杂异步流程怎么组织"的问题。这个阶段,RxJava 开始流行。
六、RxJava:几乎完美的答案

当业务复杂以后,开发者真正头疼的已经不是开线程,而是组织异步流程。
比如:
text
登录
↓
请求用户信息
↓
根据用户信息请求订单
↓
写入数据库
↓
更新 UI
用 Thread、Handler、Executors 当时是能写的,但是代码会很散,开发者自己写的还好,毕竟熟悉业务,这要是新来个人,根本不知道这是哪儿到哪儿,而且,错误处理和取消也麻烦。
RxJava 解决了这个问题,当年在 Android 里流行,就是因为它把异步任务组织成了一条数据流。
核心原理
RxJava 的核心模型可以简单理解为:
text
数据源
↓
操作符处理
↓
订阅者接收结果
线程切换由 Scheduler 控制:
kotlin
subscribeOn(Schedulers.io())
observeOn(AndroidSchedulers.mainThread())
优不优美!
这两段代码的含义是:
subscribeOn:上游任务在哪个线程执行observeOn:下游结果在哪个线程接收
这比手写 Executor + Handler 更适合表达连续异步流程。
关键代码
kotlin
private val disposables = CompositeDisposable()
fun startWork() {
val disposable = Single.fromCallable {
loadData()
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ result ->
textView.text = result
},
{ error ->
textView.text = error.message
}
)
disposables.add(disposable)
}
fun destroy() {
disposables.clear()
}
复杂一点的链式任务:
kotlin
api.login()
.flatMap { token ->
api.getUser(token)
}
.flatMap { user ->
api.getOrders(user.id)
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ orders -> showOrders(orders) },
{ error -> showError(error) }
)
它的线程和数据流是:
text
subscribeOn 指定后台线程
↓
flatMap 组织异步链路
↓
observeOn 切回主线程
↓
subscribe 接收结果
短板
RxJava 很强,但也很重。
第一,刚开始学习成本高。操作符很多,新人容易写出能跑但不好维护的代码。
第二,调试成本高。链路一长,再加上多次线程切换,问题定位不如普通同步代码直观。如果你尝试在 RxJava 的调用链中去下断点调试,你就知道我说的什么意思了!
第三,生命周期需要手动管理。忘记 dispose(),就可能造成泄漏或者无效回调。
第四,错误处理有负担。一个 onError 可能终止整条链路。
第五,简单场景下显得过度设计。一个普通网络请求,没有必要引入很复杂的响应式链路。
第六,容易滥用。虽然这个不能怪 RxJava,但是我看到很多老代码中,那些 Rx 老手们为了一个小小的线程切换任务(尤其是切换 UI 线程)去使用 RxJava,导致代码中全是 RxJava。
适用场景
一句话:RxJava 解决了任务编排问题。
RxJava 现在虽然用的不多了,但是它依然有价值,如果你之前用过较长时间的 RxJava,那么你在看到后面的 Kotlin Flow 的时候,你就会有一种熟悉的感觉!
RxJava 解决了异步流程编排问题,但也带来了新的复杂度。Kotlin 成为 Android 主力语言以后,协程提供了另一种思路:把异步代码重新写回接近同步的样子。
题外话

如果你去了解一下 RxJava,你会发现,RxJava 是对响应式扩展(Reactive Extensions,称之为 ReactiveX)规范的 Java 实现,该规范还有其他语言实现:RxJS、Rx.Net、RxScala、RxSwift 等等,甚至,还有 RxKotlin。
也就是说,ReactiveX 定义了规范,其他语言实现规范即可。
七、协程:像同步代码一样写异步代码
RxJava 把异步流程写成链式调用,能力很强,但很多业务其实并没有复杂到必须使用响应式框架,或者说,链式调用本身对于代码理解来讲,可能就是核心问题。
考虑到大多数时候,我们只是想写:
kotlin
val user = api.getUser()
val orders = api.getOrders(user.id)
showOrders(orders)
同时,这些操作不能阻塞主线程。
Kotlin 协程解决的就是这个问题:让异步代码尽量保持同步代码的阅读方式,同时提供取消、线程切换和生命周期管理能力。
核心原理
协程不是线程。
它运行在线程之上,可以在挂起点暂停,等结果回来后再继续执行。挂起期间不会阻塞线程。
几个核心概念可以这样理解:
CoroutineScope:协程的作用域,决定协程活多久;Job:具体任务,可以取消;Dispatcher:决定任务在哪类线程上执行;suspend:表示函数可以挂起,但不阻塞线程。
对应到 Android 里,最常用的是:
viewModelScope:跟ViewModel生命周期绑定;lifecycleScope:跟LifecycleOwner生命周期绑定;Dispatchers.IO:适合 IO 任务;Dispatchers.Default:适合计算任务;Dispatchers.Main:主线程。
关于协程,我写过很多文章,这里简单的列举几篇:
小伙伴们有空的话可以去看看,能加深不少对于协程的理解。
关键代码
ViewModel 中启动任务:
kotlin
class UserViewModel : ViewModel() {
private val _uiState = MutableStateFlow("Idle")
val uiState: StateFlow<String> = _uiState
fun load() {
viewModelScope.launch {
_uiState.value = "Loading"
val result = withContext(Dispatchers.IO) {
loadData()
}
_uiState.value = result
}
}
private suspend fun loadData(): String {
delay(1000)
return "Result from Coroutines"
}
}
Activity 或 Fragment 收集状态:
kotlin
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
textView.text = state
}
}
}
取消逻辑不再需要到处手写。
text
ViewModel 清理
↓
viewModelScope 自动取消内部协程
Lifecycle 进入 STOPPED
↓
repeatOnLifecycle 停止收集
Lifecycle 回到 STARTED
↓
重新开始收集
再来一个更加贴近真实业务的协程链路:
kotlin
viewModelScope.launch {
try {
val user = withContext(Dispatchers.IO) {
api.getUser()
}
val orders = withContext(Dispatchers.IO) {
api.getOrders(user.id)
}
_uiState.value = UiState.Success(orders)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message)
}
}
这段代码看起来是顺序执行,但不会阻塞主线程。
短板
协程是现代 Android 的主流方案,但它不是魔法,也不是万能药。
第一,协程不等于不会阻塞线程。如果在主线程协程里直接执行阻塞 IO,一样会卡。
第二,取消需要代码配合。大部分挂起函数支持取消,但如果内部是不可取消的阻塞调用,取消不会立刻生效。
第三,不能乱用 GlobalScope。它绕开了结构化并发,容易造成任务失控。
第四,异常处理需要理解清楚。launch、async、supervisorScope 的异常传播并不完全一样。
第五,Flow 有学习成本。冷流、热流、共享、生命周期收集,都需要认真理解。
适用场景
一句话:梭哈!这里说有点是多余的,光一个像同步代码一样写异步代码,就足够证明协程设计的有多优秀了。
现代 Android 项目里,协程基本应该是默认选择。
协程没有消灭线程,而是让大部分业务代码不再直接面对线程。
一点想法
回头看这段发展脉络,会发现每种技术都是层层递进,不断的解决项目问题的。
Thread 解决了耗时任务不阻塞主线程,Handler 解决了线程间通信,HandlerThread 让后台线程能长期运行,AsyncTask 简化了样板代码,Executors 实现了线程复用。
到了 RxJava,开始解决异步链路组织的问题;协程更进一步,把生命周期管理也收编了。
你会发现一个规律:RxJava 之前,工具解决的是"能不能用"的问题;RxJava 之后,开始解决"好不好用"的问题。
今天的 Android 开发,协程基本可以一路梭哈到底。
了解这段历史,倒不是为了背 API 替换表,而是理解开发者是怎么一步步驯服异步复杂度的。从 Thread 到 Kotlin Coroutines,变的不只是写法,而是异步代码的组织方式。
这可能也是现代 Android 异步开发最重要的变化吧。