本文约 3500 字,阅读时间约 10 分钟
一、为什么需要协程?
1.1 痛点:Android 开发中的异步问题
在 Android 开发中,我们经常需要处理以下场景:
- 网络请求(获取用户信息)
- 数据库操作(读写 Room)
- 文件 I/O(读写本地文件)
- 耗时计算(图片压缩、数据解析)
这些操作不能在主线程执行,否则会导致 ANR(应用无响应)。
1.2 传统解决方案的困境
方案一:Thread + Handler(最原始)
kotlin
// 传统方式:Thread + Handler
class OldSchoolViewModel : ViewModel() {
private val handler = Handler(Looper.getMainLooper())
fun loadUser(userId: String) {
Thread {
// 子线程执行网络请求
val result = fetchUserFromNetwork(userId)
// 切回主线程更新 UI
handler.post {
// 更新 UI
updateUI(result)
}
}.start()
}
}
问题:
- 线程创建开销大
- 容易忘记切回主线程
- 嵌套多了变成"回调地狱"
- 难以处理并发和取消
方案二:回调(Callback)
javascript
// 回调方式:Callback Hell
fun loadUserData(userId: String, callback: (User) -> Unit) {
fetchUser(userId) { user ->
fetchPosts(user.id) { posts ->
fetchComments(posts.first().id) { comments ->
// 三层回调嵌套,难以维护
callback(UserWithPosts(user, posts, comments))
}
}
}
}
问题:
- 嵌套过深,代码可读性差
- 错误处理困难
- 难以组合多个异步操作
方案三:RxJava
kotlin
// RxJava 方式
fun loadUserRx(userId: String): Observable<User> {
return Observable.create { emitter ->
val user = fetchUserFromNetwork(userId)
emitter.onNext(user)
emitter.onComplete()
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
}
问题:
- 学习曲线陡峭(Observable、Flowable、Single...)
- 操作符繁多(map、flatMap、switchMap...)
- 包体积大(约 2.5MB)
二、什么是协程?
2.1 官方定义
协程(Coroutine)是一种轻量级的并发框架 ,它允许你以顺序代码的方式编写异步逻辑。
2.2 核心特性
| 特性 | 说明 | 对比线程 |
|---|---|---|
| 轻量级 | 一个线程可以运行成千上万个协程 | 线程是操作系统资源,数量有限 |
| 挂起不阻塞 | 协程挂起时释放线程,线程可以执行其他任务 | 线程阻塞时资源被占用 |
| 结构化并发 | 协程有明确的生命周期和作用域 | 线程生命周期难以管理 |
| 可取消 | 协程可以安全地取消 | 线程取消(stop())不安全 |
2.3 协程 vs 线程:一张图看懂
yaml
时间轴 →
┌─────────────────────────────────────────┐
│ 线程模型 │
│ Thread-1: ████████████░░░░░░░░░░░░░░░░ │
│ ↑ 阻塞等待 │
│ Thread-2: ░░░░░░░░░░████████████░░░░░░ │
│ ↑ 阻塞等待 │
│ 利用率低,线程切换开销大 │
├─────────────────────────────────────────┤
│ 协程模型 │
│ Thread-1: ████░░████░░██████░░████░░██ │
│ ↑协程A ↑协程B ↑协程A ↑协程C │
│ 协程挂起时释放线程,线程利用率高 │
└─────────────────────────────────────────┘
三、协程的核心概念
3.1 suspend 函数
suspend 是协程的基石,它标记一个函数可以挂起 (暂停执行)并在将来恢复。
kotlin
// suspend 函数:可以挂起,不阻塞线程
suspend fun fetchUser(userId: String): User {
// delay 是一个 suspend 函数,会挂起当前协程
delay(1000) // 模拟网络延迟
return User(userId, "张三")
}
// 普通函数:不能调用 suspend 函数
fun normalFunction() {
// ❌ 编译错误:Suspend function 'fetchUser' should be called only from a coroutine or another suspend function
// val user = fetchUser("123")
}
// suspend 函数:可以调用其他 suspend 函数
suspend fun loadUserData(userId: String): UserData {
val user = fetchUser(userId) // 挂起,等待网络返回
val posts = fetchPosts(user.id) // 挂起,等待网络返回
return UserData(user, posts)
}
3.2 协程构建器
| 构建器 | 作用 | 返回值 |
|---|---|---|
launch |
启动一个协程,不返回结果 | Job |
async |
启动一个协程,返回结果 | Deferred<T> |
runBlocking |
阻塞当前线程,用于桥接普通代码和协程 | T |
kotlin
// launch:启动一个协程,不关心返回值
fun startCoroutine() {
CoroutineScope(Dispatchers.Main).launch {
val user = fetchUser("123") // 挂起,不阻塞主线程
updateUI(user) // 恢复,更新 UI
}
}
// async:启动一个协程,需要返回值
suspend fun loadUserWithPosts(userId: String): UserWithPosts {
coroutineScope {
val userDeferred = async { fetchUser(userId) }
val postsDeferred = async { fetchPosts(userId) }
// await 会挂起,直到两个 async 都完成
UserWithPosts(userDeferred.await(), postsDeferred.await())
}
}
// runBlocking:桥接普通代码和协程(测试或 main 函数中使用)
fun main() = runBlocking {
val user = fetchUser("123")
println(user)
}
3.3 调度器(Dispatcher)
调度器决定协程在哪个线程上执行:
| 调度器 | 用途 |
|---|---|
Dispatchers.Main |
主线程:UI 操作 |
Dispatchers.IO |
IO 线程:网络、数据库、文件 |
Dispatchers.Default |
CPU 密集型任务:计算、排序 |
Dispatchers.Unconfined |
不指定,继承父协程的调度器 |
kotlin
// 正确使用调度器
fun loadData() {
viewModelScope.launch {
// 默认在主线程
val result = withContext(Dispatchers.IO) {
// 切换到 IO 线程执行网络请求
fetchUserFromNetwork("123")
}
// 自动切回主线程更新 UI
updateUI(result)
}
}
四、协程的魔法:挂起与恢复
4.1 挂起函数的工作原理
Kotlin 编译器会将 suspend 函数转换为状态机,每个挂起点对应一个状态。
kotlin
// 你写的代码
suspend fun loadUser(userId: String): User {
val token = fetchToken() // 挂起点 1
val user = fetchUser(token) // 挂起点 2
return user
}
// 编译器生成的伪代码(简化版)
fun loadUser(userId: String, continuation: Continuation): Any? {
val stateMachine = object : CoroutineImpl(continuation) {
var label = 0
var result: Any?
override fun invokeSuspend(result: Any?): Any? {
this.result = result
when (label) {
0 -> {
label = 1
// 执行 fetchToken,挂起
if (fetchToken(this) == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED
}
1 -> {
val token = result as String
label = 2
// 执行 fetchUser,挂起
if (fetchUser(token, this) == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED
}
2 -> {
return result as User
}
}
}
}
return stateMachine.invokeSuspend(null)
}
4.2 挂起与恢复的流程图
scss
┌─────────────────────────────────────────────────────────┐
│ 协程执行流程 │
│ │
│ 开始执行 loadUser() │
│ ↓ │
│ ┌─────────┐ ┌──────────────┐ ┌──────────┐ │
│ │ fetchToken│────▶ 挂起(等待) │────▶ 恢复执行 │ │
│ └─────────┘ └──────────────┘ └──────────┘ │
│ ↓ │
│ ┌─────────┐ ┌──────────────┐ ┌──────────┐ │
│ │ fetchUser│────▶ 挂起(等待) │────▶ 恢复执行 │ │
│ └─────────┘ └──────────────┘ └──────────┘ │
│ ↓ │
│ 返回结果 │
│ │
│ 线程时间线: │
│ ████████░░░░░░░░████████░░░░░░░░████████ │
│ ↑执行代码 ↑挂起 ↑恢复 ↑挂起 ↑恢复 ↑完成 │
└─────────────────────────────────────────────────────────┘
五、协程实战:现代 Android 开发
5.1 项目依赖配置
scss
// build.gradle.kts (Module: app)
dependencies {
// 协程核心库
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
// Android 协程支持
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")
// ViewModel 协程支持
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.0")
// Lifecycle 协程支持
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.0")
}
5.2 ViewModel + 协程:标准实践
kotlin
// UserViewModel.kt
class UserViewModel : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
// 使用 viewModelScope 自动管理协程生命周期
fun loadUser(userId: String) {
viewModelScope.launch {
_uiState.value = UiState.Loading
try {
val user = withContext(Dispatchers.IO) {
userRepository.fetchUser(userId)
}
_uiState.value = UiState.Success(user)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message ?: "未知错误")
}
}
}
// 并发请求:同时获取用户信息和帖子
fun loadUserWithPosts(userId: String) {
viewModelScope.launch {
_uiState.value = UiState.Loading
try {
val result = coroutineScope {
val userDeferred = async(Dispatchers.IO) {
userRepository.fetchUser(userId)
}
val postsDeferred = async(Dispatchers.IO) {
postRepository.fetchPosts(userId)
}
UserWithPosts(
user = userDeferred.await(),
posts = postsDeferred.await()
)
}
_uiState.value = UiState.Success(result)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message ?: "未知错误")
}
}
}
// 取消协程:ViewModel 销毁时自动取消
override fun onCleared() {
super.onCleared()
// viewModelScope 会自动取消所有子协程
}
}
// UI 状态密封类
sealed class UiState {
object Loading : UiState()
data class Success<T>(val data: T) : UiState()
data class Error(val message: String) : UiState()
}
5.3 Repository 层:协程 + 网络请求
kotlin
// UserRepository.kt
class UserRepository(
private val api: UserApi,
private val dao: UserDao
) {
// 协程 + Retrofit(Retrofit 原生支持 suspend)
suspend fun fetchUser(userId: String): User {
return api.getUser(userId) // Retrofit 接口方法声明为 suspend
}
// 协程 + Room(Room 原生支持 suspend)
suspend fun saveUser(user: User) {
dao.insertUser(user)
}
// 协程 + 缓存策略
suspend fun getUserWithCache(userId: String): User {
// 先查本地缓存
val cached = dao.getUser(userId)
if (cached != null && !isExpired(cached)) {
return cached
}
// 缓存未命中或过期,请求网络
val fresh = api.getUser(userId)
saveUser(fresh) // 更新缓存
return fresh
}
}
5.4 Retrofit 接口声明
less
// UserApi.kt
interface UserApi {
// 传统方式:Call 回调
@GET("user/{id}")
fun getUserOld(@Path("id") userId: String): Call<User>
// 协程方式:suspend 函数
@GET("user/{id}")
suspend fun getUser(@Path("id") userId: String): User
// 协程 + 多个请求
@GET("user/{id}/posts")
suspend fun getUserPosts(@Path("id") userId: String): List<Post>
}
5.5 Activity/Fragment 中使用
kotlin
// UserActivity.kt
class UserActivity : AppCompatActivity() {
private val viewModel: UserViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 观察 UI 状态
lifecycleScope.launch {
viewModel.uiState.collect { state ->
when (state) {
is UiState.Loading -> showLoading()
is UiState.Success -> showUser(state.data)
is UiState.Error -> showError(state.message)
}
}
}
// 触发加载
viewModel.loadUser("123")
}
}
六、协程的高级技巧
6.1 异常处理
ini
// 方式一:try-catch(推荐)
viewModelScope.launch {
try {
val user = repository.fetchUser("123")
_uiState.value = UiState.Success(user)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message ?: "未知错误")
}
}
// 方式二:CoroutineExceptionHandler
val handler = CoroutineExceptionHandler { _, exception ->
println("捕获异常: $exception")
}
viewModelScope.launch(handler) {
val user = repository.fetchUser("123")
_uiState.value = UiState.Success(user)
}
// 方式三:supervisorScope(子协程异常不影响兄弟协程)
viewModelScope.launch {
supervisorScope {
val userDeferred = async { repository.fetchUser("123") }
val postsDeferred = async { repository.fetchPosts("123") }
// 即使 postsDeferred 失败,userDeferred 仍然可以成功
val user = try {
userDeferred.await()
} catch (e: Exception) {
null
}
}
}
6.2 超时控制
javascript
// 使用 withTimeout 设置超时
viewModelScope.launch {
try {
val user = withTimeout(5000) { // 5秒超时
repository.fetchUser("123")
}
_uiState.value = UiState.Success(user)
} catch (e: TimeoutCancellationException) {
_uiState.value = UiState.Error("请求超时")
}
}
// 使用 withTimeoutOrNull(超时返回 null)
viewModelScope.launch {
val user = withTimeoutOrNull(5000) {
repository.fetchUser("123")
}
if (user == null) {
_uiState.value = UiState.Error("请求超时")
} else {
_uiState.value = UiState.Success(user)
}
}
6.3 Flow:响应式数据流
kotlin
// Repository 层返回 Flow
class UserRepository(private val api: UserApi, private val dao: UserDao) {
// 返回 Flow,数据变化时自动推送
fun observeUser(userId: String): Flow<User> = flow {
// 先发射缓存数据
val cached = dao.getUser(userId)
if (cached != null) emit(cached)
// 再请求网络
val fresh = api.getUser(userId)
dao.insertUser(fresh) // 更新缓存
emit(fresh) // 发射新数据
}.flowOn(Dispatchers.IO) // 在 IO 线程执行
}
// ViewModel 中收集 Flow
class UserViewModel : ViewModel() {
val userFlow: StateFlow<User?> = repository.observeUser("123")
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = null
)
}
// Activity 中收集
lifecycleScope.launch {
viewModel.userFlow.collect { user ->
if (user != null) {
updateUI(user)
}
}
}
七、最佳实践总结
7.1 协程使用原则
| 原则 | 说明 |
|---|---|
| 使用 viewModelScope | ViewModel 销毁时自动取消协程 |
| 使用 lifecycleScope | Activity/Fragment 销毁时自动取消 |
| 使用 Dispatchers.IO | 网络请求、数据库操作 |
| 使用 Dispatchers.Main | UI 更新 |
| 使用 try-catch | 捕获协程中的异常 |
| 使用 withTimeout | 避免请求无限等待 |
7.2 常见陷阱
kotlin
// ❌ 错误:在 ViewModel 中创建全局协程作用域
class BadViewModel : ViewModel() {
private val scope = CoroutineScope(Dispatchers.Main) // 不会自动取消!
fun loadData() {
scope.launch {
// ...
}
}
}
// ✅ 正确:使用 viewModelScope
class GoodViewModel : ViewModel() {
fun loadData() {
viewModelScope.launch {
// ...
}
}
}
// ❌ 错误:在非 suspend 函数中调用 suspend 函数
fun loadData() {
// fetchUser("123") // 编译错误
}
// ✅ 正确:使用 runBlocking 桥接(仅限测试或 main 函数)
fun loadData() {
runBlocking {
fetchUser("123")
}
}
7.3 推荐的项目结构
bash
app/
├── data/
│ ├── api/ # Retrofit 接口
│ ├── db/ # Room 数据库
│ └── repository/ # Repository 层(协程 + 缓存)
├── domain/
│ └── model/ # 数据模型
├── ui/
│ ├── viewmodel/ # ViewModel(viewModelScope)
│ └── activity/ # Activity/Fragment(lifecycleScope)
└── di/ # 依赖注入
八、总结
协程的核心优势
- 代码简洁:用顺序代码写异步逻辑
- 性能优越:轻量级,百万协程无压力
- 生命周期安全:结构化并发,自动取消
- 学习成本低:相比 RxJava,协程更容易上手
一句话记住
协程让你用写同步代码的方式,写出高性能的异步程序。