一、协程基础概念
1.1 协程定义与本质
Kotlin 协程是一种轻量级的线程管理机制,本质上是可暂停的计算单元。与传统线程相比,协程更高效,因为它允许在不阻塞线程的情况下暂停和恢复执行。
从底层看,协程是通过状态机实现的。每次暂停(suspend)时,当前状态会被保存,恢复时再加载。这种机制使得协程非常适合处理异步操作。
1.2 协程与线程的对比
| 特性 | 线程 | 协程 |
|---|---|---|
| 资源消耗 | 高(MB 级别) | 低(KB 级别) |
| 调度方式 | 操作系统调度 | 协程自身调度 |
| 切换开销 | 高(涉及内核态切换) | 低(用户态切换) |
| 并发能力 | 受限于系统资源 | 轻松创建百万级协程 |
以下是一个简单的对比示例:
// 线程方式
fun runWithThreads() {
repeat(100_000) {
Thread {
Thread.sleep(1000)
}.start() // 可能会导致 OutOfMemoryError
}
}
// 协程方式
suspend fun runWithCoroutines() {
coroutineScope {
repeat(100_000) {
launch {
delay(1000) // 非阻塞延迟
}
}
} // 所有协程自动等待完成
}
二、协程核心组件
2.1 协程构建器
Kotlin 提供了多种协程构建器,用于不同场景:
- launch:启动一个新协程,不返回结果
- async:启动一个带返回值的协程(通过 Deferred 对象)
- runBlocking:阻塞当前线程,等待协程完成(主要用于测试)
- withContext:在指定上下文中执行协程,并返回结果
suspend fun fetchData() = coroutineScope {
// 使用 async 并行获取数据
val userDeferred = async { fetchUser() }
val profileDeferred = async { fetchProfile() }
// 合并结果
UserWithProfile(userDeferred.await(), profileDeferred.await())
}
// 使用 withContext 切换到 IO 上下文
suspend fun fetchUser(): User = withContext(Dispatchers.IO) {
// 执行网络请求
}
2.2 协程作用域
协程作用域(CoroutineScope)负责管理协程的生命周期。主要分为两类:
- 生命周期绑定作用域:如
lifecycleScope(Android)、viewModelScope - 独立作用域:如
GlobalScope(不推荐直接使用)
class MyViewModel : ViewModel() {
// viewModelScope 会在 ViewModel 销毁时自动取消协程
fun loadData() = viewModelScope.launch {
// 执行异步操作
}
}
2.3 协程调度器
协程调度器决定协程在哪个线程或线程池执行:
- Dispatchers.Default:适合CPU密集型任务(共享4核线程池)
- Dispatchers.IO:适合IO密集型任务(弹性线程池)
- Dispatchers.Main:Android主线程(UI操作)
- newSingleThreadContext:创建专用单线程
suspend fun processData() {
// 切换到 IO 调度器执行文件操作
withContext(Dispatchers.IO) {
readFile()
}
// 使用 Default 调度器进行 CPU 密集计算
withContext(Dispatchers.Default) {
compute()
}
}
三、挂起函数(Suspend Function)
3.1 挂起函数的定义与特性
挂起函数是协程的核心组成部分,用 suspend 关键字标记:
- 只能在协程内部或其他挂起函数中调用
- 可以暂停和恢复执行,不会阻塞线程
- 本质上是带有 Continuation 参数的状态机
suspend fun fetchUserData(userId: String): UserData {
// 模拟网络请求
delay(1000)
return UserData(userId, "John Doe")
}
3.2 挂起函数的实现原理
挂起函数通过 Continuation 接口实现异步逻辑:
// 简化的 Continuation 接口
interface Continuation<in T> {
val context: CoroutineContext
fun resumeWith(result: Result<T>)
}
// 编译器转换后的挂起函数
fun fetchUserData(
userId: String,
continuation: Continuation<UserData>
): Any? {
// 状态机实现
}
3.3 自定义挂起函数
使用 suspendCoroutine 和 suspendCancellableCoroutine 可以将回调式API转换为协程友好的API:
suspend fun readFile(path: String): String = suspendCancellableCoroutine { cont ->
File(path).readTextAsync(
onSuccess = { cont.resume(it) },
onError = { cont.resumeWithException(it) }
)
}
四、协程上下文与调度
4.1 协程上下文组成
协程上下文是一个不可变集合,包含:
- Job:控制协程的生命周期
- CoroutineDispatcher:决定协程执行的线程
- CoroutineName:协程的名称(用于调试)
- ExceptionHandler:协程异常处理器
val myContext = Dispatchers.IO + CoroutineName("file-worker")
4.2 协程上下文继承规则
子协程会继承父协程的上下文,但可以通过构建器指定新的上下文元素:
coroutineScope {
// 继承父协程的上下文
launch {
println(coroutineContext[CoroutineName]) // null
}
// 指定新的上下文元素
launch(Dispatchers.Default + CoroutineName("calculator")) {
println(coroutineContext[CoroutineName]) // calculator
}
}
4.3 上下文元素组合与优先级
多个相同类型的上下文元素组合时,后面的会覆盖前面的:
val context = Dispatchers.IO + CoroutineName("worker") + Dispatchers.Default
// 最终 Dispatcher 是 Dispatchers.Default
五、协程异常处理
5.1 异常传播机制
协程中的异常传播规则:
- launch:异常会立即传播给父协程
- async:异常会延迟到
await()调用时抛出 - SupervisorJob:子协程异常不会影响其他子协程
coroutineScope {
// 这个协程抛出的异常会导致整个作用域取消
launch {
throw RuntimeException("Oops!")
}
// 这个协程不会执行
launch {
delay(1000)
println("This will never print")
}
}
5.2 异常处理器(CoroutineExceptionHandler)
用于捕获未被处理的异常:
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught $exception")
}
val scope = CoroutineScope(Dispatchers.Default + handler)
scope.launch {
throw RuntimeException("Test exception")
}
5.3 资源管理与 try-finally
协程中的资源管理与普通代码相同,但要注意使用 withContext 而不是阻塞操作:
suspend fun readData(): String {
val file = openFile()
return try {
withContext(Dispatchers.IO) {
file.readText()
}
} finally {
file.close()
}
}
六、协程高级应用
6.1 通道(Channel)
通道用于协程间的通信,类似于 BlockingQueue,但支持挂起操作:
suspend fun produceNumbers(channel: Channel<Int>) {
var x = 1
while (true) {
channel.send(x++)
delay(100)
}
}
suspend fun consumeNumbers(channel: Channel<Int>) {
for (x in channel) {
println(x)
}
}
// 使用示例
val channel = Channel<Int>()
coroutineScope {
launch { produceNumbers(channel) }
launch { consumeNumbers(channel) }
delay(1000)
channel.cancel() // 关闭通道
}
6.2 数据流(Flow)
Flow 是冷数据流,类似于 RxJava 的 Observable,但与协程更紧密集成:
// 定义流
fun numbersFlow(): Flow<Int> = flow {
for (i in 1..3) {
delay(100)
emit(i)
}
}
// 使用流
coroutineScope {
launch {
numbersFlow()
.map { it * it }
.collect { println(it) }
}
}
6.3 共享状态与并发
协程提供多种并发原语:
- Mutex:类似于 Java 的 ReentrantLock
- Semaphore:限制并发协程数量
- Atomic:原子操作
val mutex = Mutex()
var counter = 0
suspend fun increment() {
mutex.withLock {
counter++
}
}
七、协程在 Android 中的应用
7.1 Android 协程最佳实践
在 Android 中使用协程的最佳实践:
- 使用
lifecycleScope和viewModelScope - 在
Dispatchers.Main上更新 UI - 使用
withContext(Dispatchers.IO)执行耗时操作 - 使用
flow和StateFlow处理异步数据流
class MyViewModel : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(Loading)
val uiState: StateFlow<UiState> = _uiState
init {
loadData()
}
private fun loadData() = viewModelScope.launch {
_uiState.value = Loading
try {
val data = withContext(Dispatchers.IO) {
repository.fetchData()
}
_uiState.value = Success(data)
} catch (e: Exception) {
_uiState.value = Error(e.message)
}
}
}
7.2 处理生命周期与内存泄漏
使用 repeatOnLifecycle 确保协程与 Activity/Fragment 生命周期同步:
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
updateUI(state)
}
}
}
}
}
八、性能优化与调试技巧
8.1 协程性能优化
优化协程性能的技巧:
- 避免不必要的调度器切换
- 使用
flow替代async/await进行连续异步操作 - 合理配置线程池大小
- 使用
buffer()和conflate()优化数据流处理
// 优化前
suspend fun loadData() = coroutineScope {
val data1 = async { fetchData1() }
val data2 = async { fetchData2() }
merge(data1.await(), data2.await())
}
// 优化后
fun loadDataFlow() = flow {
emit(fetchData1())
emit(fetchData2())
}.buffer(2)
8.2 协程调试技巧
调试协程的常用方法:
- 使用
CoroutineName为协程命名 - 启用调试模式(
Dispatchers.setMain) - 使用
runBlockingTest进行单元测试 - 分析线程转储文件识别阻塞操作
// 启用调试模式
Dispatchers.setMain(Dispatchers.Unconfined)
// 测试协程代码
@Test
fun testCoroutine() = runBlockingTest {
val result = viewModel.loadData()
assertEquals("Expected Result", result)
}
九、协程底层原理深入
9.1 协程状态机实现
Kotlin 编译器将挂起函数转换为状态机:
// 原始挂起函数
suspend fun simple(): Int {
delay(1000)
return 2
}
// 编译器生成的状态机伪代码
fun simple(continuation: Continuation<Int>): Any? {
when (continuation.label) {
0 -> {
continuation.label = 1
return delay(1000, continuation)
}
1 -> {
return 2
}
else -> throw IllegalStateException()
}
}
9.2 协程调度器工作原理
调度器通过 CoroutineDispatcher.dispatch() 方法将协程任务分配到线程:
abstract class CoroutineDispatcher : ContinuationInterceptor {
abstract fun dispatch(context: CoroutineContext, block: Runnable)
}
十、协程与其他异步方案对比
10.1 协程 vs RxJava
| 特性 | Kotlin 协程 | RxJava |
|---|---|---|
| 学习曲线 | 较低 | 较高 |
| 代码可读性 | 类似同步代码 | 复杂操作链 |
| 与 Kotlin 集成度 | 原生支持 | 需要额外依赖 |
| 背压处理 | 通过 Flow 原生支持 | 必须显式处理 |
| 内存占用 | 更低 | 较高 |
10.2 协程 vs CompletableFuture
| 特性 | Kotlin 协程 | CompletableFuture |
|---|---|---|
| 语法简洁性 | 更简洁 | 较冗长 |
| 取消机制 | 内置且完善 | 较弱 |
| 结构化并发 | 原生支持 | 需要手动管理 |
| 挂起/恢复机制 | 高效非阻塞 | 依赖线程池 |
十一、总结与最佳实践
11.1 协程使用最佳实践
- 优先使用结构化并发(
coroutineScope) - 避免使用
GlobalScope - 为长时间运行的协程指定明确的调度器
- 使用
withContext切换上下文 - 始终处理协程异常
- 使用 Flow 处理多个异步结果
11.2 何时使用协程
- 异步IO操作
- 并行计算
- 事件循环
- 生产者-消费者模式
- 复杂的异步工作流
通过掌握 Kotlin 协程,开发者可以编写更高效、更简洁、更易维护的异步代码,同时避免传统异步编程的复杂性和陷阱。协程不仅提升了代码质量,也大大改善了开发者的体验。