【Android Kotlin】协程知识体系

Android Kotlin知识汇总(三)Kotlin 协程 Kotlin 协程让异步代码像阻塞代码一样易于使用。协程可大幅简化后台任务管理,例如网络调用、本地数据访问等任务的管理。本主题介绍如何使用 Kotlin 协程解决以下问题,从而让您能够编写出更清晰、更简洁的应用代码。 阅读详情

文章目录

一、协程核心基础

1.1 什么是协程?与线程的核心区别

定义:协程是用户态轻量级线程,基于线程封装,由Kotlin语言层、库层调度,而非操作系统内核调度,是Kotlin官方标准化的异步、并发解决方案,主打低开销、高并发、代码同步式书写。
核心区别(面试高频·精细化对比)

  • 资源开销:线程由OS内核创建、调度、销毁,开销大,系统最大并发数仅数千;协程为用户态逻辑任务,无内核态切换开销,单机支持十万级并发,资源占用可忽略
  • 调度机制:线程是内核抢占式调度,系统强制切换线程执行权;协程是用户态协作式调度,主动挂起、主动恢复,无强制抢占
  • 阻塞特性:线程调用sleep、IO阻塞时,会永久占用当前内核线程,线程空闲率极低;协程suspend挂起时完全释放线程,线程可复用执行其他任务,线程利用率拉满
  • 依赖关系:协程无法脱离线程独立运行,始终依托线程池执行,是对线程的逻辑层封装
  • 编程体验:线程需手动处理回调、锁、线程切换,易产生回调地狱;协程同步顺序书写异步代码,可读性、可维护性极高

1.2 挂起与恢复

挂起函数 suspend:被 suspend 修饰的函数,仅能在协程作用域或其他挂起函数中调用,普通线程、普通函数无法直接调用。
核心特性:

  • 挂起不阻塞线程:网络请求、文件IO、delay延时等等待场景,仅挂起当前协程任务,不占用绑定线程
  • 精准可恢复性:等待条件结束后,协程会在原调度器指定线程恢复执行,保留挂起前的局部变量、执行位置
  • 异常统一管控:挂起函数不主动抛出受检异常,所有异常统一交由协程上下文异常体系处理
    底层编译原理:Kotlin编译器将所有挂起函数编译为有限状态机(FSM),自动拆分代码段、记录执行状态,实现无栈式断点续执行,这是协程挂起恢复的核心底层支撑。

挂起的本质:suspend 经编译器改写为“状态机 + Continuation”。遇到挂起点时保存现场并返回COROUTINE_SUSPENDED。
恢复的本质:挂起条件满足后(delay 到期/IO 回调返回),并不是“立刻抢回原线程A”,而是 resume 后交给Dispatcher.dispatch(…),把 continuation 包装成任务投递/入队。

二、协程四大核心组件

四大核心:CoroutineScope 作用域、Dispatcher 调度器、Job 任务、CoroutineContext 上下文,四者组合构成完整协程运行环境

2.1 CoroutineContext 协程上下文

核心定义:协程的全局配置容器,本质是一个只读键值对集合,支持元素叠加、覆盖、合并,所有协程的运行配置、生命周期、异常策略都存储在此。
核心四要素(全覆盖):

  • Dispatcher:线程调度器,决定协程执行的线程环境
  • Job:任务控制器,管控协程生命周期、取消、状态监听
  • CoroutineName:协程自定义名称,用于日志排查、线上问题定位
  • CoroutineExceptionHandler:协程全局异常兜底处理器

上下文合并规则:新上下文元素会覆盖旧上下文同Key元素,不同Key元素相互叠加,这是withContext、launch传参修改配置的底层原理。

2.2 Dispatcher 调度器

决定协程执行线程,是协程线程切换的核心,四大官方调度器工程级精细区别:

  • Dispatchers.Main:Android专属主线程调度器,依托Android主线程Handler实现,仅用于UI刷新、页面交互、控件操作,禁止执行耗时任务
  • Dispatchers.Default:默认调度器,核心适配CPU密集型任务,核心线程数 = CPU核心数,线程池固定,适合JSON解析、数据排序、算法计算、图片裁剪
  • Dispatchers.IO:IO专属调度器,适配阻塞式IO任务,底层与Default共享线程池,支持动态扩容线程,适合网络请求、文件读写、数据库CRUD、SP存储
  • Dispatchers.Unconfined:无约束调度器,跟随当前调用线程执行,挂起后恢复线程不确定,项目绝对禁止使用,仅用于测试场景

Default 和 IO 核心区别:两者底层共用同一个核心线程池,Default线程数固定,防止CPU打满卡顿;IO针对阻塞任务做了线程扩容优化,IO阻塞时自动新建线程承接新任务,不会阻塞核心计算线程。

2.3 Job 协程任务(生命周期管控·精细状态+实战坑点)

每一个协程launch/async都会生成唯一Job实例,是协程生命周期的唯一管控对象。
四大精细状态:New(新建未启动)、Active(活跃执行)、Completed(正常完成)、Cancelled(异常/手动取消)
核心特性:

  • 默认Job具备父子强绑定机制:父协程取消,所有子协程级联取消;子协程异常崩溃,父协程及所有兄弟协程全部取消(雪崩效应)
  • 支持手动管控:job.cancel()取消任务、job.join()阻塞等待任务完成、状态监听回调

SupervisorJob 精细化区别:
SupervisorJob:完全隔离子任务异常,单个子协程崩溃,不影响父协程、不影响其他子协程,完美适配多独立接口并行、批量上报场景。
实战用法:页面多接口并行请求时,自定义Scope必须搭配SupervisorJob,避免单个接口失败导致所有请求终止。

