Kotlin协程极简教程:5分钟学完关键知识点
什么是协程?
协程(Coroutine)是Kotlin提供的一种轻量级线程管理工具。你可以把它理解为一个更高效、更易用的线程替代方案。但与线程不同的是:
- 轻量级:协程的创建和切换成本远低于线程
- 挂起而非阻塞:协程挂起时不会阻塞底层线程
- 结构化并发:提供了更好的生命周期管理和取消机制
让我们通过一个简单的代码示例看看协程运行的线程情况:
fun printThreadInfo() {CoroutineScope(Dispatchers.Main).launch { println("协程1开始 - 线程: ${Thread.currentThread().name}")delay(1000)withContext(Dispatchers.IO) {println("协程1切换到IO - 线程: ${Thread.currentThread().name}")delay(1000)}println("协程1恢复 - 线程: ${Thread.currentThread().name}")}CoroutineScope(Dispatchers.Default).launch { println("协程2开始 - 线程: ${Thread.currentThread().name}")delay(1500)println("协程2结束 - 线程: ${Thread.currentThread().name}")}
}
运行结果可能如下:
协程1开始 - 线程: main
协程2开始 - 线程: DefaultDispatcher-worker-1
协程1切换到IO - 线程: DefaultDispatcher-worker-2
协程1恢复 - 线程: main
协程2结束 - 线程: DefaultDispatcher-worker-1
可以看到,同一个协程在不同阶段可能运行在不同的线程上,协程之间的执行也是并发的。
为什么要使用协程?
在安卓开发中,我们经常需要处理异步操作,比如网络请求、数据库访问等。传统方式(如回调、RxJava)会导致代码嵌套层级深、错误处理复杂等问题。协程通过"以同步方式写异步代码"的理念,完美解决了这些问题。
让我们通过多个网络请求的实战对比来看看协程的优势。
实战对比:多个网络请求处理
场景描述
我们需要依次完成以下操作:
- 获取用户基本信息
- 根据用户ID获取订单列表
- 根据订单中的商品ID获取商品详情
传统回调方式
class OrderViewModel : ViewModel() {private val repository = OrderRepository()fun loadUserOrderDetails() {// 第一步:获取用户信息repository.getUserInfo(object : Callback<User> {override fun onSuccess(user: User) {// 第二步:获取订单列表repository.getOrders(user.id, object : Callback<List<Order>> {override fun onSuccess(orders: List<Order>) {// 第三步:获取每个订单的商品详情val productIds = orders.flatMap { it.productIds }repository.getProducts(productIds, object : Callback<List<Product>> {override fun onSuccess(products: List<Product>) {// 最终合并所有数据val result = combineData(user, orders, products)// 更新UI}override fun onFailure(error: Throwable) {// 处理错误}})}override fun onFailure(error: Throwable) {// 处理错误}})}override fun onFailure(error: Throwable) {// 处理错误}})}
}
这种嵌套回调的方式不仅难以阅读,而且错误处理分散在各个层级,维护起来非常困难。
协程方式
class OrderViewModel : ViewModel() {private val repository = OrderRepository()fun loadUserOrderDetails() {viewModelScope.launch(Dispatchers.IO) {// 第一步:获取用户信息(看起来像同步调用)val user = repository.getUserInfo()// 第二步:获取订单列表val orders = repository.getOrders(user.id)// 第三步:并行获取所有商品详情val productIds = orders.flatMap { it.productIds }val productsDeferred = productIds.map { productId ->async { repository.getProductDetails(productId) }}val products = productsDeferred.awaitAll()// 最终合并所有数据val result = combineData(user, orders, products)// 更新UI}}
}
协程版本的优势:
- 代码结构清晰,顺序执行
- 错误处理集中在一处
- 可以轻松实现并行请求
- 没有回调嵌套,可读性极佳
更复杂的场景:多线程切换
class MultiThreadViewModel(private val userRepository: UserRepository,private val imageRepository: ImageRepository
) : ViewModel() {// 加载用户完整资料(包含头像处理)fun loadUserProfile(userId: String) {viewModelScope.launch {try {// 1. 主线程开始println("开始加载 - 线程: ${Thread.currentThread().name}")_loading.value = true// 2. 切换到IO线程获取用户基本信息val user = withContext(Dispatchers.IO) {println("获取用户信息 - 线程: ${Thread.currentThread().name}")userRepository.getUserInfo(userId) // 挂起函数}// 3. 回到主线程更新UI(用户基本信息)withContext(Dispatchers.Main) {println("更新用户信息UI - 线程: ${Thread.currentThread().name}")_userInfo.value = user}// 4. 再次切换到IO线程处理用户头像val processedAvatar = withContext(Dispatchers.IO) {println("处理用户头像 - 线程: ${Thread.currentThread().name}")// 假设这是耗时的图像处理操作val originalAvatar = imageRepository.downloadAvatar(user.avatarUrl)imageRepository.processAvatar(originalAvatar) // 图像处理}// 5. 最后一次回到主线程更新头像withContext(Dispatchers.Main) {println("更新头像UI - 线程: ${Thread.currentThread().name}")_avatar.value = processedAvatar_loading.value = false}} catch (e: Exception) {// 统一错误处理_error.value = e_loading.value = false}}}
}
2. 超时处理
fun loadDataWithTimeout() {viewModelScope.launch {try {val data = withTimeout(5000) { // 5秒超时repository.loadData()}// 处理数据} catch (e: TimeoutCancellationException) {// 处理超时} catch (e: Exception) {// 处理其他错误}}
}
接下来说说协程的作用域:
协程作用域详解:正确选择与使用
在Kotlin协程中,作用域(CoroutineScope)是管理协程生命周期和结构化并发的核心概念。不同的作用域适用于不同的场景,正确选择作用域对避免内存泄漏和资源浪费至关重要。
1. GlobalScope - 全局作用域
定义:生命周期与整个应用一致的全局作用域
特点:
- 启动的协程会一直运行直到完成,不会被自动取消
- 不推荐在常规Android开发中使用
- 适合应用级别的长时间运行任务
使用场景:
// 示例:应用级别的日志上传任务
fun uploadLogsPeriodically() {GlobalScope.launch {while (true) {delay(24 * 60 * 60 * 1000) // 每24小时执行一次uploadLogsToServer()}}
}
注意事项:
- 容易造成内存泄漏(不会随Activity/Fragment销毁而取消)
- 缺乏结构化并发带来的优势
- 在Android中应尽量避免使用
2. CoroutineScope - 自定义作用域
定义:通过CoroutineScope()
工厂函数创建的自定义作用域
特点:
- 需要手动管理生命周期
- 可以自定义协程上下文
- 适合在非Android组件中使用
使用场景:
// 示例:在后台服务中使用
class MyBackgroundService : Service() {private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {serviceScope.launch {// 执行后台任务processIncomingData()}return START_STICKY}override fun onDestroy() {super.onDestroy()serviceScope.cancel() // 手动取消所有协程}
}
3. viewModelScope - ViewModel专属作用域
定义:AndroidX为ViewModel提供的扩展属性
特点:
- 生命周期与ViewModel绑定
- ViewModel清除时会自动取消所有协程
- 默认使用Dispatchers.Main.immediate
使用场景:
class MyViewModel : ViewModel() {fun fetchData() {viewModelScope.launch {// 主线程安全操作_loading.value = truetry {val data = withContext(Dispatchers.IO) {repository.fetchData()}_data.value = data} catch (e: Exception) {_error.value = e} finally {_loading.value = false}}}
}
4. lifecycleScope - 生命周期感知作用域
定义:AndroidX为LifecycleOwner提供的扩展属性
特点:
- 生命周期与Activity/Fragment绑定
- 提供多种启动时机(如LAUNCH_WHEN_CREATED等)
- 默认使用Dispatchers.Main.immediate
使用场景:
class MyFragment : Fragment() {override fun onViewCreated(view: View, savedInstanceState: Bundle?) {super.onViewCreated(view, savedInstanceState)// 当至少STARTED状态时执行lifecycleScope.launch {// 更新UI的操作}// 仅在RESUMED状态时执行lifecycleScope.launchWhenResumed {// 需要界面可见才执行的操作}}
}
需要注意的是,viewModelScope和lifecycleScope是Android扩展作用域(本质是CoroutineScope的封装),这一点其实从源码可以看得出。
CoroutineScope(接口)↑+-------+-------+| |
GlobalScope(对象) AbstractCoroutineScope(抽象类)↑+-----------+-----------+| |CloseableCoroutineScope LifecycleCoroutineScope(viewModelScope使用) (lifecycleScope实现)
什么是协程挂起函数?
挂起函数(Suspend Function)是Kotlin协程的核心概念之一,它允许我们在不阻塞线程的情况下暂停协程的执行。当我们在协程中调用一个挂起函数时,实际上发生了一个非常重要的操作:协程的挂起。
suspend fun fetchData(): String {delay(1000) // 模拟耗时操作return "Data loaded"
}
挂起函数的本质
当协程遇到挂起函数时,会发生以下过程:
- 协程挂起:当前协程的执行被暂停,控制权返回给调用者
- 脱离当前上下文:协程从当前的调度器/线程上"脱离"下来
- 执行挂起逻辑:挂起函数内部的代码开始执行(可能在相同线程,也可能在不同线程)
- 恢复执行:当挂起函数完成后,协程会被重新调度到适当的线程继续执行
关键理解点:挂起函数不会阻塞线程,而是让协程自身暂停,线程可以自由地去执行其他任务。
调度器(Dispatcher)的角色
调度器决定了协程在哪个或哪些线程上运行。Kotlin提供了几种标准调度器:
Dispatchers.Default
:CPU密集型任务Dispatchers.IO
:IO密集型任务Dispatchers.Main
:主线程(Android UI线程)Dispatchers.Unconfined
:不限定特定线程
// 示例:在不同调度器间切换
GlobalScope.launch(Dispatchers.Main) {val data = withContext(Dispatchers.IO) {fetchFromNetwork() // 在IO线程执行}updateUI(data) // 回到Main线程
}
withContext与上下文切换
withContext
是一个非常重要的挂起函数,它允许我们在不创建新协程的情况下切换协程的上下文(主要是调度器)。当然你可以理解为它最重要的作用就是在不开新的协程情况下去切换线程
理解withContext
的关键:
- 它是一个挂起函数,会暂停当前协程的执行
- 它切换上下文执行给定的代码块
- 完成后会恢复原始上下文并返回结果
suspend fun loadData() = withContext(Dispatchers.IO) {// 这里在IO线程执行fetchFromDatabase() + fetchFromNetwork()
}
// 调用loadData()后会自动回到原来的调度器
为了更好的理解挂起函数,这里有一个案例:
CoroutineScope.launch(Dispatchers.Main) { Log.d(TAG, "Parent coroutine starts") launch(Dispatchers.IO) { delay(2000L) Log.d(TAG, "Child1 coroutine finished") } launch(Dispatchers.Main) { delay(2000L) Log.d(TAG, "Child2 coroutine finished") } delay(1000L) Log.d(TAG, "Parent coroutine continues")
}
由于协程里面开了两个新的协程,而且这两个新的协程都是延时两秒执行,所以日志可能是这样的:
Parent coroutine starts
Parent coroutine continues
Child1 coroutine finished
Child2 coroutine finished// 或者是这样的
Parent coroutine starts
Parent coroutine continues
Child2 coroutine finished
Child1 coroutine finished
在中间加一个挂起函数
CoroutineScope.launch(Dispatchers.Main) { Log.d(TAG, "Parent coroutine starts") launch(Dispatchers.IO) { delay(1000L) Log.d(TAG, "Child1 coroutine finished") } withContext(Dispatchers.IO){ delay(1000L) Log.d(TAG, "withContext at io task finish") } launch(Dispatchers.Main) { delay(1000L) Log.d(TAG, "Child2 coroutine finished") } delay(500L) Log.d(TAG, "Parent coroutine continues")
}
那它们的执行顺序,或者说日志一定是这样的
Parent coroutine starts
Child1 coroutine finished
withContext at io task finish
Parent coroutine continues
Child2 coroutine finished
这也证明了前面说的,挂起函数相当于让父协程暂停执行后面的代码,先执行完我挂起函数里面的逻辑之后再恢复。
协程Job
什么是协程的Job?
在Kotlin协程中,Job
是一个表示协程生命周期的重要接口。简单来说,Job
就是协程的一个句柄(handle),通过它我们可以控制和监视协程的执行状态。
当你使用launch
构建器启动一个协程时,它会返回一个Job
对象:
val job = scope.launch { // 协程代码
}// 取消协程
job.cancel()
这个Job
对象代表了当前启动的协程,我们可以通过它来管理协程的生命周期。
Job的主要功能
Job
提供了以下主要功能:
- 取消协程:通过
cancel()
方法可以取消协程的执行 - 等待完成:通过
join()
方法可以挂起当前协程,直到目标协程完成 - 状态查询:可以检查协程是否处于活动状态、是否已完成或是否被取消
- 父子关系:可以建立协程之间的父子关系,实现结构化并发
Job的生命周期状态
一个Job在其生命周期中会经历以下状态:
- New (新建状态,使用
Job()
构造函数创建但尚未启动) - Active (活跃状态,协程正在执行)
- Completing (完成中状态,协程即将完成)
- Cancelling (取消中状态,协程正在取消)
- Cancelled (已取消状态)
- Completed (已完成状态)
Job的取消机制
当调用cancel()
时,协程会:
- 进入取消状态
- 取消其所有子协程
- 在下一个挂起点抛出
CancellationException
- 执行finally块中的清理代码
需要注意的是,协程的取消是协作式的(collaborative),这意味着协程代码需要检查取消状态或调用挂起函数才能响应取消请求。
组合使用cancel和join
通常我们会将cancel()
和join()
组合使用,以确保协程完全停止:
job.cancel() // 请求取消
job.join() // 等待取消完成
Kotlin提供了一个便捷的扩展函数cancelAndJoin()
来合并这两个操作:
job.cancelAndJoin()
Job与SupervisorJob
1. Job 的作用
- 协程父子关系:
Job
可以形成父子关系,父协程取消时,所有子协程也会被取消。 - 手动控制协程:
val job = scope.launch { /* 子协程 */ } job.cancel() // 取消单个协程 scope.cancel() // 取消整个作用域下的所有协程
2. Job 和 SupervisorJob 的区别
Job
和 SupervisorJob
都是协程的“工作”对象,但它们的异常传播机制不同:
特性 | Job | SupervisorJob |
---|---|---|
异常传播 | 子协程失败会取消父协程和兄弟协程 | 子协程失败不会影响父协程和兄弟协程 |
适用场景 | 需要严格的任务依赖关系 | 需要独立运行的任务(如 UI 更新、日志记录) |
创建方式 | Job() | SupervisorJob() |
2.1 Job 的异常传播(默认行为)
如果使用普通 Job
,子协程抛出异常时,整个协程作用域都会被取消:
val scope = CoroutineScope(Job() + Dispatchers.Default)scope.launch {delay(100)throw RuntimeException("Child 1 failed!") // 取消整个 scope
}scope.launch {delay(200)println("Child 2") // 不会执行,因为 scope 已被取消
}
结果:
Exception in thread "DefaultDispatcher-worker-1" RuntimeException: Child 1 failed!
👉 所有子协程都会被取消。
2.2 SupervisorJob 的异常传播
如果使用 SupervisorJob
,子协程的失败不会影响其他协程:
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)scope.launch {delay(100)throw RuntimeException("Child 1 failed!") // 仅自身失败
}scope.launch {delay(200)println("Child 2") // 仍然执行
}
结果:
Exception in thread "DefaultDispatcher-worker-1" RuntimeException: Child 1 failed!
Child 2
👉 只有抛出异常的协程被取消,其他协程不受影响。
协程的启动方式:launch与async
在Kotlin协程中,主要有两种启动协程的方式:launch
和async
。它们都用于启动协程,但在用途和行为上有显著区别。
launch - 启动不需要返回结果的协程
launch
用于启动一个不需要返回结果的协程,通常用于执行一些"即发即弃"的后台任务。
viewModelScope.launch {// 执行一些后台操作fetchDataFromNetwork()updateUI()
}
特点:
- 返回一个
Job
对象,可用于取消协程或等待其完成 - 不返回任何结果
- 适合执行不需要返回值的操作
- 异常会向上传播,可能导致协程取消
async - 启动需要返回结果的协程
async
用于启动一个需要返回结果的协程,返回一个Deferred
对象,可以通过await()
获取结果。
viewModelScope.launch {val deferredResult: Deferred<String> = async {fetchDataFromNetwork()}val result = deferredResult.await()updateUI(result)
}
特点:
- 返回一个
Deferred
对象,它是Job
的子类 - 可以通过
await()
获取结果 - 适合并行执行多个任务并组合结果
- 异常不会立即传播,只有在调用
await()
时才会抛出
async/await在ViewModel中的使用场景
在ViewModel中,async
和await
配合使用特别适合以下场景:
1. 并行执行多个网络请求
fun loadUserData(userId: String) {viewModelScope.launch {val userDeferred = async { userRepository.getUser(userId) }val postsDeferred = async { postRepository.getPosts(userId) }val user = userDeferred.await()val posts = postsDeferred.await()_userData.value = UserData(user, posts)}
}
2. 组合多个数据源
fun loadDashboardData() {viewModelScope.launch {try {val statsDeferred = async { analyticsRepository.getStats() }val newsDeferred = async { newsRepository.getLatestNews() }val notificationsDeferred = async { notificationRepository.getUnread() }val dashboardData = DashboardData(stats = statsDeferred.await(),news = newsDeferred.await(),notifications = notificationsDeferred.await())_dashboardData.value = dashboardData} catch (e: Exception) {_errorMessage.value = "Failed to load dashboard data"}}
}
3. 超时处理
fun loadDataWithTimeout() {viewModelScope.launch {try {val data = withTimeout(5000) {async { dataRepository.loadData() }.await()}_data.value = data} catch (e: TimeoutCancellationException) {_errorMessage.value = "Request timed out"}}
}
最佳实践
- 异常处理:使用
try-catch
包裹await()
调用或整个协程块 - 结构化并发:始终在ViewModel中使用
viewModelScope
,它会在ViewModel清除时自动取消所有子协程 - 避免全局Scope:不要在ViewModel中使用
GlobalScope
- 取消敏感操作:检查
isActive
或使用ensureActive()
处理可取消操作 - 主线程安全:使用
withContext(Dispatchers.IO)
等切换调度器
launch与async的选择指南
场景 | 选择 |
---|---|
不需要结果 | launch |
需要结果 | async + await |
并行多个任务 | 多个async + awaitAll |
即发即弃的任务 | launch |
需要组合结果 | async |
通过合理使用launch
和async
,可以构建出高效、响应式的ViewModel实现,同时保持代码的清晰和可维护性。