Android 线程发展shi

最近看了一些线程相关的文章,有感而发,想顺着时间线(并不是严格意义上的语言特性时间线,更多的是开发者使用时间线)看看 Android 里线程相关的代码是怎么一步步演变过来的。于是就有了这篇"牢骚文"。

为什么标题是《发展shi》而不是《发展史》?

因为早年间 Android 对多线程后台任务的处理,说实话,就是一坨 shi。

不过往根上说,Android 线程问题本质上就围绕这么几个点:

  • 不要阻塞主线程;
  • 后台任务完成后要能回到主线程;
  • 线程资源不能失控;
  • 异步流程要能组织起来;
  • 任务要能取消;
  • 生命周期要能兜住。

甚至任何多线程系统解决的问题,也会包含如上几点。

早期我们用 Thread + Handler 做线程通信,后来有了 HandlerThreadAsyncTask、线程池。业务复杂以后,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

ThreadHandlerExecutors 当时是能写的,但是代码会很散,开发者自己写的还好,毕竟熟悉业务,这要是新来个人,根本不知道这是哪儿到哪儿,而且,错误处理和取消也麻烦。

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"
    }
}

ActivityFragment 收集状态:

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。它绕开了结构化并发,容易造成任务失控。

第四,异常处理需要理解清楚。launchasyncsupervisorScope 的异常传播并不完全一样。

第五,Flow 有学习成本。冷流、热流、共享、生命周期收集,都需要认真理解。

适用场景

一句话:梭哈!这里说有点是多余的,光一个像同步代码一样写异步代码,就足够证明协程设计的有多优秀了。

现代 Android 项目里,协程基本应该是默认选择。

协程没有消灭线程,而是让大部分业务代码不再直接面对线程。

一点想法

回头看这段发展脉络,会发现每种技术都是层层递进,不断的解决项目问题的。

Thread 解决了耗时任务不阻塞主线程,Handler 解决了线程间通信,HandlerThread 让后台线程能长期运行,AsyncTask 简化了样板代码,Executors 实现了线程复用。

到了 RxJava,开始解决异步链路组织的问题;协程更进一步,把生命周期管理也收编了。

你会发现一个规律:RxJava 之前,工具解决的是"能不能用"的问题;RxJava 之后,开始解决"好不好用"的问题。

今天的 Android 开发,协程基本可以一路梭哈到底。

了解这段历史,倒不是为了背 API 替换表,而是理解开发者是怎么一步步驯服异步复杂度的。从 ThreadKotlin Coroutines,变的不只是写法,而是异步代码的组织方式。

这可能也是现代 Android 异步开发最重要的变化吧。

相关推荐
李斯维2 小时前
Jetpack 可观察数据容器 LiveData 的高级用法
android·android jetpack·androidx
天才少年曾牛2 小时前
Android新增服务添加selinux权限
android·java·frameworks
knighthood20012 小时前
ros2-quick-runner插件v0.0.4版本发布
android·java·开发语言
故渊at2 小时前
第五板块:Android 系统服务与电源管理 | 第十八篇:Battery Service 与 电量统计(Fuel Gauge)算法
android·算法·battery·电源·电池·电源管理·电量统计
2501_915909062 小时前
iOS IPA文件反编译与打包操作方法详解
android·ios·小程序·https·uni-app·iphone·webview
问心无愧051310 小时前
ctf show web入门111
android·前端·笔记
ha_lydms17 小时前
AnalyticDB分区、分布键性能优化
android·大数据·分布式·性能优化·分布式计算·分区·analyticdb
星辰17 小时前
Ijkplayer重新编译支持h264裸流
android
测试开发-学习笔记18 小时前
Android studio安装
android·ide·android studio