2.4 CoroutineScope 协程作用域(工程规范+实战场景)

协程的运行容器,所有协程必须依托作用域启动,核心价值:绑定业务生命周期,统一批量取消任务,彻底杜绝协程内存泄漏。
Android 四大常用作用域·精细适用场景:

  • GlobalScope:全局进程级作用域,生命周期跟随APP进程,业务代码禁止使用。仅用于全局日志、进程级常驻任务,页面业务使用必泄漏
  • viewModelScope:ViewModel专属作用域,默认绑定SupervisorJob+Main调度器,ViewModel销毁自动cancel,无内存泄漏,是页面业务首选作用域
  • lifecycleScope:Activity/Fragment专属,绑定页面生命周期,页面onDestroy自动取消所有协程,适合页面临时异步任务
  • 自定义Scope:独立业务组件、工具类专用,手动绑定组件生命周期,销毁主动cancel

三、协程启动方式与场景区别

3.1 launch 与 async/await 核心区别+落地场景 **

  • launch

无返回值异步任务启动器,返回Job对象,仅用于执行无需结果的异步任务,侧重「执行过程」。
实战场景:埋点上报、日志打印、延时弹窗、无需回调的后台任务

  • async/await

有返回值异步任务启动器,返回Deferred泛型对象,通过await()阻塞获取任务结果,侧重「执行结果」,支持并行计算。
实战场景:多接口并行请求、多文件并行读取、多任务并发计算

  1. launch:开启协程,返回 Job,无业务返回值,适合不需要拿到结果的任务;内部异常直接抛出。

  2. async:开启协程,返回 Deferred,用于产生返回值;任务立刻后台执行,本身不会等待。

  3. await:挂起函数,不启动协程,阻塞等待 Deferred 拿到结果;异常在 await 调用点抛await:暂停当前协程,等待目标 async 任务做完,拿到返回值,再继续往下跑。
    异常关键点:

  1. launch:内部异常直接抛出,父协程直接崩溃取消。
  2. async:异常会被存到 Deferred,只有调用 await () 才抛出异常,不调用 await,异常不会触发

3.2 串行执行 vs 并行执行

  • 串行执行(耗时叠加)
    顺序调用await,上一个任务完成再执行下一个,总耗时 = 所有任务耗时总和,适合有依赖的任务(先查缓存、再请求网络)。
//串行,顺序await
val a = apiA()
val b = apiB()
  • 并行执行(耗时取最大)
    先批量启动async任务,再统一await,任务并发执行,总耗时 = 耗时最长的任务,适合无依赖的独立任务,大幅优化页面加载速度。
//并行:先全部启动,再等待结果
val deferredA = async { apiA() }
val deferredB = async { apiB() }
val resultA = deferredA.await()
val resultB = deferredB.await()

工程硬性规范:首页多个无依赖接口,必须使用并行async,禁止串行,减少页面白屏、加载耗时。

3.2.1 实现串行的几种方式

  • 方式 1:直接顺序调用(最基础,自动串行)
import kotlinx.coroutines.*

fun main() = runBlocking {
    // 协程内部顺序执行 → 串行
    launch {
        task1()
        task2()
        task3()
    }.join()
}

suspend fun task1() {
    delay(1000)
    println("任务1完成")
}
suspend fun task2() {
    delay(1000)
    println("任务2完成")
}
suspend fun task3() {
    delay(1000)
    println("任务3完成")
}

  • 方式 2:async 顺序 await
runBlocking {
    val r1 = async { task1() }.await()
    val r2 = async { task2() }.await()
    val r3 = async { task3() }.await()
}

  • 方式 3:循环串行执行一批任务
runBlocking {
    val tasks = listOf(::task1, ::task2, ::task3)
    for (t in tasks) t()
}

✅ 串行特点:后一个依赖前一个结果时,适合串行。

3.2.2 实现并行

  • 方式 1:async + awaitAll(推荐)
fun main() = runBlocking {
    val deferreds = listOf(
        async { task1() },
        async { task2() },
        async { task3() }
    )
    // 等待全部并行任务完成
    deferreds.awaitAll()
    println("全部并行任务结束")
}

  • 方式 2:launch + joinAll(不需要返回值)
runBlocking {
    val jobs = listOf(
        launch { task1() },
        launch { task2() },
        launch { task3() }
    )
    jobs.joinAll()
}

  • 方式 3:批量集合并行(动态任务列表)
runBlocking {
    val tasks = listOf(::task1, ::task2, ::task3)
    val deferreds = tasks.map { async { it() } }
    deferreds.awaitAll()
}

四、协程异常处理体系

4.1 两种异常传播机制(核心本质)

  • launch 异常:异常主动向上传播,触发协程崩溃,父、子所有关联协程全部取消,直接抛出崩溃信息
  • async 异常:异常延迟封装,存储在Deferred对象中,不主动传播,仅在调用await()时才会抛出,不调用则无任何崩溃提示(隐藏坑点)

4.2 三种异常捕获方案(优先级+适用场景)

  • 局部try-catch(首选):精准包裹挂起函数/await方法,单独捕获单个任务异常,不影响其他任务,精度最高、工程最常用
  • CoroutineExceptionHandler(全局兜底):作用域级统一异常捕获,仅能捕获launch主动传播的异常,无法捕获async await异常,用于全局崩溃兜底上报
  • SupervisorJob(异常隔离):阻断异常雪崩,实现单任务失败、整体任务正常执行,适配批量并行任务

4.3 高频面试坑点精解

