为什么用协程

Android 主线程不能做耗时操作(网络、IO),以前用回调、RxJava,代码很容易变成”回调地狱”。Kotlin 协程用近乎同步的写法处理异步,并且能在 Activity 销毁时自动取消,天然防止内存泄漏。

一、基础概念

1
2
3
4
5
6
7
// 在协程里请求网络数据
fun loadUser() {
viewModelScope.launch { // 在 ViewModel 的作用域里启动协程
val user = api.getUser() // suspend 函数,自动切后台执行
userLiveData.value = user // 回到主线程更新 UI
}
}

三个关键词:

  • launch:启动一个协程,不等待结果;
  • async:启动协程并返回 Deferred,可以 await() 拿结果;
  • suspend:标记挂起函数,只能在协程里调用。

二、Dispatchers:线程切换

1
2
3
4
5
6
viewModelScope.launch(Dispatchers.IO) {      // IO 线程池,适合网络/文件
val data = api.fetchData()
withContext(Dispatchers.Main) { // 切回主线程更新 UI
textView.text = data
}
}

常用调度器:

Dispatcher 用途
Dispatchers.Main 主线程,更新 UI
Dispatchers.IO 网络、磁盘 IO(线程池)
Dispatchers.Default CPU 密集计算
Dispatchers.Unconfined 不限制,一般不用

记住:网络/IO 用 Dispatchers.IO,更新 UI 用 Dispatchers.Main。用 withContext 而不是手动 GlobalScope 切线程。

三、结构化并发:作用域是核心

协程最重要的一点是”结构化并发“:协程必须属于某个作用域(Scope),作用域取消时,它下面的所有子协程一起取消。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 错误示范:GlobalScope 不受生命周期控制
GlobalScope.launch {
// Activity 销毁了它还在跑,可能崩溃/泄漏
}

// 正确示范:跟随页面生命周期
class MyActivity : AppCompatActivity() {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
scope.launch {
val user = api.getUser()
render(user)
}
}

override fun onDestroy() {
super.onDestroy()
scope.cancel() // 页面销毁,取消所有协程
}
}

Android 生态里直接使用现成作用域:

  • viewModelScope:ViewModel 销毁时自动取消
  • lifecycleScope:Activity/Fragment 销毁时自动取消
  • repeatOnLifecycle:只在页面可见时执行

四、async/await:并行请求

1
2
3
4
5
6
7
8
9
10
11
viewModelScope.launch {
// 两个请求并行发起
val userDeferred = async(Dispatchers.IO) { api.getUser() }
val ordersDeferred = async(Dispatchers.IO) { api.getOrders() }

// 都完成后一起使用
val user = userDeferred.await()
val orders = ordersDeferred.await()

render(user, orders)
}

串行(慢)和并行(快)的选择:无依赖请求用 async 并行,有依赖的用 await 串联。

五、异常处理

1
2
3
4
5
6
7
8
9
10
11
12
viewModelScope.launch {
try {
val user = api.getUser()
render(user)
} catch (e: IOException) {
showToast("网络异常,请稍后重试")
} catch (e: Exception) {
showToast("加载失败")
} finally {
hideLoading()
}
}

注意 SupervisorJob 和 Job 的区别:SupervisorJob 中子协程失败不会级联取消兄弟协程,适合 UI 场景;Job 会级联取消。

1
2
// UI 场景推荐:一个任务失败不影响其他任务
CoroutineScope(SupervisorJob() + Dispatchers.Main)

六、Flow:协程版的响应式数据流

需要持续产生数据(如监听数据库、输入框防抖)用 Flow:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class UserRepository {
fun observeUsers(): Flow<List<User>> = flow {
while (true) {
emit(db.getUsers()) // 发射数据
delay(5000) // 每 5 秒轮询
}
}
}

// 在 UI 层收集
lifecycleScope.launch {
repo.observeUsers()
.flowOn(Dispatchers.IO) // 上游在 IO 线程
.collect { users ->
adapter.submitList(users) // 主线程收集
}
}

Flow 天然支持协程取消:作用域销毁,收集自动停止。

常见坑

  1. 在 launch 外直接调用 suspend 函数:编译报错,suspend 函数只能在协程作用域调用;
  2. 忘记 withContext(Dispatchers.Main) 更新 UI:在 IO 线程改 UI 会崩溃;
  3. 用 GlobalScope 写业务:几乎都是错的,脱离生命周期的协程 = 内存泄漏风险;
  4. async 没 await:协程照样跑,只是没人等结果,浪费资源。

总结

协程上手三步:viewModelScope.launch 起协程 → 网络/IO 用 Dispatchers.IO + withContext(Main) 更新 UI → 无依赖请求用 async 并行。把”作用域跟着生命周期走”这个原则记住,协程就是最顺手的异步方案。