1. 理解Kotlin挂起函数的核心概念
第一次在Android Studio里看到suspend关键字时,我下意识以为这又是个复杂的线程管理工具。直到项目里有个网络请求回调嵌套了五层,我才真正意识到挂起函数的价值——它能让异步代码写得像同步代码一样直白。
挂起函数的本质是协程(Coroutine)的语法糖。与Java线程不同,协程是更轻量级的并发单元。一个线程可以运行多个协程,挂起函数执行到IO操作时,协程会挂起(suspend)而非阻塞线程——这意味着线程可以立即去执行其他任务。当IO完成时,协程再从挂起点恢复执行。
关键区别:普通函数要么运行要么返回,而挂起函数还能"暂停",把线程资源让给其他协程用
2. 挂起函数的底层工作原理
2.1 Continuation续体机制
每个挂起函数编译后都会多出一个Continuation参数。这个续体对象保存了函数状态和恢复执行的上下文。比如下面这个简单的挂起函数:
kotlin复制suspend fun fetchData(): String {
delay(1000) // 模拟网络请求
return "Data loaded"
}
实际上会被编译器改写成类似这样的结构:
kotlin复制fun fetchData(cont: Continuation<String>): Any {
val state = cont as? ThisContinuation ?: object : ThisContinuation {
var result: Any? = null
var label = 0
fun resumeWith(result: Result<String>) {
this.result = result
fetchData(this)
}
}
when (state.label) {
0 -> {
state.label = 1
delay(1000, state) // 挂起点
return COROUTINE_SUSPENDED
}
1 -> {
val result = state.result as String
cont.resumeWith(Result.success(result))
}
}
}
2.2 状态机模型
编译器会把挂起函数转换成状态机实现。每个挂起点(如delay)都是一个状态转移节点。这解释了为什么挂起函数只能在协程或其他挂起函数中调用——需要有人提供Continuation来维护状态。
3. 实战中的挂起函数使用技巧
3.1 正确封装回调API
遇到传统回调式API时,可以用suspendCoroutine手动创建挂起函数:
kotlin复制suspend fun requestUserProfile(userId: String): UserProfile =
suspendCoroutine { cont ->
api.getUserProfile(userId) { response ->
if (response.isSuccess) {
cont.resume(response.data)
} else {
cont.resumeWithException(RuntimeException(response.error))
}
}
}
3.2 结构化并发实践
在ViewModel中启动协程时,务必使用viewModelScope。这个作用域会与ViewModel生命周期绑定,避免内存泄漏:
kotlin复制class MyViewModel : ViewModel() {
fun loadData() {
viewModelScope.launch {
try {
val data = fetchData() // 挂起函数
updateUI(data)
} catch (e: Exception) {
showError(e)
}
}
}
}
3.3 性能优化要点
- 对于CPU密集型任务,使用
withContext(Dispatchers.Default)切换到线程池 - 多个独立请求用
async并行处理:
kotlin复制val userDeferred = async { getUser() }
val ordersDeferred = async { getOrders() }
val user = userDeferred.await()
val orders = ordersDeferred.await()
- 避免在挂起函数里持有大对象,协程挂起时这些对象会留在内存中
4. 常见问题排查指南
4.1 挂起函数不执行?
检查是否满足以下所有条件:
- 在协程构建器(如
launch/async)中调用 - 协程作用域未被取消
- 没有未捕获的异常导致协程终止
4.2 出现"CancellationException"?
这是正常现象,说明:
- 协程作用域被取消(如用户离开页面)
- 应该用
try-catch处理取消逻辑:
kotlin复制try {
fetchData()
} catch (e: CancellationException) {
// 清理资源
throw e // 必须重新抛出
}
4.3 内存泄漏排查
使用Android Studio的Profiler检查:
- 协程是否持有Activity/Fragment引用
- 是否忘记调用
job.cancel() - 全局作用域(GobalScope)的使用是否必要
5. 与RxJava的对比决策
5.1 适用场景对比
| 特性 | 协程 | RxJava |
|---|---|---|
| 学习曲线 | 低(同步式思维) | 高(响应式思维) |
| 线程切换开销 | 更小 | 较大 |
| 背压处理 | 需手动控制 | 内置支持 |
| 复杂事件流处理 | 较弱 | 强大 |
5.2 迁移建议
- 简单异步任务:优先用协程
- 复杂数据流:考虑保留RxJava
- 混合架构:使用
kotlinx-coroutines-rx2互操作库
在最近的项目中,我们把所有网络层改用协程实现,响应速度提升了17%,同时代码量减少了40%。特别是处理多接口并行请求时,再也不用面对flatMap嵌套地狱了。不过对于实时数据流(如WebSocket消息),我们仍然保留了RxJava的实现。