问题:为什么全局异常处理器抓不到 async 异常?
标准答案:async属于延迟异常机制,异常不会主动向上传播,仅在await()调用时被动抛出,全局异常处理器仅监听主动传播的协程异常,因此无法捕获,必须手动try-catch await方法。

4.4 异常处理标准实战模板

// 并行任务+异常隔离+精准捕获,工程最优范式
viewModelScope.launch(SupervisorJob()) {
    val task1 = async(Dispatchers.IO) { api1() }
    val task2 = async(Dispatchers.IO) { api2() }
    
    // 单个任务异常不影响其他任务
    try {
        task1.await()
    } catch (e: Exception) {
        // 单独处理接口1异常
    }
    try {
        task2.await()
    } catch (e: Exception) {
        // 单独处理接口2异常
    }
}

五、协程核心进阶原理

5.1 挂起函数无线程阻塞核心原理

传统线程Thread.sleep/IO阻塞:线程进入阻塞状态,永久占用当前内核线程,线程无法处理其他任务,资源利用率极低。
协程suspend挂起:触发挂起后,立即释放当前绑定的线程,线程回归线程池复用,协程任务进入等待队列;当IO/延时任务完成,调度器重新分配线程恢复任务执行,这是协程高并发、高效率的根本原因。

5.2 协程线程切换原理(withContext精细解析)

withContext(Dispatchers.XXX) 是协程唯一正规线程切换方式,原理:触发协程挂起 → 脱离原有线程 → 在目标调度器线程恢复执行后续代码,基于状态机保存执行上下文,无需手动Handler回调、线程切换。
实战优势:子线程IO请求、主线程刷新UI,代码自上而下顺序书写,彻底解决回调地狱,代码可读性、可维护性大幅提升。

// 标准线程切换模板
viewModelScope.launch {
    // 1. 子线程执行IO网络请求
    val data = withContext(Dispatchers.IO) {
        apiService.getHomeData()
    }
    // 2. 自动切回主线程更新UI
    updateUI(data)
}

5.3 父子协程层级与取消机制

默认Job强绑定:父协程取消,所有子协程立即终止;子协程异常,父协程直接取消,产生任务雪崩。
SupervisorJob解绑层级:仅阻断子异常向上传播,父协程取消依然会终止所有子协程(核心坑点),仅实现子异常不影响全局,并非完全解绑生命周期。

六、Android 项目实战场景

6.1 网络请求工业级标准写法

核心规范:viewModelScope承载 + IO调度请求 + Main刷新UI + try-catch兜底 + 自动取消防泄漏

fun loadHomeData() {
    viewModelScope.launch(Dispatchers.Main) {
        try {
            // 子线程IO请求
            val result = withContext(Dispatchers.IO) {
                api.loadHomeInfo()
            }
            // 主线程更新UI
            refreshView(result)
        } catch (e: Exception) {
            // 统一异常处理:报错、兜底UI
            showErrorToast()
        }
    }
}

6.2 延时任务/轮询任务替代Handler

使用delay()替代Handler.postDelayed,无需手动移除任务,页面销毁自动取消,无内存泄漏。

// 3秒延时弹窗
viewModelScope.launch {
    delay(3000)
    showDialog()
}

// 轮询请求(自动终止)
viewModelScope.launch {
    while (isActive) { // 协程活跃状态判断,销毁自动终止循环
        pollNetData()
        delay(2000)
    }
}

6.3 多接口并行容错请求

SupervisorJob异常隔离 + async并行 + 单接口容错,单个接口失败不影响页面整体渲染

fun loadMultiData() {
    viewModelScope.launch(SupervisorJob()) {
        val listDeferred = async(Dispatchers.IO) { api.getList() }
        val userDeferred = async(Dispatchers.IO) { api.getUser() }

        // 容错处理
        val list = try { listDeferred.await() } catch (e: Exception) { emptyList() }
        val user = try { userDeferred.await() } catch (e: Exception) { null }
        
        // 刷新UI
        refreshData(list, user)
    }
}

6.4 协程防抖实战(解决快速点击重复请求)

通过Job状态判断任务是否活跃,杜绝短时间重复点击触发多次请求

private var requestJob: Job? = null

fun submitData() {
    // 任务活跃则直接返回,防抖拦截
    if (requestJob?.isActive == true) return
    
    requestJob = viewModelScope.launch(Dispatchers.IO) {
        // 提交网络请求
        api.submit()
    }
}

七、协程性能优化与避坑

7.1 内存泄漏终极规避规范

  • 禁止GlobalScope处理页面业务,仅用于进程级常驻任务
  • 页面异步任务统一使用lifecycleScope、viewModelScope,依托生命周期自动取消
  • 自定义业务Scope必须在组件销毁、页面退出时手动cancel
  • 协程内部禁止直接持有Activity/View强引用,避免页面销毁后任务残留持有引用

7.2 高频性能坑点精细化解析

  • 主线程协程做耗时计算:Dispatchers.Main协程内执行JSON解析、排序等计算任务,阻塞主线程,引发UI卡顿、ANR,必须切Default/IO调度器
  • 无依赖接口串行请求:浪费网络耗时,必须改为async并行执行,优化加载速度
  • 未处理async延迟异常:线上隐藏崩溃隐患,偶现闪退,必须全部try-catch包裹await
  • 无限循环协程无状态判断:while死循环未判断isActive,页面销毁后依然后台死循环,造成性能损耗、泄漏

八、高频面试真题+精细化标准答案

1. 协程为什么比线程更高效?

协程是用户态轻量级逻辑任务,创建销毁无内核态切换开销,支持十万级并发;核心优势是挂起不阻塞线程,最大化复用线程池资源,线程利用率远高于传统线程。而线程为内核调度,阻塞会永久占用线程资源,并发能力弱、开销大。

