跳到主要内容

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生命周期用途
lifecycleScopeActivity / FragmentUI 操作、数据收集
viewModelScopeViewModel数据加载、业务逻辑
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 有什么区别?

答案

特性launchasync
返回值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: 协程比线程有什么优势?

答案

  1. 轻量:协程是用户态调度,创建成本远低于线程
  2. 结构化并发:生命周期自动管理,避免泄漏
  3. 代码简洁suspend fun 用同步写法表达异步逻辑
  4. 取消支持:内置协作式取消,链式传播
  5. 与 Jetpack 集成lifecycleScopeviewModelScope、Flow

相关链接