做Android开发,你肯定遇到过这样的痛点:发起网络请求时,直接在主线程调用会触发ANR;用Thread
+Handler
切换线程,代码绕得像团乱麻;处理并发任务时,还要手动管理线程池——这些问题,Kotlin协程能帮你一次性解决。它不是“新的线程框架”,而是更轻量、更易读的异步编程方案,让你用“写同步代码的方式做异步操作”。

协程到底能帮你解决什么问题
先举个最常见的Android场景:你需要从服务器拉取用户信息,然后更新UI。用传统方式,你得这么写:
// 传统Thread+Handler方式
Thread {
val user = api.getUserInfo() // 网络请求(耗时)
runOnUiThread {
tv_user.text = user.name // 更新UI
}
}.start()
这段代码不算复杂,但如果再加个“读取本地缓存”“处理错误”的逻辑,代码会瞬间变得臃肿。而用协程,你可以写成这样:
// 协程方式
lifecycleScope.launch {
val cache = withContext(Dispatchers.IO) { getCache() } // 读缓存(IO线程)
val user = withContext(Dispatchers.IO) { api.getUserInfo() } // 网络请求(IO线程)
tv_user.text = user.name // 直接更新UI(主线程)
}
没有Thread
,没有Handler
,代码线性且易读——这就是协程的核心价值:消除异步代码的“回调嵌套”,让异步逻辑像同步代码一样直观。
第一步:把协程引入Android项目
要在Android中用协程,首先得加依赖。打开你的app/build.gradle
(Module级别),添加以下两行:
dependencies {
// 协程核心库
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
// Android专属支持(绑定生命周期、主线程调度器)
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
}
注意:版本号要选最新的(截至2025年8月,1.7.3是稳定版)。添加后同步项目,协程就 ready 了。
用Launch启动第一个协程任务
协程不能“单独存在”,必须运行在协程作用域(CoroutineScope)中。Android官方推荐使用与生命周期绑定的Scope,比如:
– lifecycleScope
:绑定Activity/Fragment的生命周期,页面销毁时自动取消协程;
– viewModelScope
:绑定ViewModel的生命周期,ViewModel销毁时自动取消协程;
– (不推荐用GlobalScope
,它的生命周期和应用一致,容易导致内存泄漏)
比如,在Activity中启动第一个协程:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 用lifecycleScope启动协程(自动绑定Activity生命周期)
lifecycleScope.launch {
Log.d("Coroutine", "协程启动了:${Thread.currentThread().name}")
delay(1000) // 模拟耗时操作(不会阻塞主线程!)
Log.d("Coroutine", "协程完成了:${Thread.currentThread().name}")
}
}
}
运行这段代码,你会看到日志:
– 启动时的线程是main
(主线程);
– delay(1000)
不会阻塞主线程,因为协程的“挂起”是非阻塞的;
– 完成时的线程还是main
——说明协程默认运行在主线程,但能处理耗时操作?
不对,等一下:delay
是挂起函数(用suspend
修饰),它会“暂停”协程的执行,但不会占用线程。真正的耗时操作(比如网络请求、文件IO),需要用withContext
切换到后台线程。
withContext:切换线程的正确姿势
协程的“线程切换”是它最常用的功能之一。Android中,你需要记住三个核心调度器(Dispatcher):
调度器 | 适用场景 | 线程类型 |
---|---|---|
Dispatchers.Main |
Android主线程,更新UI | 单线程(UI线程) |
Dispatchers.IO |
网络请求、文件IO、数据库操作 | 线程池(多线程) |
Dispatchers.Default |
CPU密集型任务(比如排序、计算) | 线程池(多线程) |
比如,你想发起网络请求并更新UI,用withContext
切换线程:
lifecycleScope.launch {
// 1. 切换到IO线程做网络请求(耗时操作)
val user = withContext(Dispatchers.IO) {
api.getUserInfo() // 假设api.getUserInfo()是网络请求
}
// 2. 自动切回Main线程,更新UI
tv_user.text = user.name
}
这里的关键是:withContext
会暂停当前协程,在指定的调度器上执行任务,完成后自动切回原线程(比如Main线程)。你不用手动调用runOnUiThread
,代码更简洁。
async/await:处理并发任务的利器
如果需要同时执行多个异步任务(比如同时请求“用户信息”和“订单列表”),async
+await
是最优解。它能让多个任务“并行执行”,最后合并结果。
比如,你需要同时拉取两个接口的数据:
viewModelScope.launch {
// 1. 启动两个并发任务(都在IO线程)
val userDeferred = async(Dispatchers.IO) { api.getUserInfo() }
val ordersDeferred = async(Dispatchers.IO) { api.getOrders() }
// 2. 等待两个任务完成(不会阻塞线程)
val user = userDeferred.await()
val orders = ordersDeferred.await()
// 3. 合并结果更新UI
_uiState.value = UiState.Success(user, orders)
}
– async
会返回一个Deferred
对象(类似“未来的结果”);
– await
会“等待”任务完成,并获取结果;
– 两个任务是并行执行的,总耗时等于“最长的那个任务”的时间(比如请求用户用1秒,请求订单用1.5秒,总耗时1.5秒),比串行执行(2.5秒)快很多。
协程的取消与异常处理
协程的“可取消性”是它的重要特性——当用户退出页面时,你需要取消未完成的协程,避免内存泄漏或无效请求。
1. 自动取消:用生命周期绑定的Scope
最推荐的方式是使用lifecycleScope
或viewModelScope
——当页面销毁/ViewModel销毁时,这些Scope会自动取消所有子协程。比如:
// 在ViewModel中使用viewModelScope
class MainViewModel : ViewModel() {
fun fetchData() {
viewModelScope.launch {
val data = api.fetchData() // 网络请求
_data.value = data
}
}
}
当用户退出Activity时,ViewModel会被销毁,viewModelScope
中的协程会自动取消,无需手动处理。
2. 手动取消:用Job
对象
如果需要手动取消协程(比如用户点击“取消按钮”),可以保存Job
对象:
class MainActivity : AppCompatActivity() {
private var fetchJob: Job? = null
fun startFetch() {
fetchJob = lifecycleScope.launch {
val data = api.fetchData()
tv_data.text = data
}
}
fun cancelFetch() {
fetchJob?.cancel() // 手动取消协程
}
}
3. 异常处理:两种常用方式
协程的异常处理和同步代码类似,用try-catch
或CoroutineExceptionHandler
:
– 方式1:局部异常处理(try-catch
):
lifecycleScope.launch {
try {
val data = withContext(Dispatchers.IO) { api.fetchData() }
tv_data.text = data
} catch (e: IOException) {
// 处理网络错误
tv_error.text = "网络连接失败"
} catch (e: Exception) {
// 处理其他错误
tv_error.text = "加载失败"
}
}
– 方式2:全局异常处理(CoroutineExceptionHandler
):
如果多个协程需要统一处理异常,可以用CoroutineExceptionHandler
:
// 1. 创建全局异常处理器
val globalExceptionHandler = CoroutineExceptionHandler { _, throwable ->
Log.e("CoroutineError", "全局异常: ${throwable.message}")
// 比如弹Toast提示用户
Toast.makeText(this, "加载失败", Toast.LENGTH_SHORT).show()
}
// 2. 在启动协程时传入处理器
lifecycleScope.launch(globalExceptionHandler) {
val data = withContext(Dispatchers.IO) { api.fetchData() }
tv_data.text = data
}
实战:用协程重构网络请求代码
最后,我们用一个真实的Android场景——“网络请求+更新UI”——来展示协程的威力。假设你用Retrofit
做网络请求,传统方式需要用Callback
:
// 传统Retrofit Callback方式
api.getUserInfo().enqueue(object : Callback<User> {
override fun onResponse(call: Call<User>, response: Response<User>) {
if (response.isSuccessful) {
runOnUiThread { tv_user.text = response.body()?.name }
}
}
override fun onFailure(call: Call<User>, t: Throwable) {
runOnUiThread { tv_error.text = "加载失败" }
}
})
用协程重构后,代码会简洁很多:
1. 第一步:给Retrofit接口加suspend
修饰符(Retrofit 2.6.0+支持协程):
interface UserApi {
@GET("user/info")
suspend fun getUserInfo(): Response<User> // 加suspend修饰
}
2. 第二步:用协程发起请求:
lifecycleScope.launch {
val response = withContext(Dispatchers.IO) { api.getUserInfo() }
if (response.isSuccessful) {
tv_user.text = response.body()?.name
} else {
tv_error.text = "加载失败(${response.code()})"
}
}
没有Callback
,没有runOnUiThread
,代码线性且易维护——这就是协程的魅力。
最后提醒:协程的“避坑指南”
- 不要用
GlobalScope
:它的生命周期和应用一致,容易导致内存泄漏(比如Activity销毁了,协程还在跑); - 不要嵌套
withContext
:比如withContext(Dispatchers.IO) { withContext(Dispatchers.IO) { ... } }
,会浪费线程池资源; - 用
lifecycleScope
/viewModelScope
:优先使用与生命周期绑定的Scope,自动管理协程生命周期; - 不要在
Main
线程做耗时操作:即使协程默认跑在Main线程,也不能在里面做网络请求或文件IO——一定要用withContext(Dispatchers.IO)
切换线程。
学会协程不是“为了用新框架”,而是让你的代码更简洁、更易读、更易维护。它解决的是Android开发中最核心的“异步编程痛点”——如果你还在用Thread
+Handler
或者AsyncTask
,赶紧试试协程吧!
原创文章,作者:,如若转载,请注明出处:https://zube.cn/archives/259