2. viewModelScope 为什么不会内存泄漏?

viewModelScope绑定ViewModel生命周期,自带SupervisorJob,当页面销毁、ViewModel被系统回收时,作用域会自动调用cancel()终止所有协程任务,清空所有引用,杜绝页面View、Activity引用残留导致的内存泄漏。

3. SupervisorJob 和普通 Job 核心区别?

普通Job父子协程强绑定,子协程异常会触发全局任务雪崩,父子、兄弟协程全部取消;SupervisorJob实现子任务异常隔离,单个子协程失败仅终止自身,不影响父协程和其他子协程,适合多独立并行任务场景。

4. launch 和 async 适用场景区别?

launch用于无返回值、无需结果的异步任务,如埋点、日志、延时操作;async用于需要获取返回结果的异步任务,支持多任务并行,适用于多接口同时请求、批量数据读取等需要结果回调的场景。

5. 协程线程切换底层原理?

通过withContext切换调度器,触发协程挂起,依托编译器生成的状态机保存当前执行上下文,释放原有线程,在目标调度器线程恢复执行后续代码,实现无回调、顺序式线程切换。

6. 全局异常处理器为什么捕获不到 async 异常?***

async采用延迟异常机制,异常封装在Deferred对象中,不会主动向上传播,仅在调用await()时被动抛出;而CoroutineExceptionHandler仅捕获主动传播的协程异常,因此无法捕获,必须手动try-catch处理。

7. Dispatchers.IO 和 Default 区别?***

两者底层共享核心线程池;Default适配CPU密集型计算,线程数固定为CPU核心数,避免CPU过载;IO适配阻塞式IO任务,支持线程池动态扩容,解决IO阻塞导致的线程占用问题,吞吐量更高。

九、补充:协程核心拓展知识

9.1 isActive 状态判断核心用法 **

协程取消后,内部依然会执行完当前代码块,不会立即终止。在无限循环、长耗时任务中,必须通过 isActive 判断协程活跃状态,及时终止任务,避免无效执行、性能损耗。
实战场景:轮询任务、批量数据遍历、大文件读写

9.2 常用协程 API 详解

协程库提供了丰富的 API 来简化并发编程。以下是一些核心且高频的 API 及其工程化用法。

9.2.1 并发与等待 API

awaitAll(vararg deferreds: Deferred<T>): List<T>
  • 作用:并发执行多个 Deferred 任务,并等待所有任务完成。所有任务成功则返回结果列表;任意一个任务抛出异常,则整个 awaitAll 调用会抛出该异常(CancellationException 除外)。
  • 场景:并行请求多个无依赖的接口,并等待全部结果返回后再统一处理。
  • 代码示例:
    viewModelScope.launch {
        val deferred1 = async { api.fetchUserInfo() }
        val deferred2 = async { api.fetchProductList() }
        val deferred3 = async { api.fetchBannerData() }
        
        try {
            // 等待三个接口全部完成
            val results: List<Any> = awaitAll(deferred1, deferred2, deferred3)
            val userInfo = results[0] as UserInfo
            val productList = results[1] as List<Product>
            val bannerData = results[2] as List<Banner>
            // 更新UI
            updateUI(userInfo, productList, bannerData)
        } catch (e: Exception) {
            // 任何一个接口失败,都会进入catch
            showError("部分数据加载失败")
        }
    }
    
completeDeferred(value: T): Boolean
  • 完整签名:fun <T> CompletableDeferred<T>.complete(value: T): Boolean
  • 作用:手动为一个 CompletableDeferred 实例设置成功结果。返回 true 表示设置成功(该 Deferred 尚未完成),返回 false 表示该 Deferred 已处于完成或取消状态。
  • 核心场景:将回调(Callback)或监听器(Listener)模式转换为协程的挂起函数。通常在回调中调用此方法来“唤醒”等待中的 await()。
  • 代码示例:将传统的网络回调封装为挂起函数。
    suspend fun fetchDataSuspend(): Data {
        val deferred = CompletableDeferred<Data>()
        
        // 传统回调API
        oldApi.fetchData(object : Callback<Data> {
            override fun onSuccess(data: Data) {
                // 回调成功,手动完成Deferred
                deferred.complete(data)
            }
            override fun onFailure(error: Throwable) {
                // 回调失败,手动完成异常
                deferred.completeExceptionally(error)
            }
        })
        
        // 挂起,等待回调触发complete
        return deferred.await()
    }
    

9.2.2 超时控制 API

withTimeout(timeMillis: Long): T
withTimeoutOrNull(timeMillis: Long): T?
  • 作用:为一段挂起代码块设置执行超时。withTimeout 超时会抛出 TimeoutCancellationException;withTimeoutOrNull 超时则安静地返回 null。
  • 场景:网络请求、数据库查询、文件操作等需要限制最大耗时的场景。
  • 代码示例:
    viewModelScope.launch {
        // 方案1:超时抛出异常
        try {
            val result = withTimeout(5000) { // 5秒超时
                api.loadHeavyData()
            }
        } catch (e: TimeoutCancellationException) {
            showToast("请求超时")
        }
        
        // 方案2:超时返回null(更简洁)
        val resultOrNull = withTimeoutOrNull(3000) {
            api.loadConfig()
        }
        if (resultOrNull == null) {
            // 超时处理
            loadLocalCache()
        }
    }
    

9.2.3 结构化并发与作用域 API

