Android ---【Kotlin入门】为什么要用协程?Kotlin 协程原理 + 实战深度解析
Kotlin 协程(coroutine)学习
以下干货满满,掌握以下内容一定会对你在项目开发中有所帮助,记得收藏!!!
文章目录
一、什么是协程?
->协程是 “轻量级的任务 / 执行单元”,它本身不是线程
->协程必须跑在线程上,多个协程可以在同一个线程上交替执行,协程是个并发管理工具。
->线程是操作系统调度的,协程是 由Kotlin 协程库调度的(在用户态切换,开销很小)
可以简单理解成:
线程:大卡车,重、贵、切换成本高
协程:卡车上的包裹,一个卡车可以装很多包裹,调度由 “物流公司”(协程库)来安排
所以:
开启一个协程 ≠ 一定新建一个线程
二、为什么要用协程?
使用协程是为了解决 回调地狱(Callback Hell)
什么是回调地狱?它本质是多层异步回调嵌套,导致代码横向膨胀、可读性极差、异常处理困难。
2.1 回调地狱的详细示例(模拟 App 中 “登录→获取用户信息→获取用户订单” 流程)
传统的写法:
// 模拟网络请求工具类(模拟Retrofit的回调风格)
object HttpUtils {
// 第一步:登录接口(异步,回调返回token)
fun login(username: String, password: String, callback: (String?, Exception?) -> Unit) {
Thread {
Thread.sleep(1000) // 模拟网络耗时
if (username == "user" && password == "123456") {
callback("token_123456", null) // 登录成功返回token
} else {
callback(null, Exception("登录失败:用户名或密码错误"))
}
}.start()
}
// 第二步:根据token获取用户信息(异步,依赖第一步的token)
fun getUserInfo(token: String, callback: (String?, Exception?) -> Unit) {
Thread {
Thread.sleep(800) // 模拟网络耗时
if (token.startsWith("token_")) {
callback("用户信息:id=1, 姓名=张三", null) // 获取用户信息成功
} else {
callback(null, Exception("token无效,获取用户信息失败"))
}
}.start()
}
// 第三步:根据用户信息获取订单(异步,依赖第二步的用户信息)
fun getOrderList(userInfo: String, callback: (List<String>?, Exception?) -> Unit) {
Thread {
Thread.sleep(600) // 模拟网络耗时
if (userInfo.contains("id=1")) {
callback(listOf("订单1:外卖", "订单2:快递"), null) // 获取订单成功
} else {
callback(null, Exception("用户信息无效,获取订单失败"))
}
}.start()
}
}
// 调用侧:传统回调方式(典型的回调地狱)
fun main() {
println("开始执行异步流程...")
// 第一步:登录
HttpUtils.login("user", "123456") { token, loginError ->
if (loginError != null) {
println("登录失败:${loginError.message}")
return@login
}
println("登录成功,token:$token")
// 第二步:根据token获取用户信息(嵌套第一层)
HttpUtils.getUserInfo(token!!) { userInfo, userError ->
if (userError != null) {
println("获取用户信息失败:${userError.message}")
return@getUserInfo
}
println("获取用户信息成功:$userInfo")
// 第三步:根据用户信息获取订单(嵌套第二层)
HttpUtils.getOrderList(userInfo!!) { orderList, orderError ->
if (orderError != null) {
println("获取订单失败:${orderError.message}")
return@getOrderList
}
println("获取订单成功:$orderList")
println("整个异步流程执行完成!")
}
}
}
// 主线程不会阻塞,这里会先打印
println("主线程继续执行...")
Thread.sleep(3000) // 等待异步流程完成
}
回调地狱的核心问题:
1.代码嵌套层级深(上面只有 3 层,实际项目可能 5-10 层),像 “金字塔” 一样横向膨胀,可读性极差;
2.异常处理分散(每一层都要单独 catch 错误),容易遗漏;
3.代码逻辑被切割,无法像同步代码一样线性阅读和调试。
用协程重构上述代码(彻底消除回调嵌套)
import kotlinx.coroutines.*
// 第一步:将异步回调方法封装为挂起函数(suspend)
object HttpCoroutineUtils {
// 登录 - 封装为挂起函数(用suspendCancellableCoroutine适配回调)
suspend fun login(username: String, password: String): String {
return suspendCancellableCoroutine { continuation ->
HttpUtils.login(username, password) { token, error ->
if (error != null) {
continuation.resumeWithException(error) // 异常抛出去
} else {
continuation.resume(token!!) // 成功返回结果
}
}
}
}
// 获取用户信息 - 封装为挂起函数
suspend fun getUserInfo(token: String): String {
return suspendCancellableCoroutine { continuation ->
HttpUtils.getUserInfo(token) { userInfo, error ->
if (error != null) {
continuation.resumeWithException(error)
} else {
continuation.resume(userInfo!!)
}
}
}
}
// 获取订单 - 封装为挂起函数
suspend fun getOrderList(userInfo: String): List<String> {
return suspendCancellableCoroutine { continuation ->
HttpUtils.getOrderList(userInfo) { orderList, error ->
if (error != null) {
continuation.resumeWithException(error)
} else {
continuation.resume(orderList!!)
}
}
}
}
}
// 调用侧:协程方式(线性代码,无嵌套)
fun main() = runBlocking { // 启动协程作用域(main函数用runBlocking)
println("开始执行协程异步流程...")
// 用launch启动子协程(不阻塞主线程)
val job = launch(Dispatchers.IO) { // IO线程执行网络请求
try {
// 第一步:登录(同步写法,实际是异步执行)
val token = HttpCoroutineUtils.login("user", "123456")
println("登录成功,token:$token")
// 第二步:获取用户信息(无嵌套,线性执行)
val userInfo = HttpCoroutineUtils.getUserInfo(token)
println("获取用户信息成功:$userInfo")
// 第三步:获取订单(无嵌套,线性执行)
val orderList = HttpCoroutineUtils.getOrderList(userInfo)
println("获取订单成功:$orderList")
println("整个协程流程执行完成!")
} catch (e: Exception) {
// 统一异常处理(所有步骤的异常都能捕获)
println("流程执行失败:${e.message}")
}
}
// 主线程继续执行
println("主线程继续执行...")
job.join() // 等待协程执行完成(可选)
println("程序结束")
}
这样写异步代码就像写同步代码(顺序写、但不会阻塞线程)
还有我们之前用的网络请求Retrofit也是回调地狱,onResponse,onFailure
三、协程的核心基础概念
- 协程作用域(CoroutineScope)
协程必须在作用域中启动,作用域管理协程生命周期,避免内存泄漏。
| 常用作用域 | 适用场景 | 生命周期特点 | 自动取消 / 销毁特性 |
|---|---|---|---|
| lifecycleScope | Activity/Fragment | 绑定页面生命周期,自动销毁 | 自动取消:Lifecycle 走到 DESTROYED 时,作用域内所有协程自动取消 |
| viewModelScope | ViewModel | 绑定VM生命周期 | 自动取消:ViewModel.onCleared () 时自动取消所有协程 |
| MainScope | 自定义场景(如非页面类、手动管理) | 需手动在onDestroy取消 | 不会自动取消:必须手动调用 cancel() (如在 onDestroy 中) |
| GlobalScope | 全局任务 | 绑定整个应用进程生命周期 ,进程级,慎用(易内存泄漏) | 不会自动取消:进程不死,协程不销毁 |
- 调度器(Dispatcher):指定协程运行线程
调度器决定协程在哪个线程执行,核心有 4 种:
| 调度器 | 解释 | 适用场景 |
|---|---|---|
| Dispatchers.Main | 主线程 | 用于更新 UI |
| Dispatchers.IO | 非主线程 | IO 密集型任务(网络 / 文件读写),固定的64个线程的线程池,CPU基本不工作,I/O的磁盘工作 |
| Dispatchers.Default | 非主线程 | 计算密集型任务(排序 / 加密),线程数 = CPU 核心数 |
| Dispatchers.Unconfined | 非主线程 | 不指定线程,极少使用 |
@JvmStatic
public actual val Default: CoroutineDispatcher = DefaultScheduler
@JvmStatic
public actual val Main: MainCoroutineDispatcher get()= MainDispatcherLoader.dispatcher
@JvmStatic
public actual val Unconfined: CoroutineDispatcher = Kotlinx.coroutines.Unconfined
@JvmStatic
public val IO: CoroutineDispatcher = DefaultIoScheduler
// Dispatchers.IO
internal object DefaultIoScheduler : ExecutorCoroutineDispatcher(), Executor {
private val default = UnlimitedIoScheduler.limitedParallelism(
systemProp(
IO_PARALLELISM_PROPERTY_NAME,
64.coerceAtLeast(AVAILABLE_PROCESSORS)
)
)
}
- 协程构建器:启动协程的两种方式
| 构建器 | 返回值 | 适用场景 |
|---|---|---|
| launch | Job | 无返回值的任务(如更新UI) |
| async | Deferred | 有返回值的任务(如并行请求) |
示例:并行任务合并结果
lifecycleScope.launch {
val def1 = async(Dispatchers.IO) { getList1() }
val def2 = async(Dispatchers.IO) { getList2() }
val result = def1.await() + def2.await() // 合并结果
}
四、协程是不是就是开了个子线程?
不是。协程是跑在线程上的 “轻量级任务”,多个协程可以复用同一个线程。
协程 ≠ 线程,协程是任务,线程是执行载体
五、在主线程上的协程有什么意义?
lifecycleScope.launch(Dispatchers.Main) {
// 这里在主线程
}
看起来好像 “没啥用”,因为本来就在主线程,为什么还要开协程?
它的意义主要有这几个:
->可以在主线程里 “挂起”,然后再回来
->结构化并发、自动取消
->可以组合多个 suspend 任务,而不阻塞主线程
3.1 可以在主线程里 “挂起”,然后再回来
协程的关键能力是 挂起与恢复(suspend & resume)。在主线程上的协程,遇到 suspend 函数时,可以:
暂停当前协程的执行去干别的事(比如让主线程继续处理 UI 事件、刷新界面),等 suspend 函数完成后,自动切回主线程恢复执行,比如:
lifecycleScope.launch(Dispatchers.Main) {
// 1. 这里在主线程
val data = fetchData() // 这是一个 suspend 函数,可能切到 IO 线程
// 3. 自动回到主线程,继续执行
textView.text = data
}
suspend fun fetchData(): String = withContext(Dispatchers.IO) {
// 2. 这里在 IO 线程
delay(1000)
"result"
}
3.2 结构化并发、自动取消
在主线程上的协程,仍然享受协程的 “生命周期管理” 能力。协程会在 onDestroy 时自动取消,即便它主要是在主线程干活,也会被取消。
3.2 可以组合多个 suspend 任务,而不阻塞主线程
在主线程协程里,可以调用多个挂起函数,它们内部可以切线程,但外部看起来是顺序的:
lifecycleScope.launch(Dispatchers.Main) {
showLoading() // 主线程:显示加载框
val user = repo.getUser() // suspend,内部切 IO
val posts = repo.getPosts(user.id) // 再切 IO
hideLoading() // 主线程:隐藏加载框
updateUi(posts) // 主线程:更新界面
}
六、挂起函数执行完会自动切回主线程?
准确的说法是:
挂起函数执行完之后,会恢复到它被调用时所在的协程上下文(包括调度器)。
会不会回到主线程,取决于调用它的那个协程当时在哪个调度器上。
// 情况 A:在主线程协程里调用挂起函数
lifecycleScope.launch(Dispatchers.Main) {
// 这里在 Main
val data = fetchData() // 挂起函数
// 恢复后,这里还是在 Main,可以直接更新 UI
textView.text = data
}
// 情况 B:在 IO 协程里调用同一个挂起函数
lifecycleScope.launch(Dispatchers.IO) {
// 这里在 IO
val data = fetchData() // 同一个挂起函数
// 恢复后,这里还是在 IO,不能更新 UI
}
如果你是在 Dispatchers.Main 的协程里调用挂起函数,那恢复后自然就在主线程
如果你是在 Dispatchers.IO 的协程里调用,恢复后还在 IO 线程
七、挂起和阻塞的区别,什么叫挂起,什么叫阻塞?
挂起就相当于delay(12000)
阻塞就相当于Thread.sleep(12000)
挂起之后你可以干其他事,但是阻塞之后你就要等,不能干其他事
什么叫挂起:当前协程不再占用它正在工作的这个线程
例子 1:用Thread.sleep(12000)(阻塞)
import kotlinx.coroutines.*
import java.lang.Thread.sleep
fun main() = runBlocking {
// 在主线程的协程作用域中执行
println("主线程开始:${Thread.currentThread().name}")
launch(Dispatchers.Main) { // 主线程协程
println("协程1开始(sleep):${Thread.currentThread().name}")
sleep(12000) // 阻塞主线程12秒
println("协程1结束(sleep):${Thread.currentThread().name}")
}
launch(Dispatchers.Main) { // 另一个主线程协程
println("协程2开始:${Thread.currentThread().name}") // 这行代码会等12秒后才执行!
println("协程2结束:${Thread.currentThread().name}")
}
println("主线程代码执行完:${Thread.currentThread().name}")
}
主线程开始:main
协程1开始(sleep):main
// 这里卡12秒!协程2的代码完全不执行,主线程被sleep阻塞
协程1结束(sleep):main
协程2开始:main
协程2结束:main
主线程代码执行完:main
例子 2:用delay(12000)(挂起)
import kotlinx.coroutines.*
fun main() = runBlocking {
println("主线程开始:${Thread.currentThread().name}")
launch(Dispatchers.Main) { // 主线程协程
println("协程1开始(delay):${Thread.currentThread().name}")
delay(12000) // 协程挂起,释放主线程
println("协程1结束(delay):${Thread.currentThread().name}")
}
launch(Dispatchers.Main) { // 另一个主线程协程
println("协程2开始:${Thread.currentThread().name}") // 立刻执行!不用等12秒
println("协程2结束:${Thread.currentThread().name}")
}
println("主线程代码执行完:${Thread.currentThread().name}")
}
主线程开始:main
协程1开始(delay):main
协程2开始:main // 立刻执行!没有等待
协程2结束:main
主线程代码执行完:main
// 等待12秒后...
协程1结束(delay):main
八、runBlocking 与 lifecycleScope/viewModelScope的区别
runBlocking 是阻塞式协程启动器,仅用于 main 函数 / 单元测试,绝对不能在 Android 主线程业务代码中使用(会 ANR);
lifecycleScope/viewModelScope 是非阻塞式、生命周期绑定的 Android 专用协程作用域,是业务开发的首选(非阻塞、自动取消、适配页面 / VM 生命周期);
后续更多内容请关注微信公众号:

更多推荐

所有评论(0)