Kotlin 协程进阶
问题
Kotlin 协程在 Android 中的进阶用法有哪些?
答案
结构化并发
Android 协程的核心原则是结构化并发(Structured Concurrency)—— 协程必须在某个 Scope 内启动,Scope 取消时所有子协程自动取消。
取消传播规则:
- 父协程取消 → 所有子协程被取消
- 子协程异常 → 父协程取消 → 其他子协程取消
- 使用
SupervisorJob时子协程异常不会影响兄弟协程
Android 内置 Scope
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// 1. lifecycleScope —— 跟随 Activity/Fragment 生命周期
lifecycleScope.launch {
val data = fetchData()
textView.text = data
}
// 2. 指定生命周期阶段
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
// 仅在 STARTED 以上状态收集
viewModel.uiState.collect { state ->
updateUI(state)
}
}
}
}
}
class MyViewModel : ViewModel() {
init {
// 3. viewModelScope —— ViewModel 销毁时自动取消
viewModelScope.launch {
loadData()
}
}
}
| Scope | 生命周期 | 用途 |
|---|---|---|
lifecycleScope | Activity / Fragment | UI 操作、数据收集 |
viewModelScope | ViewModel | 数据加载、业务逻辑 |
GlobalScope | 进程级别(不推荐) | 全局单次任务 |
| 自定义 Scope | 手动控制 | SDK、后台服务 |
Dispatcher 调度器
lifecycleScope.launch {
// Dispatchers.Main:主线程(UI 操作)
showLoading()
val data = withContext(Dispatchers.IO) {
// Dispatchers.IO:IO 密集型(网络、数据库、文件)
// 共享线程池,最多 64 个线程
api.fetchData()
}
val result = withContext(Dispatchers.Default) {
// Dispatchers.Default:CPU 密集型(排序、解析)
// 线程数 = CPU 核心数
parseData(data)
}
// 自动回到 Main
showData(result)
}
Job 与取消
class SearchViewModel : ViewModel() {
private var searchJob: Job? = null
fun search(query: String) {
// 取消上一次搜索
searchJob?.cancel()
searchJob = viewModelScope.launch {
delay(300) // 防抖
val results = withContext(Dispatchers.IO) {
repository.search(query)
}
_searchResults.value = results
}
}
}
异常处理
// 1. try-catch(推荐)
viewModelScope.launch {
try {
val data = repository.fetchData()
_uiState.value = UiState.Success(data)
} catch (e: CancellationException) {
throw e // 不要捕获取消异常!
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message)
}
}
// 2. CoroutineExceptionHandler(全局兜底)
val handler = CoroutineExceptionHandler { _, exception ->
Log.e("Coroutine", "Uncaught exception", exception)
}
viewModelScope.launch(handler) {
riskyOperation()
}
// 3. SupervisorJob(子协程异常隔离)
viewModelScope.launch {
supervisorScope {
launch { task1() } // 失败不影响 task2
launch { task2() }
}
}
不要捕获 CancellationException
CancellationException 用于协程取消传播。捕获它会阻止取消机制正常工作。在 catch 块中应重新抛出。
并发模式
// 1. 并行请求
suspend fun loadDashboard(): DashboardData = coroutineScope {
val user = async { api.getUser() }
val orders = async { api.getOrders() }
val notifications = async { api.getNotifications() }
DashboardData(
user = user.await(),
orders = orders.await(),
notifications = notifications.await()
)
}
// 2. 超时控制
val result = withTimeoutOrNull(5000L) {
api.fetchData()
} ?: defaultData
// 3. 并发限制(Semaphore)
val semaphore = Semaphore(3) // 最多 3 个并发
urls.map { url ->
async {
semaphore.withPermit {
download(url)
}
}
}.awaitAll()
常见面试问题
Q1: launch 和 async 有什么区别?
答案:
| 特性 | launch | async |
|---|---|---|
| 返回值 | Job(没有结果) | Deferred<T>(有结果) |
| 异常传播 | 立即传播到父协程 | 在 await() 时抛出 |
| 用途 | 执行不需要返回值的任务 | 需要返回结果、并行计算 |
Q2: withContext 和 async + await 的区别?
答案:
withContext(Dispatchers.IO) { ... }:切换线程执行代码块,串行,等待完成后返回结果async { ... }.await():启动新协程,可以并行多个 async 然后 await
只有一个异步任务时两者效果相同,有多个并行任务时用 async。
Q3: viewModelScope 是如何自动取消的?
答案:
viewModelScope 内部创建了一个 CloseableCoroutineScope,在 ViewModel.onCleared() 时调用 scope.cancel()。这个 Scope 使用 SupervisorJob() + Dispatchers.Main.immediate 作为 CoroutineContext,确保子协程异常不会互相影响,并默认在主线程执行。
Q4: 协程比线程有什么优势?
答案:
- 轻量:协程是用户态调度,创建成本远低于线程
- 结构化并发:生命周期自动管理,避免泄漏
- 代码简洁:
suspend fun用同步写法表达异步逻辑 - 取消支持:内置协作式取消,链式传播
- 与 Jetpack 集成:
lifecycleScope、viewModelScope、Flow