coroutineScope { }
supervisorScope { }
  • 作用:创建一个新的子作用域。它们都是挂起函数,会等待其内部所有子协程完成后才完成自身。这是实现结构化并发的关键。
    • coroutineScope:内部任一子协程失败,会取消整个作用域及其所有子协程。
    • supervisorScope:内部子协程失败不会取消其他子协程或父作用域,实现了异常隔离。
  • 场景:当你需要在一个挂起函数内部并发启动多个子协程,并希望它们作为一个整体被管理时。
  • 代码示例:
    suspend fun fetchUserAndPosts(userId: String): Pair<User, List<Post>> = coroutineScope {
        // 这两个async是兄弟关系,都在coroutineScope管理下
        val userDeferred = async { api.getUser(userId) }
        val postsDeferred = async { api.getPosts(userId) }
        
        // 等待两者都完成
        val user = userDeferred.await()
        val posts = postsDeferred.await()
        Pair(user, posts)
    }
    // 在viewModel中调用
    viewModelScope.launch {
        val (user, posts) = fetchUserAndPosts("123")
        // 使用结果
    }
    

9.2.4 任务控制与状态 API

Job 状态属性
  • isActive: 协程是否处于活跃状态(已启动且未完成/未取消)。在循环或长任务中必须检查。
  • isCompleted: 协程是否已完成(正常结束或取消)。
  • isCancelled: 协程是否已被取消。
  • 示例:
    viewModelScope.launch {
        var i = 0
        while (isActive) { // 关键检查!
            // 模拟工作
            delay(1000)
            i++
            if (i > 10) break
        }
        Log.d("Coroutine", "Job completed. isActive=$isActive, isCancelled=$isCancelled")
    }
    
cancelAndJoin()
  • 作用:取消一个 Job 并挂起等待其完成(包括所有清理工作)。比单独调用 cancel() 后 join() 更简洁安全。
  • 场景:需要确保一个任务被完全取消并清理后再执行后续操作。
    private var dataLoadingJob: Job? = null
    
    fun loadData() {
        dataLoadingJob?.cancelAndJoin() // 等待旧任务完全停止
        dataLoadingJob = viewModelScope.launch {
            // 加载新数据
        }
    }
    
ensureActive()
  • 作用:如果当前协程已不活跃(isActive 为 false),则立即抛出 CancellationException。是 if (!isActive) throw CancellationException() 的简洁写法。
  • 场景:在可能耗时的循环或计算中,频繁检查协程状态,及时响应取消。
    viewModelScope.launch {
        for (item in largeList) {
            ensureActive() // 每次循环都检查,及时退出
            processItem(item) // 耗时处理
        }
    }
    
yield()
  • 作用:主动挂起当前协程,将线程让给其他协程执行,然后立即恢复。用于避免一个协程长时间独占线程,提升响应公平性。
  • 场景:在 CPU 密集型的循环中,防止阻塞事件循环(特别是在单线程调度器如 Dispatchers.Main 上)。
    viewModelScope.launch(Dispatchers.Default) {
        for (i in 1..1_000_000) {
            heavyComputation(i)
            yield() // 每计算一次就让出一次,避免卡死线程
        }
    }
    

9.2.5 async 的启动模式:CoroutineStart.LAZY

  • 作用:async(start = CoroutineStart.LAZY) 创建的 Deferred 任务不会立即启动,只有在首次调用 await() 或 start() 时才会开始执行。
  • 场景:需要延迟计算、或按需触发异步任务时。
  • 代码示例:
    viewModelScope.launch {
        // 定义任务,但不执行
        val lazyTask = async(start = CoroutineStart.LAZY) {
            println("Lazy task executed!")
            expensiveComputation()
        }
        
        // ... 其他操作
        
        if (needResult) {
            val result = lazyTask.await() // 此时才开始执行计算
        }
        // 如果 never call await(), the task never runs.
    }
    

9.2.6 runblocking

runBlocking 返回的是协程作用域 CoroutineScope,但它本身是一个普通顶层函数,不是接口 / 类定义的 Scope
补充类似用法对比
在这里插入图片描述

9.3 深入理解join的挂起含义

一个很严重的误区: job.join挂起的不是job这个协程,而是调用job的那个协程;

实例 1:简单等待单个协程完成再往下执行

viewModelScope.launch(Dispatchers.Main) {
    val job = launch(Dispatchers.IO) {
        println("开始网络请求")
        delay(1500) //模拟网络
        println("网络请求完成")
    }

    println("我不会等job,直接走到这里")
    job.join() //挂起当前Main协程,等待job执行完毕
    println("网络结束之后,才执行更新UI")
}


//输出:1和2的顺序不是固定的,可能反过来
1.我不会等job,直接走到这里
2.开始网络请求
3.网络请求完成
4.网络结束之后,才执行更新UI

实际上不推荐这么写,正确的写法是:

viewModelScope.launch(Dispatchers.Main) {
    println("准备发起网络")
    // 切换IO,这里会挂起外层协程,等待IO块全部执行完,再回来主线程
    withContext(Dispatchers.IO) {
        println("开始网络请求")
        delay(1500)
        println("网络请求完成")
    }
    // 一定等上面全部跑完,才走到这里更新UI
    println("网络结束之后,才执行更新UI")
}

delay和join的差异

  • delay 是我自己睡一段时间,我把线程交出去;
  • join 是我等另一个协程干完活,我自己挂起让出线程,但是那个被等待的协程继续跑,不会停下来。

十、实战案例

题 1:同时发起 3 个独立接口,全部返回后合并渲染,任一失败整体失败

思路:coroutineScope + async 并发;coroutineScope 语义就是 “所有子协程完成才返回,任一异常全部取消”,正好匹配 “整体失败” 需求。

