Kotlin协程详解与现代Android开发实践

本文约 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/               # 依赖注入

八、总结

协程的核心优势

  1. 代码简洁:用顺序代码写异步逻辑
  2. 性能优越:轻量级,百万协程无压力
  3. 生命周期安全:结构化并发,自动取消
  4. 学习成本低:相比 RxJava,协程更容易上手

一句话记住

协程让你用写同步代码的方式,写出高性能的异步程序。


参考资料

相关推荐
plainGeekDev2 小时前
Kotlin 常见坑速查:object/lateinit/return 那些容易踩的坑
kotlin
plainGeekDev2 小时前
Android 高级岗 Kotlin 面试题:这些答不上来,基本告别大厂了
kotlin
plainGeekDev3 小时前
Kotlin 泛型与扩展:out/in 搞不懂?扩展函数到底扩展了啥?
kotlin
plainGeekDev3 小时前
Kotlin 特殊类型篇:密封类比枚举好使在哪?Nothing 到底是个啥?
kotlin
沅霖5 小时前
Android Studio Java工程开发环境,怎么切换到Kotlin开发环境
android·kotlin·android studio
Kapaseker5 小时前
Kotlin SharedFlow 的三个参数到底有啥用
android·kotlin
阿巴斯甜6 小时前
by 和by lazy 懒加载
kotlin
三少爷的鞋8 小时前
Android 架构系列之MVVM 和 MVI 算架构吗?
android·kotlin
只可远观1 天前
Android 自动埋点(页面打开 / 关闭 + 点击事件)完整方案
android·kotlin