协程速通 · Android

6. 挂起函数 suspend

suspend 修饰的函数只能在协程或其它挂起函数里调用。它最大的特点是:可以在不阻塞线程的情况下「暂停、把线程让出去干别的、稍后再恢复」——恢复后从暂停点继续往下执行。调用方写出来的代码是顺序的,但底层并不卡线程。

对照你熟悉的 XML 开发

以前异步要靠回调地狱(onSuccess{ onSuccess{ ... }})或 AsyncTask;suspend 让你用「看起来像同步、实际是异步」的写法,val a = loadA(); val b = loadB() 顺序写,不卡主线程。

一、suspend 全家福(按使用频率)

频率API作用典型场景
🔥 极高suspend fun定义一个可挂起的函数所有「可能耗时」的业务方法
🔥 极高withContext(...)切换调度器并带回结果耗时操作切 IO/Default
🔥 极高delay(...)非阻塞地等待轮询、节流、动画时序
🔥 极高Deferred.await()取 async 的结果(会挂起)并行后汇总
高emit(...) / emitAll(...)在 Flow 里发射值(本身是 suspend)Flow 构建器内部
高suspendCancellableCoroutine { }把回调 API 包成 suspendGPS、蓝牙、旧 SDK 回调
高coroutineScope { }临时作用域(见 04 章)组织并行 + 统一取消
中withTimeout / withTimeoutOrNull超时即取消网络 / 操作加时限
中ensureActive() / yield()检查取消 / 让出线程长循环里响应取消
中callbackFlow { }把回调转成 Flow监听型 API 暴露成数据流
低suspendCoroutine { }不可取消版桥接(少用)极老 API 桥接,注意取消语义

二、实战场景

5.1 Repository 把线程切换藏起来

// Repository 层:把 withContext 藏在实现里,对外只暴露干净的 suspend
class UserRepository(private val api: UserApi) {
    suspend fun fetchUser(id: String): User =
        withContext(Dispatchers.IO) { api.getUser(id) }
}

// 调用方(ViewModel)写起来就是顺序代码,不用关心线程
class UserViewModel(private val repo: UserRepository) : ViewModel() {
    private val _ui = MutableStateFlow<User?>(null)
    val ui: StateFlow<User?> = _ui.asStateFlow()

    fun load() = viewModelScope.launch {
        val u = repo.fetchUser("1")   // ✅ 看起来同步,实际在 IO 跑
        _ui.value = u
    }
}
原则

把「可能耗时的操作」标记成 suspend,把线程切换藏在实现里。调用方代码永远是干净的「顺序写法」。

5.2 顺序 vs 并行

// 顺序(慢):A 完才发起 B
suspend fun loadSeq() {
    val a = repo.getUser()      // 等 A
    val b = repo.getOrders()    // 再等 B
}

// 并行(快):用 async 同时发起,再 await 汇总
suspend fun loadPar() = coroutineScope {
    val a = async { repo.getUser() }
    val b = async { repo.getOrders() }
    show(a.await(), b.await())
}

5.3 回调 → suspend:包住老 API

很多旧 SDK(定位、蓝牙、第三方)只有回调。用 suspendCancellableCoroutine 包一层,调用方就能顺序写;关键是 invokeOnCancellation 里做清理,协程被取消时释放资源。

suspend fun getLocation(): Location = suspendCancellableCoroutine { cont ->
    val listener = object : LocationListener {
        override fun onLocationChanged(loc: Location) {
            cont.resume(loc)                    // 恢复协程并带回结果
            locationClient.removeListener(this)
        }
        override fun onError(e: Exception) {
            cont.resumeWith(Result.failure(e)) // 异常也恢复(调用方 try-catch)
            locationClient.removeListener(this)
        }
    }
    locationClient.requestLocationUpdates(listener)
    cont.invokeOnCancellation { locationClient.removeListener(listener) }  // 取消时清理
}

5.4 suspend 与 Flow

emit 本身就是挂起函数;Flow 的收集(collect)也必须在协程里。两者天然配合:上游挂起发射,下游挂起收集。

fun tickers(): Flow<Int> = flow {
    for (i in 1..10) {
        delay(1000)        // 挂起,不阻塞线程
        emit(i)            // emit 也是 suspend
    }
}

viewModelScope.launch {
    tickers().collect { value -> _ui.value = value }
}

5.5 在 ViewModel / Compose 里调用 suspend

// ViewModel:用 viewModelScope.launch 进入协程
fun onRefresh() = viewModelScope.launch { _ui.value = repo.fetch() }

// Compose:用 rememberCoroutineScope 拿一个可随组件取消的作用域
val scope = rememberCoroutineScope()
Button(onClick = { scope.launch { viewModel.refresh() } }) { Text("刷新") }

5.6 取消与超时

viewModelScope.launch {
    try {
        val data = withTimeout(3000) { repo.fetch() }  // 3s 超时抛 TimeoutCancellationException
        render(data)
    } catch (e: TimeoutCancellationException) {
        showError("加载超时")
    }
}

长循环要响应取消:协程被取消时,不会强行打断正在跑的 CPU 代码。如果你的 suspend 里有 while 长循环,要主动检查:循环开头调 ensureActive()(会抛 CancellationException 退出),否则任务会「取消不掉」,继续空跑。

三、真实开发中会踩的坑

坑 1:在普通函数里调 suspend:fun bad() { repo.fetchUser("1") } 会直接编译报错——suspend 只能在协程 / 挂起函数里调。解决:把调用方也标成 suspend,或包进 viewModelScope.launch { }。

坑 2:用 runBlocking 在 UI 线程等结果 → ANR:runBlocking { repo.fetch() } 会阻塞当前线程直到协程结束。在 onClick 里这么写,主线程被卡死 → ANR。runBlocking 只用于 main() 入口和单元测试。UI 里用 launch / async。

坑 3:suspend 里做阻塞调用却不切线程:在 suspend 函数里写 Thread.sleep(1000) 或同步 Socket/File 读写,即使函数是 suspend,它仍然阻塞当前线程。耗时操作必须 withContext(Dispatchers.IO) 包起来。

坑 4:把 suspend 当「立即返回」:suspend 只是「可能挂起」,调用后不会马上拿到结果,真正执行可能在 IO 线程跑几百毫秒。别假定它同步返回去写后续逻辑;需要结果就 val x = suspendFun() 顺序接,或用 async/await 并行。

坑 5:用 suspendCoroutine 而非 suspendCancellableCoroutine:suspendCoroutine 桥接的回调无法被取消(协程取消后回调仍可能触发)。包老 API 时优先用 suspendCancellableCoroutine 并加 invokeOnCancellation 清理。

四、小结

  • suspend 不阻塞线程,只是「暂停让出、稍后恢复」。
  • 耗时操作标成 suspend,线程切换藏进 withContext,调用方写顺序代码。
  • 并行用 async/await;回调 API 用 suspendCancellableCoroutine 包。
  • 长循环 / 阻塞点要响应取消(ensureActive / 切 Dispatchers.IO)。
  • UI 线程里永远别用 runBlocking。

Kotlin 协程速通 · Android 开发 · 内容基于 kotlinx.coroutines 1.7+