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

三、协程的核心基础概念

  1. 协程作用域(CoroutineScope)
    协程必须在作用域中启动,作用域管理协程生命周期,避免内存泄漏。
常用作用域 适用场景 生命周期特点 自动取消 / 销毁特性
lifecycleScope Activity/Fragment 绑定页面生命周期,自动销毁 自动取消:Lifecycle 走到 DESTROYED 时,作用域内所有协程自动取消
viewModelScope ViewModel 绑定VM生命周期 自动取消:ViewModel.onCleared () 时自动取消所有协程
MainScope 自定义场景(如非页面类、手动管理) 需手动在onDestroy取消 不会自动取消:必须手动调用 cancel() (如在 onDestroy 中)
GlobalScope 全局任务 绑定整个应用进程生命周期 ,进程级,慎用(易内存泄漏) 不会自动取消:进程不死,协程不销毁
  1. 调度器(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)
		)
	)	
}
  1. 协程构建器:启动协程的两种方式
构建器 返回值 适用场景
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 生命周期);

后续更多内容请关注微信公众号:

在这里插入图片描述

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