suspend fun loadPage(): PageData = coroutineScope {
    val d1 = async(Dispatchers.IO) { api.userInfo() }
    val d2 = async(Dispatchers.IO) { api.config() }
    val d3 = async(Dispatchers.IO) { api.banner() }
    PageData(d1.await(), d2.await(), d3.await())
}

题 2:接口超时 + 失败重试 3 次,带退避

思路:withTimeout 包单次请求;外层循环计数,指数退避 delay。

suspend fun <T> requestWithRetry(
    times: Int = 3,
    block: suspend () -> T
): T {
    repeat(times - 1) { attempt ->
        try {
            return withTimeout(3000) { block() }
        } catch (e: Exception) {
            if (e is CancellationException) throw e
            delay(300L * (attempt + 1)) // 退避
        }
    }
    return withTimeout(3000) { block() } // 最后一次不捕获
}

题 3:SSE / 流式接口,页面退出时安全关闭连接

思路:持有 Job,退出时 cancelAndJoin(),保证 finally 里的流关闭执行完,避免连接泄漏。

private var sseJob: Job? = null

fun startSse() {
		// 启动新连接前,关闭旧连接
    sseJob?.cancel()
    sseJob = viewModelScope.launch(Dispatchers.IO) {
        try {
            api.streamChat().collect { chunk -> _ui.emit(chunk) }
        } finally {
            sseClient.close() // 取消时必走
        }
    }
}

override fun onCleared() {
    super.onCleared()
}

题 4:搜索框防抖 + 最新请求生效,旧请求丢弃

思路:Flow.debounce 防抖 + collectLatest 自动取消上一次未完成的请求。

viewModelScope.launch {
    searchQuery
        .debounce(300)          // 停止输入300ms后触发
        .filter { it.length >= 2 }
        .collectLatest { kw ->   // 新值来,取消上一次collect
            val result = withContext(Dispatchers.IO) { api.search(kw) }
            _ui.value = result
        }
}

题 5:串行执行任务链,中间可取消,异常统一处理

思路:withContext 串行切换线程(不新建协程),外层 try-catch 兜底。
对比题1,为啥async是并行,withContext是串行
withContext不会创建新的协程,只是挂起当前协程(下述案例就是Main);Async是创建一个新的协程,然后丢给调度队列去执行

viewModelScope.launch {
    try {
        val token = withContext(Dispatchers.IO) { api.refreshToken() }
        val data = withContext(Dispatchers.IO) { api.fetchData(token) }
        val cached = withContext(Dispatchers.IO) { db.save(data) }
        _ui.value = cached
    } catch (e: Exception) {
        _ui.value = Error(e.message)
    }
}

题 6:多个任务并发,但只要第一个成功返回,其余取消

思路:select + async,谁先 onAwait 返回就用谁,其余 cancel。
场景:主备接口竞速、多 CDN 节点测速。

suspend fun fastest(): String = coroutineScope {
    val d1 = async { api.fromCDN() }
    val d2 = async { api.fromOrigin() }
    val result = select<String> {
        d1.onAwait { it }
        d2.onAwait { it }
    }
    coroutineContext.cancelChildren() // `select`只是**选择第一个就绪事件,不会自动取消其它 async 任务**。
    // `coroutineContext.cancelChildren()`:取消当前父上下文的全部子 Job,
    // **父协程自己不取消**,只干掉 d1/d2 剩下那个慢任务。
    result
}

  1. select
    select 是协程的选择表达式:监听多个可挂起的事件,哪一个事件最先完成,就执行对应分支,整个 select 立刻返回这个结果,其它分支不再处理。

select 会挂起当前协程,直到第一个事件就绪,就结束 select。

  1. deferred.onAwait { it }
    onAwait 是 select 的子从句:监听一个Deferred(async 返回对象),当这个 async 执行完成,就执行 lambda,lambda 返回值作为 select 整体返回值。
  • d1.onAwait {it}:d1 完成 → select 取 d1 的结果

题 7:生命周期感知 —— 页面不可见时暂停轮询,可见恢复

思路:lifecycleScope.launchWhenStarted,进入后台自动挂起,回前台恢复,销毁自动取消。

lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        while(true){
            val data = withContext(Dispatchers.IO){ api.poll() }
            updateUi(data)
            delay(5000)
        }
    }
}


kotlin协程-Android实战 都说三年是程序员的一个坎,能否晋升或者提高自己的核心竞争力,这几年就十分关键。技术发展的这么快,从哪些方面开始学习,才能达到高级工程师水平,最后进阶到Android架构师/技术专家?我总结了这 5大块;我搜集整理过这几年阿里,以及腾讯,字节跳动,华为,小米等公司的面试题,把面试的要求和技术点梳理成一份大而全的“ Android架构师”面试 PDF(实际上比预期多花了不少精力),包含知识脉络 + 分支细节。Java语言与原理;大厂,小厂。Android面试先看你熟不熟悉Java语言高级UI与自定义view。 阅读详情

相关推荐

kotlin协程一 :kotlin协程介绍以及android中简单使用

在kotlin中协程的概念 kotlin协程这个概念不是一天两天提出的了,有兴趣想学一学,但是在网上你可能会看到很多非常专业的术语: 协程和线程类似;就像一种轻量级的线程;是协作式的,不需要线程的同步操作;协程是用户态的,他的切换不需要和操作系统交互............ 等等一些话。 讲道理,看完这些话仿佛看到了一个超越自己认知的新东西,这些话在其他的语言中可能是对的,但是和kotli...

zy_jibai 1472

Android面试核心知识体系与实战技巧

