很多 Android 开发者学习协程的成长路线几乎都一样。
从最开始:
能跑就行。
到最后:
开始设计异步边界、调度策略和数据流模型。
这其实是一个非常明显的认知升级过程。
第一境:launch(IO) 满天飞
刚接触协程的时候,大多数人的代码长这样:
kotlin
button.setOnClickListener {
CoroutineScope(Dispatchers.IO).launch {
val user = api.getUser()
withContext(Dispatchers.Main) {
tv.text = user.name
}
}
}
或者:
kotlin
GlobalScope.launch(Dispatchers.IO) {
}
项目里到处都是:
text
launch(IO)
launch(IO)
launch(IO)
launch(IO)
特点:
- 谁耗时谁 IO
- 谁更新 UI 谁 Main
- ViewModel、UseCase、Repository 全都切线程
- 每层都觉得自己应该负责线程
结果:
text
Main
↓
IO
↓
IO
↓
IO
↓
Main
线程切换次数比业务逻辑还多。
这一阶段的核心认知:
协程 = 开线程。
实际上:
协程从来不仅仅线程管理框架。
第二境:withContext() 满天飞
kotlin
suspend fun loadUser(): User {
return withContext(Dispatchers.IO) {
api.getUser()
}
}
然后:
kotlin
suspend fun queryUser(): User {
return withContext(Dispatchers.IO) {
dao.query()
}
}
再然后:
kotlin
suspend fun updateUser() {
withContext(Dispatchers.IO) {
dao.insert()
}
}
最后项目变成:
text
withContext(IO)
withContext(IO)
withContext(IO)
withContext(IO)
甚至:
kotlin
withContext(IO) {
repository.load()
}
repository:
withContext(IO) {
remote.load()
}
remote:
withContext(IO) {
retrofit.load()
}
线程路径:
text
Main
↓
IO-1
↓
IO-2
↓
IO-3
虽然比 launch(IO) 好一点,
但本质还是:
把协程当线程切换框架使用。
第三境:开始定义 suspend
这一阶段开始意识到:
text
异步能力应该向下沉。
开始写:
kotlin
interface UserRepository {
suspend fun getUser(): User
}
而不是:
kotlin
fun getUser(callback: (User) -> Unit)
或者:
kotlin
fun getUser(): Deferred<User>
ViewModel:
kotlin
viewModelScope.launch {
val user = repository.getUser()
}
Repository:
kotlin
override suspend fun getUser(): User {
return api.getUser()
}
此时开始理解:
suspend 描述的是异步能力。
也开始理解:
text
同步写法
异步执行
这是协程表达异步的方式。
第四境:开始理解 Main Safe
继续深入以后会发现:
真正负责线程的,
应该是:
text
Data Layer
而不是:
text
ViewModel
例如:
kotlin
override suspend fun getUser(): User {
return dao.query()
}
于是上层变成:
kotlin
viewModelScope.launch {
val user = repository.getUser()
}
ViewModel 不需要知道:
- Room
- Retrofit
- 文件 IO
- ContentProvider
- Binder
是否阻塞。
这就是:
text
Main Safe
调用方无需关心线程。
这其实已经进入架构设计阶段了。
第五境:开始理解"不是所有 suspend 都需要 IO"
很多人最终会发现:
下面这些其实根本不需要:
kotlin
withContext(IO) {
retrofitApi.load()
}
因为:
text
Retrofit suspend
OkHttp Dispatcher
本身已经异步。
Room:
text
Room Query Executor
DataStore:
text
DataStore Scope
Coil:
text
ImageLoader Dispatcher
很多 SDK:
text
自身线程池
这时候再包一层:
kotlin
withContext(IO)
往往只是:
text
Main
↓
IO Dispatcher
↓
SDK线程池
多了一层调度中转站。
这也是很多人看到:
text
DefaultDispatcher-worker-1
DefaultDispatcher-worker-2
OkHttp Dispatcher
Room Executor
线程数量疯狂增长的原因。
第六境:开始设计执行模型,而不是 Dispatcher
真正高级的协程开发者已经很少讨论:
text
这里该不该 IO?
而是在思考:
text
谁拥有执行权?
例如:
Application:
kotlin
private val appExecutor =
Executors.newFixedThreadPool(4)
val applicationScope =
CoroutineScope(
SupervisorJob() +
appExecutor.asCoroutineDispatcher()
)
所有业务任务:
kotlin
applicationScope.launch {
}
真正阻塞:
kotlin
withContext(ioDispatcher) {
}
整个 App:
- 最大线程数可控
- 调度模型可预测
- 不会出现几十个 worker 线程
这已经开始进入:
text
执行模型设计
而不是:
text
线程切换技巧
第七境:忘掉 Dispatcher,开始思考边界
最后会发现:
协程真正重要的其实只有三个问题:
谁负责生命周期?
text
viewModelScope
applicationScope
lifecycleScope
谁负责取消?
text
Job
SupervisorJob
谁负责执行?
text
Retrofit
Room
DataStore
AppExecutor
而:
text
Dispatchers.IO
Dispatchers.Default
反而越来越少出现。
最终境界
从:
text
协程 = 开线程
到:
text
协程 = 异步编程
再到:
text
协程 = 生命周期管理
最后到:
text
协程 = 执行模型设计
这基本就是一个 Android 开发者从初级到高级,再到架构师的协程认知升级路线。
很多人学了几年协程,依然停留在:
text
这个接口要不要切 IO?
而真正的问题其实早就变成:
text
这个任务到底应该由谁执行?
谁应该拥有调度权?
谁应该拥有生命周期?
当开始问这些问题的时候,说明已经开始真正理解协程了。
参考
从 Callback 到 Coroutines:Android 异步并发方案的演进
"结构化"这个词,本质上就是------把混乱的东西变成有组织、有规则、有边界的东西
从"调用方的如履薄冰"到"接口的天然语义":Room/DataStore/Retrofit 的启示
现代 Android 官方为什么更推荐 Repository 暴露 suspend fun,而不是在内部 launch
# Android 架构指南之Data 层不要再暴露 start/stop 了:用 Flow 接管生命周期
别再 launch(IO) 了:协程线程切换的 3隐藏反模式
从送外卖看Android Clean架构:为什么老板不需要知道外卖员开什么车