若星光 發表於 2025-8-29 10:00:19

kotlin中关于协程的使用详解

<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li>一、什么是协程?</li><li>二、为什么在Android中使用协程?</li><li>三、协程的使用</li><ul class="second_class_ul"><li>1、常用Api</li><li>2、使用示例</li><ul class="third_class_ul"><li>(1)、launch(无返回)</li><li>(2)、async(有返回,并行)</li><li>(3)、withcontext(一次性切线程并拿结果)</li><li>(4)、runBlocking(阻塞主线程,仅测试用)</li><li>(5)、coroutineScope(子作用域,任一失败全部取消)</li><li>(6)、supervisorScope(子作用域,失败互不影响)</li></ul></ul><li>四、挂起函数</li><ul class="second_class_ul"><li>1、什么是挂起函数</li><ul class="third_class_ul"></ul><li>2、挂起函数是如何工作的?</li><ul class="third_class_ul"></ul><li>3、挂起函数的使用场景</li><ul class="third_class_ul"></ul></ul></ul></div><p class="maodian"></p><h2>一、什么是协程?</h2>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;协程是Kotlin提供的一种轻量级的线程管理框架,它允许我们以同步的方式编写异步代码,让代码更加简洁易读。与线程相比,协程的创建和切换开销更小,一个应用程序可以轻松创建数千个协程而不会导致性能问题。</p>
<p class="maodian"></p><h2>二、为什么在Android中使用协程?</h2>
<ol><li>​<strong>​避免回调地狱​</strong>​:以顺序的方式编写异步代码</li><li>​<strong>​主线程安全​</strong>​:轻松切换线程,确保UI操作在主线程执行</li><li>​<strong>​简化错误处理​</strong>​:使用try-catch处理异步操作中的异常</li><li>​<strong>​生命周期感知​</strong>​:与Android组件生命周期自动绑定</li></ol>
<p class="maodian"></p><h2>三、协程的使用</h2>
<p class="maodian"></p><h3>1、常用Api</h3>
<p>先列举下关于协程的常用的六种的Api以及对用的各种功能,如下表:</p>
<table border="1" cellpadding="1" cellspacing="1"><caption>协程的 6 种常用 API</caption><tbody><tr><td>函数</td><td>作用</td><td>启动协程</td></tr><tr><td><span>launch { } 串行</span></td><td>启动<strong>无返回</strong>的协程、异常会​<strong>​立即抛出​</strong>​给父级,导致整个作用域取消。</td><td>✅</td></tr><tr><td><span>async { }&nbsp; 并行</span></td><td><p>启动<strong>有返回</strong>的协程(并行)异常只在调用&nbsp;<code>.await()</code>时​<strong>​抛出​</strong>​。</p></td><td>✅</td></tr><tr><td>withContext(Dispatchers.x) { }</td><td><strong>一次性</strong>切换线程并返回结果</td><td>❌</td></tr><tr><td>runBlocking { }</td><td><strong>阻塞当前线程</strong>直到协程完成(不常用)</td><td>❌</td></tr><tr><td>coroutineScope { }</td><td><span><strong>子作用域</strong></span>,失败时全部取消</td><td>❌</td></tr><tr><td>supervisorScope { }</td><td><span><strong>子作用域</strong></span>,失败时<strong>不影响兄弟协程</strong></td><td>❌</td></tr></tbody></table>
<p><strong>这6种api各自有各自的功能:</strong></p>
<ol><li>​​<span>launch、async</span>用于启动新协程的构建器​​&nbsp;(真正意义上的&ldquo;开启协程&rdquo;)</li><li>​​<span>withContext、coroutineScope、supervisorScope</span>用于控制线程和作用的域构建器​​&nbsp;(在已有协程内划分作用域)</li><li><span>runBlocking</span>​​一个特殊的阻塞式构建器​​&nbsp;(主要用于测试,非Android日常开发)</li></ol>
<p class="maodian"></p><h3>2、使用示例</h3>
<p class="maodian"></p><h4>(1)、launch(无返回)</h4>
<div class="jb51code"><pre class="brush:java;">// 在 Activity / ViewModel 中
lifecycleScope.launch {
    Log.d("TAG", "launch 开始")
    delay(1000)
    Log.d("TAG", "launch 结束")   // 1 秒后打印
}</pre></div>
<p class="maodian"></p><h4>(2)、async(有返回,并行)</h4>
<div class="jb51code"><pre class="brush:java;">suspend fun main() = coroutineScope {
    val a = async { delay(800); 1 }
    val b = async { delay(600); 2 }
    val sum = a.await() + b.await()   // 两个 delay 并行跑
    println(sum)                      // 输出 3
}</pre></div>
<div class="jb51code"><pre class="brush:java;">suspend fun main() = coroutineScope {
    val a = async { delay(800); 1 }
    val b = async { delay(600); 2 }
    // 一行等全部,返回 List&lt;Int&gt;
    val list = awaitAll(a, b)   // 并行等待,顺序与入参一致
    println(list.sum())         // 输出 3
}</pre></div>
<p>async开启协程的方式写了两个示例,因为这里&nbsp;<strong>awaitAll()&nbsp;</strong>和&nbsp;<strong>await()&nbsp;</strong>的使用上有点区别</p>
<table><thead><tr><th>方式</th><th>代码</th><th>异常传播</th><th>适用场景</th></tr></thead><tbody><tr><td>逐个&nbsp;<code>await()</code></td><td><code>a.await()+b.await()</code></td><td>第一个异常会阻断第二个</td><td>数量少</td></tr><tr><td><code>awaitAll()</code></td><td><code>awaitAll(a, b)</code></td><td><strong>合并异常</strong>,一次性抛出</td><td>数量多,更整洁</td></tr></tbody></table>
<p>3.1在开启协程时可以指定线程的作用域</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;launch和&nbsp;async函数本身可以接受一个&nbsp;CoroutineContext类型的参数(通过参数指定上下文),你可以通过这个参数来​​指定协程的调度器、异常处理器等​​。从广义上讲,这也是在&ldquo;设置&rdquo;协程运行的上下文环境。</p>
<div class="jb51code"><pre class="brush:java;">// 在 ViewModel 的 viewModelScope 这个“父作用域”中启动新协程
viewModelScope.launch { // 这个 launch 是 viewModelScope 的子协程
    // 代码
}
viewModelScope.async { // 这个 async 也是 viewModelScope 的子协程
    // 代码
}
//指定线程
// 设置调度器:在IO线程池运行
viewModelScope.launch(Dispatchers.IO) {
    // 网络请求等IO操作
}
// 设置异常处理器
val exceptionHandler = CoroutineExceptionHandler { _, exception -&gt;
    println("Caught $exception")
}
viewModelScope.launch(exceptionHandler) {
    // 可能会抛出异常的代码
}
// 可以组合多个上下文元素
viewModelScope.launch(Dispatchers.Default + exceptionHandler) {
    // ...
}</pre></div>
<p class="maodian"></p><h4>(3)、withcontext(一次性切线程并拿结果)</h4>
<p>withContext 可以切到 任何 Dispatcher,常用只有这3个:</p>
<table border="1" cellpadding="1" cellspacing="1"><tbody><tr><td>Dispatcher类型</td><td>线程</td><td>使用场景</td></tr><tr><td>Dispatchers.Main</td><td>主线程(UI)</td><td>更新界面</td></tr><tr><td>Dispatchers.IO</td><td>子线程池</td><td>网络 / 文件 / 数据库</td></tr><tr><td>Dispatchers.Default</td><td>子线程池</td><td>CPU 密集计算</td></tr></tbody></table>
<p>Dispatchers.IO和&nbsp;Dispatchers.Default管理的都是子线程(后台线程),但它们是为​​完全不同类型的工作任务​而设计的,因此它们的底层线程池策略有显著区别。</p>
<div class="jb51code"><pre class="brush:java;">suspend fun main() {
    val threadName = withContext(Dispatchers.IO) {
      Thread.currentThread().name   // 在 IO 线程池里执行
    }
    println(threadName)               // 例如:DefaultDispatcher-worker-1
}</pre></div>
<p class="maodian"></p><h4>(4)、runBlocking(阻塞主线程,仅测试用)</h4>
<div class="jb51code"><pre class="brush:java;">fun main() = runBlocking {
    println("start")
    delay(1000)
    println("end")   // 整个 main 会等 1 秒
}</pre></div>
<p class="maodian"></p><h4>(5)、coroutineScope(子作用域,任一失败全部取消)</h4>
<div class="jb51code"><pre class="brush:java;">suspend fun main() = coroutineScope {
    launch {
      delay(200)
      throw RuntimeException("boom")   // 异常
    }
    launch {
      delay(1000)
      println("never reach")         // 被取消
    }
}</pre></div>
<p class="maodian"></p><h4>(6)、supervisorScope(子作用域,失败互不影响)</h4>
<div class="jb51code"><pre class="brush:java;">suspend fun main() = supervisorScope {
    launch {
      delay(200)
      throw RuntimeException("boom")   // 兄弟不受影响
    }
    launch {
      delay(500)
      println("still alive")         // 会打印
    }
}</pre></div>
<ul><li>launch / async / withContext:业务代码用的最多</li><li>runBlocking:单元测试时使用</li><li>coroutineScope / supervisorScope:并发任务时控制异常</li></ul>
<p class="maodian"></p><h2>四、挂起函数</h2>
<p class="maodian"></p><h3>1、什么是挂起函数</h3>
<p>说到协程,就不得不说提到挂起函数,什么是挂起函数?</p>
<ul><li><strong>标记</strong>:<strong>suspend</strong>&nbsp;关键字。</li><li><strong>能力</strong>:只能在协程或另一个挂起函数里调用。</li><li><strong>本质</strong>:是一种可以被协程挂起(暂停执行),而不会阻塞其所在线程的函数。编译器把函数切成「状态机」,遇到 delay()、withContext()&nbsp;等挂起点就<strong>挂起</strong>,线程空出来干别的,等结果回来再<strong>恢复</strong>继续执行。</li></ul>
<p class="maodian"></p><h3>2、挂起函数是如何工作的?</h3>
<p>挂起函数的背后是 ​<strong>​状态机​</strong>​ 和 ​<strong>​Continuation​</strong>​ 概念。</p>
<p>​<strong>​编译器魔法​</strong>​:当你编译一个&nbsp;<code>suspend</code>函数时,编译器会做额外的转换。它会为协程体生成一个状态机(State Machine)。每个挂起点(即调用另一个&nbsp;<code>suspend</code>函数的地方)都成为状态机的一个可能状态。</p>
<p>​<strong>​Continuation​</strong>​:可以把它理解为一个​<strong>​回调对象​</strong>​,它封装了 &ldquo;协程在挂起之后该如何恢复执行&rdquo; 的信息,包括它应该从哪一行代码继续、当时的局部变量是什么等等。</p>
<p>当你调用&nbsp;withContext(Dispatchers.IO)&nbsp;{&nbsp;...&nbsp;}时,实际上发生了:</p>
<ol><li>协程在执行到&nbsp;withContext时,会​​挂起​​。</li><li>它将&nbsp;withContext块内的代码和&nbsp;Continuation(恢复信息)一起提交给协程调度器。</li><li>调度器安排一个线程(IO线程池中的线程)来执行这个块。</li><li>执行完毕后,调度器再通知协程:&ldquo;你交代的任务完成了&rdquo;,并把结果和&nbsp;Continuation一起,安排回原来的线程(或者你指定的调度器,如&nbsp;Dispatchers.Main)。</li><li>协程根据&nbsp;Continuation的信息,​​恢复​​到挂起点之后的状态,继续执行。</li></ol>
<p>上面这种描述太抽象,我们不如想象成一个快递员(​<strong>​一个线程​</strong>​)在送包裹(​<strong>​执行任务​</strong>​)。</p>
<p>​<strong>​1、普通函数(阻塞)​</strong>​:快递员到了一个办公室楼下,需要等收件人下来签字。在收件人下来之前,他什么都不做,就干等着(​<strong>​阻塞​</strong>​)。这期间他没法去送别的包裹,效率很低。</p>
<p><strong>2、​回调函数(非阻塞但复杂)​</strong>​:快递员把包裹交给前台,并留下一个纸条(​<strong>​回调函数​</strong>​)说:&ldquo;等收件人来了,打电话叫我回来签字&rdquo;。然后他就去送别的包裹了。等前台打电话来,他再回来处理。这样效率高了,但流程变得复杂,如果包裹多,需要留很多纸条,管理起来很混乱(​<strong>​回调地狱​</strong>​)。</p>
<p>​<strong>​3、挂起函数(挂起-恢复)​</strong>​:快递员到了办公室楼下,他给收件人打了个电话说:&ldquo;我到了,你下来吧&rdquo;。​<strong>​在收件人下楼的这段时间里,他并没有干等着,而是被派去送隔壁楼的另一个小包裹(挂起当前任务,线程去干别的事了)​</strong>​。等收件人快到楼下了,快递员也送完隔壁的小包裹回来了,然后顺利签字完成主要任务。</p>
<p>在这个比喻中:</p>
<ol><li>​<strong>​快递员​</strong>​:就是一个线程。</li><li>​<strong>​送主要包裹​</strong>​:就是执行协程体里的代码。</li><li>​<strong>​打电话让收件人下楼​</strong>​:就是调用一个&nbsp;<code>suspend</code>函数(比如&nbsp;delay,&nbsp;withContext,&nbsp;或者你的自定义挂起函数)。</li><li>​<strong>​去送隔壁的小包裹​</strong>​:线程被释放,可以去执行其他任务(可能是其他协程的任务)。</li><li>​<strong>​收件人下楼完成,快递员回来签字​</strong>​:挂起的条件满足,协程在(可能是原来的,也可能是另一个)线程上​<strong>​恢复​</strong>​,继续执行后面的代码。</li></ol>
<p>​<strong>​<span>关键点:​</span></strong><span>​&nbsp;</span></p>
<p>&nbsp; &nbsp; &nbsp; &nbsp; 1、挂起函数不会阻塞线程,而是释放线程去干别的活,等它等待的操作(如网络请求、磁盘IO、延迟)完成后,协程会在合适的时机和线程上​<strong>​恢复​</strong>​执行。</p>
<p>&nbsp; &nbsp; &nbsp; &nbsp; 2、挂起函数本身不指定线程​​:suspend关键字只是一个标记,告诉编译器这个函数可以在协程中使用并可能挂起。它本身并不包含任何线程信息。线程由调度器(Dispatcher)决定​​:真正决定代码在哪个线程上运行的是协程的上下文中的&nbsp;<strong>CoroutineDispatcher(协程调度器)</strong>➡️(Dispatchers.Main / IO / Default)。</p>
<p>&nbsp; &nbsp; &nbsp; &nbsp; 3、挂起函数的作用域不一定在子线程中。它的执行线程完全取决于它在被调用时所在的协程上下文(CoroutineContext),以及它内部使用的调度器(Dispatcher)。​挂起函数的核心是&ldquo;挂起&rdquo;(suspend),而不是&ldquo;切换线程&rdquo;。线程切换只是实现挂起的一种常用手段。</p>
<p class="maodian"></p><h3>3、挂起函数的使用场景</h3>
<p>(1)情况一:在主线程启动,并在主线程调用挂起函数</p>
<div class="jb51code"><pre class="brush:java;">viewModelScope.launch(Dispatchers.Main) { // 1. 在主线程启动协程
    // 2. 当前上下文是 Dispatchers.Main
    doSomeWork() // 3. 调用挂起函数
}
// 这个挂起函数没有使用 withContext 切换线程
// 因此它将继承调用者的上下文,即在主线程运行
suspend fun doSomeWork() {
    // 这里的代码会在 Dispatchers.Main 上执行
    // 如果在这里执行耗时操作,会阻塞主线程!
    heavyOperation() // ❌ 危险!会阻塞UI!
}
fun heavyOperation() {
    Thread.sleep(2000) // 模拟耗时阻塞操作
}</pre></div>
<p>这是最常见的Android场景。协程在主线程启动,挂起函数内部没有切换上下文,那么它就会在主线程运行。在这个例子中,挂起函数&nbsp;doSomeWork()的作用域是主线程。</p>
<p>(2)情况二:正确的&ldquo;主线程安全&rdquo;挂起函数</p>
<div class="jb51code"><pre class="brush:java;">viewModelScope.launch(Dispatchers.Main) { // 1. 在主线程启动协程
    // 2. 当前上下文是 Dispatchers.Main
    val result = doSomeSafeWork() // 3. 调用挂起函数(挂起点)
    updateUI(result) // 6. 恢复后,仍在主线程,安全更新UI
}
// 这是一个主线程安全的挂起函数
suspend fun doSomeSafeWork(): String {
    // 4. 函数开始执行时,仍在主线程
    // 但 withContext 会将协程的执行挂起,并将代码块交给 IO 调度器
    return withContext(Dispatchers.IO) {
      // 5. 这个代码块现在在IO线程池中的某个线程执行
      // 模拟网络请求或数据库操作,不会阻塞主线程
      Thread.sleep(2000)
      "Result from network"
    }
    // withContext 完成后,协程会自动切回原来的上下文(Dispatchers.Main)
    // 所以返回值是在主线程被接收的
}</pre></div>
<p>&nbsp;一个良好的挂起函数应该内部处理线程切换,保证无论从哪个线程调用它,其耗时操作都在后台进行,并最终将结果返回给调用方线程。</p>
<p>doSomeSafeWork() 函数​​内部​​的&nbsp;withContext 代码块的作用域是子线程(IO线程)。但从外部看,这个函数​​被调用和返回​​的上下文(viewModelScope通常是&nbsp;Dispatchers.Main)是主线程。</p>
<p>(3)情况三:在子线程启动协程</p>
<div class="jb51code"><pre class="brush:java;">viewModelScope.launch(Dispatchers.Default) { // 1. 在Default线程池启动
    // 2. 当前上下文是 Dispatchers.Default
    doSomeWork() // 3. 这个挂起函数将在 Default 线程执行
}
suspend fun doSomeWork() {
    // 在 Dispatchers.Default 上执行
}</pre></div>
<p>如果明确指定一个后台调度器启动协程,那么挂起函数(如果不内部切换)就会在那个后台线程运行。</p>
<p>(4)总结</p>
<table><thead><tr><th><p>场景</p></th><th><p>挂起函数所在线程</p></th><th><p>说明</p></th></tr></thead><tbody><tr><td><p>​<strong>​默认情况​</strong>​</p></td><td><p>​<strong>​继承调用方协程的上下文​</strong>​</p></td><td><p>挂起函数不自动切换线程,它在哪个线程被调用,就在哪个线程运行。</p></td></tr><tr><td><p>​<strong>​</strong>使用<strong>&nbsp;</strong><code>withContext</code><strong>​</strong>​</p></td><td><p>​<strong>​</strong>由<strong>&nbsp;</strong><code>withContext</code>的参数决定​​</p></td><td><p>这是​<strong>​主动控制​</strong>​挂起函数内部代码执行线程的标准方式。</p></td></tr><tr><td><p>​​设计目标​​</p></td><td><p>​​实现主线程安全​​</p></td><td><p>一个好的挂起函数应该内部使用&nbsp;<code>withContext</code>确保任何耗时操作都不在主线程进行,让调用者无需关心线程细节。</p></td></tr></tbody></table>
<p>​<strong>​</strong>挂起函数的作用域不一定在子线程中。它的线程环境是​​动态的​​和​​可预测的​​。</p>
<p>​​动态的​​:取决于调用它的协程上下文和它内部使用的调度器。</p>
<p>可预测的​​:开发者可以通过&nbsp;withContext精确地控制其内部代码应该在哪个线程上执行。</p>
<p>​<strong><span>​注意:</span></strong>​​&nbsp;</p>
<p><strong><span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span></strong> 1、<strong><span>永远不要假设一个挂起函数会在后台线程运行。</span></strong>如果要执行耗时操作,​​必须​​在挂起函数内部使用&nbsp;withContext(Dispatchers.IO)或&nbsp;withContext(Dispatchers.Default)来明确切换到合适的线程。这才是编写&ldquo;主线程安全&rdquo;挂起函数的关键。</p>
<p>&nbsp; &nbsp; &nbsp; &nbsp; 2、<strong><span>结构化并发(Structured Concurrency)</span></strong>,这是协程设计的核心哲学,要求协程的生命周期与它的启动作用域(如&nbsp;ViewModel的&nbsp;viewModelScope或&nbsp;Activity的&nbsp;lifecycleScope)绑定。&nbsp; &nbsp; &nbsp;&nbsp; &nbsp;&nbsp;</p>
<p>到此这篇关于kotlin中关于协程的使用的文章就介绍到这了,更多相关kotlin协程使用内容请搜索琼殿技术社区以前的文章或继续浏览下面的相关文章希望大家以后多多支持琼殿技术社区!</p>
                           
                            <div class="art_xg">
                              <b>您可能感兴趣的文章:</b><ul><li>Kotlin协程之Flow的使用与原理解析</li><li>Kotlin使用协程实现高效并发程序流程详解</li><li>Kotlin协程Context应用使用示例详解</li><li>Kotlin协程Channel特点及使用细节详解</li><li>Kotlin 协程思维模型的引入使用建立</li><li>Kotlin协程的基础与使用示例详解</li><li>Kotlin协程概念原理与使用万字梳理</li></ul>
                            </div>

                        </div>
                        <!--endmain-->
頁: [1]
查看完整版本: kotlin中关于协程的使用详解