Android开发中的多线程与消息机制是构建高性能应用的核心技术,涉及Handler-Looper-MessageQueue的协同工作原理。通过理解线程模型和内存管理机制,开发者可以优化应用性能,避免常见的内存泄漏问题。在实际开发中,合理使用线程池和异步任务处理技术(如Kotlin协程或RxJava)能显著提升应用响应速度。这些技术不仅适用于日常开发,也是Android高级面试中的高频考点,特别是在探讨系统原理和性能优化方案时。掌握这些基础原理和技术实践,有助于开发者在面试中展现深厚的技术功底,同时为构建稳

weixin_34357436的博客 341

如何正确的在 Android 上使用 Kotlin 协程?

前言 你还记得是哪一年的 Google IO 正式宣布 Kotlin 成为 Android 一级开发语言吗?是 Google IO 2017 。如今两年时间过去了,站在一名 Android 开发者的角度来看,Kotlin 的生态环境越来越好了,相关的开源项目和学习资料也日渐丰富,身边愿意去使用或者试用 Kotlin 的朋友也变多了。常年混迹掘金的我也能明显感觉到 Kotlin 标签下的文章慢慢变...

ma1756295644的博客 1446

kotlin知识体系(五) :Android 协程全解析,从作用域到异常处理的全面指南

介绍Android中协程相关的知识点

朱豪凯的博客 1988

学习stm32H7之路开始(STM32H7系列1) 前期准备,HAL库框架

2020.01.07 几个重要的参考网址 1.电子工程世界网 http://tags.eeworld.com.cn/tags/STM32H7 链接: link. 2.转:https://blog.youkuaiyun.com/simon223/article/list/2?t=1 链接: link. ———————————————— 版权声明:本文为优快云博主「Simon223」的原创文章,遵循 CC 4....

kengkengkeng963的博客 3408

CGNS快速入门到实战(二):VS配置调用CGNS静态链接库

CGNS快速入门到实战这一系列分成三篇文章展开叙述,具体有:(一)CGNS静态链接库编译;(二)VS配置调用CGNS静态链接库;(三)CGNS文件及案例展示。本篇文章重点讲述VS配置调用CGNS静态链接库。

ahang_99的博客 2616

WanAndroidClient项目总览:Kotlin+MVP打造学习阅读器Android客户端的完整指南

WanAndroidClient 是一款用 **Kotlin** 编写的玩Android(wanandroid.com)学习内容阅读客户端,基于经典的 **Kotlin + MVP + Kotlin协程 + Retrofit2** 技术栈构建。整个项目仅用一个 app 模块、约 60 个 Kotlin 文件,就完整实现了首页、知识体系、搜索、登录、收藏等 10 大功能,是初学者学习 Android

gitblog_01192的博客 1078

输入过欠压保护电路分析

输入过欠压保护电路仿真分析

HW-BASE的博客 6905

一篇讲透掌握 Kotlin 协程

Kotlin协程学习指南:从基础到进阶 摘要:本文系统介绍了Kotlin协程的学习路径。基础部分涵盖协程本质、环境配置和核心概念;进阶内容包括结构化并发、挂起函数、async/await、异常处理等关键能力;实战部分展示了安卓项目中的最佳实践和并发任务处理技巧。文章还总结了常见错误规避方法和底层原理要点,强调从实践到理论的学习路径,帮助开发者系统掌握协程技术。核心要点包括:协程的轻量级特性、结构化并发原则、Flow响应式编程以及生命周期管理等重要概念。

fuge2008的博客 957

手把手教你学Simulink--H桥逆变器的PWM控制设计与仿真

本文详细介绍了如何在Simulink环境中设计和仿真一个基于PWM控制的H桥逆变器。H桥逆变器广泛应用于电机驱动、UPS和可再生能源系统等领域,通过PWM技术可以有效控制输出电压的幅度和频率,减少谐波失真,提高系统效率和稳定性。文章首先介绍了所需的工具和环境,包括MATLAB/Simulink和SimscapeElectrical工具箱。接着,详细阐述了从定义任务需求、创建Simulink项目、建立基础电气模型、集成传感器模型、实现PWM控制算法到设置仿真参数和运行仿真的完整步骤。最后,文章总结了通过Sim

MHD0815的博客 718

45、概率与模糊性:粗糙事件与模糊事件概率的探索

本文探讨了概率理论与模糊性理论在处理现实世界不确定性中的应用,重点分析了模糊事件和粗糙事件的概率定义、性质及其条件概率。同时,介绍了近似贝叶斯网络分类器在信息系统中的应用,以及如何通过概率决策约简提高分类效率。这些方法为处理不确定信息提供了灵活的工具,并在决策和数据分析中具有重要价值。

ik67890123的博客 133

[译] Kotlin 协程高级使用技巧

面试题集可以帮助你查漏补缺,有方向有针对性的学习,为之后进大厂做准备。但是如果你仅仅是看一遍,而不去学习和深究。那么这份面试题对你的帮助会很有限。最终还是要靠资深技术水平说话。网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。建议先制定学习计划,根据学习计划把知识点关联起来,形成一个系统化的知识体系。学习方向很容易规划,但是如果只通过碎片化的学习,对自己的提升是很慢的。

2401_84121646的博客 1010

RK3588 实战级 YOLOv5 Android 全解析:NPU 量化 + 多线程 + 跟踪算法,源码一次买齐

本文将带你完成一个完整的AI视觉项目:在搭载RK3588芯片的Android开发板上运行实时目标检测与跟踪应用。实战目标:成功运行官方YOLOV5摄像头实时检测案例技术深度:深入理解RKNN开发框架和源码实现核心技能:掌握NPU推理、多线程并发、目标跟踪等关键技术。

FJN110的博客 156

Android 技术文章合集 | Jetpack、Kotlin、Window & View、事件机制、多线程、存储等

代码说」:一个专注于 Android 技术细节拆解的分享号,欢迎关注!

小马快跑的博客 931

自动化测试与AI的结合应用

自动化测试,人工智能,机器学习,深度学习,测试用例生成,缺陷预测,测试优化 1. 背景介绍 在软件开发领域,自动化测试已成为不可或缺的一部分,它可以提高测试效率、降低测试成本,并确保软件质量。然而,传统的自动化测试方法往往依赖于人工编写的测试用例,这不仅耗时费力,而且难以应对复杂软件

AI架构师小马 2246

【异常】Claude Code CLI工具 模型加载异常 /init报错排查与完整解决方案 There‘s an issue with the selected model (glm-5). It

此类跨厂商模型配置报错,核心诱因是API生态的兼容性不匹配。优先使用工具原生支持的模型体系,可快速解决90%以上的同类报错;若需跨厂商使用目标模型,需完成API端点、鉴权密钥、模型ID的全链路适配,确保全流程配置合规,即可完成异常修复。

奶爸的编程之路,也就一周冷个三天 3702

【kotlin 协程】万字协程 一篇完成kotlin 协程进阶

在 Android 平台上,协程主要用来解决两个问题:从本质上来说,协程就是一个轻量级的线程。在使用协程之前,我们需要先引入Coroutine 的包 创建协程的方式有很多种,这里不延伸协程的高级用法(热数据通道Channel、冷数据流Flow.…),创建协程这里介绍常用的三种方式: runBlocking 启动一个协程会阻塞调用它的线程,只到里面的代码执行结束,返回值是泛型T。 launch启动一个协程不会阻塞调用线程,必须要在协程作用域(CoroutineScope)中才能调用,返回值是一个Job asy

半身风雪 5676

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

Android Kotlin 协程详解

Kotlin 协程是 Android 异步编程的革命性工具,通过轻量级、结构化并发和响应式编程,极大提升了代码的可读性和可维护性。掌握协程的基础、进阶技巧和最佳实践,能够显著提高开发效率,减少内存泄漏和线程管理问题。建议结合官方文档和实际项目不断实践,逐步深入理解协程的核心原理和应用场景。

ZLGSPACE的博客 3377

IDEA的使用指南

IDEA基于java的使用手册,可以最快速的上手,在使用中不断的丰富自己的使用技巧.

visio安装包

Microsoft Office Visio 是一款专业的制图软件。利用经过更新的形状、协作工具和数据链接图表,创建专业级图表以简化复杂的信息。

Android Kotlin 协程(coroutine )入门篇

ps:文章会很长 协同程序(coroutine)实在是太好用了,所以我们来学习吧!

我怀里的猫的博客 9469

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

Kotlin协程的使用

目录 引入kotlin协程 kotlin协程的三种启动方式 初识协程: 第一种启动方式(runBlocking:T) 第二种启动方式(launch:Job) 第三种启动方式(async/await:Deferred) 实现你的第一个Coroutine程序 浅析Kotlin协程 ——协程是什么?https://blog.youkuaiyun.com/qq_17798399/article/det...

青冘的技术学习博客 1万+

Android Kotlin中协程详解

使用Kotlin协程需要引入Kotlin协程依赖包,这里引入的Kotlin核心依赖包,需要根据当前项目使用的Kotlin版本来引入,我使用的Kotlin版本为1.4.32所以引入的Kotlin核心依赖包版本为:1.4.3。那么这里可能就有人要问了,这个挂起函数有什么用呢,在实际项目中,我们可以封装一些网络请求,IO操作等耗时的功能封装在挂起函数,这样别人看到之后也能明白,这些挂起函数是要在协程中进行使用的。中执行,所以当开启协程的时候,协程作用域中的代码不一定是执行在子线程的,这取决于这个线。

路宇的博客 2053

在 Android 开发中使用 Kotlin 协程 (一) -- 初识 Kotlin 协程

前言 最近在研究 Kotlin 协程,发现功能真的超级强大,很有用,而且很好学,如果你正在或计划使用 Kotlin 开发 Android,那么 Kotlin 协程你一定不能错过! 协程是什么? 我们平常接触的都是进程、线程,在开发中使用最多的就是线程,比如主线程、子线程,而且操作系统里最小可操作的单元就是线程,那协程又是什么?协程是比线程更小的单位,但并不是说在操作系统里最小可操作单元就从线程变成...

weixin_34191845的博客 5252

Kotlin协程在android中的使用总结

协程的三大件共同构成了Kotlin协程的核心机制,使得开发者能够编写高效、易于理解和维护的异步代码。

ti_an_1989的专栏 1455

Android Kotlin协程实战

数据通信系统或计算机网络系统中,传输媒体的带宽或容量往往会大于传输单一 信号的需求,为了有效地利用通信线路,希望一个信道同时传输多路信号,这就是 所 谓 的 多 路 复 用 技 术 (M u l t i p l e x i n g )。

AdrianAndroid的专栏 1604

Android Kotlin 协程

Android Kotlin 协程是一种基于Kotlin语言的轻量级线程管理框架,由JetBrains公司于2017年开发并集成在Kotlin 1.3版本的标准库中。它通过挂起函数(suspend function)和结构化并发(structured concurrency)的概念,为Android开发提供了简单高效的异步编程解决方案。JetBrains官方数据显示,使用协程可以将异步代码量减少50%以上,同时显著降低内存泄露的风险。

Made In SQL 874
上一篇: 【Android面试】数据库
下一篇: 【Android 架构】MVC/MVP/MVVM/MVI架构
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值