Android Kotlin协程实战

你能听懂的Kotlin协程课,跟老司机学,不用自己瞎折腾

认识协程

协程难在哪儿?

  • Ja v a中不曾出现的,新概念
  • 概念不清晰,我们看到的大都是不同语言对于协程的实现或者衍生
  • Kotlin 基础不扎实
  • 多线程编程基础太薄弱

协程是什么?

协程基于线程,它是轻量级线程。

在An dr oid 中协程用来解决什么问题?

  • 处理耗时任务,这种任务常常会阻塞主线程。
  • 保 证 主线 程 安 全 , 即 确 保 安 全 地 从 主线 程 调 用 任 何 s u s p e n d 函 数 。

异 步任 务

使用协程

协程是什么?

  • 协程让异步逻辑同步化,杜绝回调地狱。
  • 协程最核心的点就是,函数或者一段程序能够被挂起,稍后再在挂起的位 置恢复。

协 程的挂起与恢复

常规函数基础操作包括:invoke (或call)和return,协程新增了suspend和resume:

• suspend---也称为挂起或暂停,用于暂停执行当前协程,并保存所有局部变量;

• resume---用于让已暂停的协程从其暂停处继续执行。

堆栈帧中的函数调用流程

挂起函数

  • 使用suspen d关键字修饰的函数叫作挂起函数。
  • 挂起函数只能在协程体内或其他挂起函数内调用。

调度器

协程的两部分

Kotlin的协程实现分为两个层次:

• 基础设施层,标准库的协程API,主要对协程提供了概念和语义上最基本的支持

• 业务框架层,协程的上层框架支

任 务泄 漏

  • 当某个协程任务丢失,无法追踪,会导致内存、CPU、磁盘等资源浪费,甚至发 送一个无用的网络请求,这种情况称为任务泄漏。
  • 为了能够避免协程泄漏,Kotlin引入了结构化并发机制。

结构化并发

使用结构化并发可以做到:

• 取消任务,当某项任务不再需要时取消它。

• 追踪任务,当任务正在执行是,追踪它。

• 发出错误信号,当协程失败时,发出错误信号表明有错误发生

CoroutineScope

  • 定义协程必须指定其CoroutineScope,它会跟踪所有协程,同样它还可以取消
    由它所启动的所有协程。
  • 常用的相关API有:
    • Globalscope,生命周期是pr ocess级别的,即使Activity或Fragment已经被销毁,协程仍然在执行。 • MainSc ope,在Activity中使用,可以在onDestroy(中取消协程。
    • viewModelScope,只能在ViewModel中使用,绑定ViewModel的生命周期。
    • lifecycleScope,只能在Activity、Fragment中使用,会绑定Activity和Fragment的生命周期。

启动协程

协 程构 建 器

  • launch与async构建器都用来启动新协程
    • launch,返回一个Job并且不附带任何结果值。
    • async,返回一个Deferred, Deferred也是一个Job,可以使用.await(在一个延期的值 上得到它的最终结果。
  • 等待一个作业 • joinSawait
    • 组合并发

协 程的启动 模式

DEFAULT:协程创建后,立即开始调度,在调度前如果协程被取消,其将直接进入取消 响应的状态。

ATOMIC:协程创建后,立即开始调度,协程执行到第一个挂起点之前不响应取消。

LAZY:只有协程被需要时,包括主动调用协程的start、join或者await等函数时才会开始

调度,如果调度前就被取消,那么该协程将直接进入异常结束状态。

UNDISPATCHED:协程创建后立即在当前函数调用栈中执行,直到遇到第一个真正挂起 的点

协 程的作用域构建器

coroutineScope-SrunBlocking

• runBlocking是常规函数,而coroutineScope是挂起函数。

• 它们都会等待其协程体以及所有子协程结束,主要区别在于runBlocking方法会阻塞当

前 线 程 来 等 待 , 而 c o r o u t i n e S c o p e 只 是 挂 起 , 会释 放 底 层 线 程 用 于其 他 用 途 。

协 程的作用域构建器

coroutineScope-supervisorScope

• coroutineScope:一个协程失败了,所有其他兄弟协程也会被取消。

• supervisorScope:一个协程失败了,不会影响其他兄弟协程。

J ob对象

对 于 每 一 个 创 建 的 协 程 (通 过 l a u n c h 或 者 a s y n c ) , 会 返 回 一 个 J o b 实 例 , 该 实 例 是 协 程

的唯一标示,并且负责管理协程的生命周期。
一个任务可以包含一系列状态:新创建(New)、活跃(Active)、完成中(Completi

ng)、已完成 (Completed)、取消中 (Cancelling)和已取消 (Cancelled)。虽然 我们无法直接访问这些状态,但是我们可以访问J o b 的属性:i s Ac t i v e 、i s Ca n ce l l e d 和i s

Completed.

J ob的生命周期

如 果 协 程 处 于活 跃 状 态 , 协 程 运 行 出 错 或 者 调 用 j o b . c a n c e l ()都 会 将 当 前 任 务 置 为 取 消 中 (C a n c e l l i n g ) 状 态 (i s A c t i v e = f a l s e , i s C a n c e l l e d = t r u e )。 当 所 有 的 子 协 程 都 完 成 后 , 协 程 会 进 入 已 取 消 (Cancelled)状态,此时isCompleted= true。

取消协程

协程的取消

取消作用域会取消它的子协程。 >被取消的子协程并不会影响其余兄弟协程。

协程通过抛出一个特殊的异常Cancell ationException 来处理取消操作。

所 有 k o t l i n x . c o r o u t i n e s 中 的 挂 起 函 数 (w i t h C o n t e x t 、 d e l a y 等 ) 都 是 可 取 消 的 。

C P U密集型任务取消

isActive是一个可以被使用在CoroutineScope中的扩展属性,检查Job是否处于活跃状态。 > ensureActive(),如果job处于非活跃状态,这个方法会立即抛出异常。

yield函数会检查所在协程的状态,如果已经取消,则抛出Cancellati onExcepti on予以响

应。此外,它还会尝试出让线程的执行权,给其他协程提供执行机会。

协程取消的副作用

在 f i n a l l y 中释 放 资 源 。

use函数:该函数只能被实现了Closeable的对象使用,程序结束的时候会自动调用close 方法,适合文件对象。

不能取消的任务

处于取消中状态的协程不能够挂起(运行不能取消的代码),当协程被取消后需要调用挂起函数,我们需要将清理任务的代码放置于NonCancellableCoroutineContext 中。

这样会挂起运行中的代码,并保持协程的取消中状态直到任务处理完成。

超时任务

很多情况下取消一个协程的理由是它有可能超时。

with Time out OrNull 通过返回null来进行超时操作,从而替代抛出一个异常。

协程的上下文

协程的上下文是什么?

CoroutineContext是 一组用于定义协程行为的元素。它由如下几项构成:

• Jo b:控制协程的生命周期

• Cor outineDis patc her:向合适的线程分发任务

• CoroutineName:协程的名称,调试的时候很有用 • CoroutineExceptionHandler:处理未被捕捉的异常

组 合 上下文中的元素

有时我们需要在协程上下文中定义多个元素。我们可以使用+操作符来实现。比如说,我 们可以显式指定 一个调度器来启动协程并且同时显式指定一个命名

协程 上下文的继承

对于新创建的协程,它的Cor outineContext会包含一个全新的Job实例,它会帮助我们 控制协程的生命周期。而剩 下的元素会从Corout ine Cont ext的父类继承,该父类可能是另外一个协程或者创建该协程的CoroutineSc ope。

协程 上下文的继承

最终的父级Cor outineCont ext 会内含Dispatchers.IO而不是scope对象里的Disp atchers.Mai n,因为 它被 协 程 的 构 建 器 里 的 参 数 覆 盖了。 此 外 , 注 意 一 下父 级 C o r o u t i n e C o n t e x t 里 的 J o b 是 s c o p e 对 象的Job (红色),而新的Job实例(绿色)会赋值给新的协程的CoroutineContext。

协程的异常处理

异常处理的必要性

当应用出现一些意外情况时,给用户提供合适的体验非常重要,一方面,目睹应

用崩溃是个很糟糕的体验,另一方面,在用户操作失败时,也必须要能给出正确 的提示信息。

异常的传播

协程构建器有两种形式:自动传播异常 (launch与actor),向用户暴露异常 (async与produce)当这些构建器用于创建 一个根协程时 (该协程不是另 一个协程 的子协程),前者这类构建器,异常会在它发生的第一时间被抛出,而后者则依

赖用户来最终消费异常,例如通过await或receive 。

非根协程的异常

其他协程所创建的协程中,产生的异常总是会被传播。

异常的传播特性

当一个协程由于一个异常而运行失败时,它会传播这个异常并传递给它的父级。接下来, 父级会进行下面几步操作:

• 取消它自己的子级

• 取消它自己

• 将异常传播并传递给它的父级

SupervisorJob

使用Su pervisorJob时,一个子协程的运行失败不会影响到其他子协程。Supervis or Job 不会传播异常给它的父级,它会让子协程自己处理异常。

这种需求常见于在作用域内定义作业的UI组件,如果任何一个UI的子作业执行失

败了,它并不总是有必要取消整个UI组件,但是如果UI组件被销毁了,由于它的 结果不再被需要了,它就有必要使所有的子作业执行失败。

supervisorScope

当作业自身执行失败的时候,所有子作业将会被全部取消。

异常的捕获

使用Cor outineEx cepti onHandler对协程的异常进行捕获。

以下的条件被满足时,异常就会被捕获:

• 时机:异常是被自动抛出异常的协程所抛出的(使用launch,而不是async时);

• 位置:在CoroutineScope的CoroutineContext中或在一个根协程(CoroutineScope 或者supervisorSc ope 的直接子协程)中。

A n d r oi d 中全局 异常处理

全局异常处理器可以获取到所有协程未处理的未捕获异常,不过它并不能对异常进行捕获, 虽然不能阻止程序崩溃,全局异常处理器在程序调试和异常上报等场景中仍然有非常大的

用处。

我们需要在Classpath 下面创建META-INF/services目录,并在其中创建一个名为kotlinx. coroutines.Cor outineExceptionHandler的文件,文件内容就是我们的全局异常处理器

的全类名。

取消与异常

取 消 与 异常 紧 密 相 关 , 协 程 内 部 使 用 C a n c e l l a t i o n E x c e p t i o n 来进 行 取 消 , 这 个异 常 会被 忽略。

当子协程被取消时,不会取消它的父协程。

如 果 一 个 协 程 遇 到 了C a n c e l l a t i o n E x c e p t i o n 以 外 的 异 常 , 它 将 使 用 该 异 常 取 消 它 的 父 协程。当父协程的所有子协程都结束后,异常才会被父协程处理。

常聚合

当协程的多个子协程因为异常而失败时,一般情况下取第一个异常进行处理。在 第一个异常之后发生的所有其他异常,都将被绑定到第一个异常之上。

认识Flow

如何表示多个值?

挂起函数可以异步的返回单个值,但是该如何异步返回多个计算好的值呢?

异步返回多个值的方案

集合

序列

挂起函数

Flow

Fl ow 与其他方式的区别

名为fl ow的Fl ow类型构建器函数。

f l o w {. . }构 建 块 中 的 代 码 可 以 挂 起 。

函数si mpl eFlow不再标有susp end修饰符。

流使用emit 函数发射值。

流使用col l ec t 函数收集值。

Fl o w 应用

在Androi d当中,文件下载是Fl ow的一个非常典型的应用。

冷流

Fl ow是一种类似于序列的冷流,fl ow构建器中的代码直到流被收集的时候才运行。

流 的连 续 性

流的每次单独收集都是按顺序执行的,除非使用特殊操作符。

从上游到下游每个过渡操作符都会处理每个发射出的值,然后再交给末端操作符。

流 构建 器

flowOf构建器定义了一个发射固定值集的流。

使用.asFl ow()扩展函数,可以将各种集合与序列转换为流。

上下文

流的收集总是在调用协程的上下文中发生,流的该属性称为上下文保存。

f l o w {. . }构 建 器 中 的 代 码 必 须 遵 循 上 下 文 保 存 属 性 , 并 且 不 允 许 从 其 他 上 下 文 中 发 射 (emit)。

flowOn操作符,该函数用于更改流发射的上下文。

启动流

使用launchl n替換collect 我们可以在单独的协程中启动流的收集

流的取消

流采用与协程同样的协作取消。像往常一样,流的收集可以是当流在一个可取消的挂起函数 (例如delay)中挂起的时候取消。

流的取消检测

为方便起见,流构建器对每个发射值执行附加的ensureActive 检测以进行取消,这 意 味 着 从 f l o w {... } 发 出 的 繁 忙 循 环 是 可 以 取 消 的 。

出于性能原因,大多数其他流操作不会自行执行其他取消检测,在协程处于繁忙循环的情况下,必须明确检测是否取消。

通 过 c a n c e l l a b l e 操 作 符 来执 行 此 操 作 。

背压

buffer(,并发运行流中发射元素的代码。

c o n f l a t e (), 合 并 发 射 项 , 不 对 每 个 值 进 行 处 理 。

collectLate st(),取消并重新发射最后一个值。

当必须更改Cor outineDispatcher时,flowOn操作符使用了相同的缓冲机制,但 是buffer函数显式地请求缓冲而不改变执行上下文。

操作符

过 渡流 操 作 符

可以使用操作符转换流,就像使用集合与序列一样。

过渡操作符应用于上游流,并返回下游流。

这些操作符也是冷操作符,就像流一样。这类操作符本身不是挂起函数。

它运行的速度很快,返回新的转换流的定义。

末端流操作符

末端操作符是在流上用于启动流收集的挂起函数。collect是最基础的未端操作符,但是 还有另外 一些更方便使用的末端操作符:

• 转化为各种集合,例如t oList 与t oSet 。

• 获取第一个(first)值与确保流发射单个(single)值的操作符。

• 使 用 r e d u c e 与 f o l d 将 流 规 约 到 单 个值 。

组合多个流

就像Kotlin标准库中的Sequen ce.zip扩展函数一样,流拥有一个zip操作符用于组 合两个流中的相关值。

展平流

流表示异步接收的值序列,所以很容易遇到这样的情况 :每个值都会触发对另一 个值序列的请求,然而,由于流具有异步的性质,因此需要不同的展平模式,为此,存在一系列的流展平操作符: • flatMapConcat连接模式,

• flatMapMerge 合并模式

• flatMapLatest 最新展平模式

异常

流的异常处理

当运算符中的发射器或代码抛出异常时,有几种处理异常的方法:

• try/catcht

• catch函数

流的完成

当流收集完成时 (普通情况或异常情况),它可能需要执行一个动作。

• 命令式finally块

• onCompletion声明式处理

Channel-通道

认识Chann el

Channel实际上是一个并发安全的队列,它可以用来连接协程,实现不同协程的 通信。

Channel的容量

Channel实际上就是一个队列,队列中一定存在缓冲区,那么一旦这个缓冲区满了,并且 也 一 直没 有 人 调 用 r e c e i v e 并 取 走 函 数 , s e n d 就 需 要 挂 起 。 故 意 让 接 收 端 的 节奏 放 慢 , 发 现send总是会挂起,直到re ceive之后才会继续往下执行。

迭代Chann el

Channel 本身确实像序列,所以我们在读取的时候可以直接获取一个Channel 的it erator.

produceSactor

构造生产者与消费者的便捷方法。

我们可以通过pr oduce 方法启动一个生产者协程,并返回一个Receiv eChann el,其他协

程 就 可 以 用 这 个 C h a n n e l 来 接 收 数 据 了。 反 过 来 , 我 们 可 以 用 a c t o r 启 动 一 个消 费 者 协 程 。

Channel的关闭

pr oduce和actor返回的Channel都会随着对应的协程执行完毕而关闭,也正是这样,Ch a n n e l 才 被 称 为热 数据 流 。

对于一个Channel,如果我们调用了它的close方法,它会立即停止接收新元素,也就是 说 这 时 它 的 i s C l o s e d F o r S e n d 会 立 即 返 回 t r u e 。 而 由 于C h a n n e l 缓 冲 区 的 存 在 , 这 时 候 可能还有 一些元素没有被处理完,因此要等所有的元素都被读取之后isClosedForReceive 才会 返 回 t r u e 。

Ch annel的生命周期最好由主导方来维护,建议由主导的一方实现关闭。

BroadcastChannel

前面提到,发送端和接收端在Channel 中存在一对多的情形,从数据处理本身来

讲,虽然有多个接收端,但是同一个元素只会被一个接收端读到。广播则不然,

多个接收端不存在互斥行为。

多路复用

什么是多路复用

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

复 用 多 个a w a i t

两个API分别从网络和本地缓存获取数据,期望哪个先返回就先用哪个做展示。

复用多个Channel

跟await类似,会接收到最快的那个channel消息。

SelectClause

我们怎么知道哪些事件可以被select呢?其实所有能够被select的事件都是SelectClauseN类型,包括:

• Select ClauseO:对应事件没有返回值,例如join没有返回值,那么onJoin就是SelectClauseN类型。使用时,onJ oin的参数是一个无参函数。

• SelectClause1:对应事件有返回值,前面的onAWait和onReceive都是此类情况。

• Select Clause2:对应事件有返回值,此外还需要一个额外的参数,例如Channel.onSend有两个参数,第一

个是Channel数据类型的值,表示即将发送的值;第二个是发送成功时的回调参数。
如果我们想要确认挂起函数是否支持select,只需要查看其是否存在对应的Select Clause

N类型可回调即可。

使用Flow实现多路复用

多数情况下,我们可以通过构造合适的Fl ow来实现多路复用的效果。

并发安全

不安全的并发访问

我们使用线程在解决并发问题的时候总是会遇到线程安全的问题,而Java 平台上

的Kotlin协程实现免不了存在并发调度的情况,因此线程安全同样值得留意。

协 程的并发工具

除了我们在线程中常用的解决并发问题的手段之外,协程框架也提供了一些并发 安全的工具,包括:

• Channel:并发安全的消息通道,我们已经非常熟悉。

• Mutex:轻量级锁,它的lock和unlock从语义上与线程锁比较类似,之所以轻量是因为它在获取

不到锁时不会阻塞线程,而是挂起等待锁的释放。

• Semaphore:轻量级信号量,信号量可以有多个,协程在获取到信号量后即可执行并发操作。

当 S e m a p h o r e 的 参 数 为 1 时 , 效 果 等 价 于M u t e x 。

避免访问外部可变状态

编写函数时要求它不得访问外部状态,只能基于参数做运算,通过返回值提供运 算结果。

相关推荐
肖。354878709417 小时前
[技巧-11]AndroidManifest.xml完善小技巧。
android
小羊子说17 小时前
Android 车机开发中常用的adb 脚本(更新中)
android·linux·adb·性能优化·车载系统
用户76074953978317 小时前
Android页面四大布局运行结果
android
风往哪边走17 小时前
搜索框自定义
android
用户82492819253618 小时前
把android资源类型详解
android
IT观测18 小时前
深度分析俩款主流移动统计工具Appvue和openinstall
android·java·数据库
用户3386755819518 小时前
Android 四种常用布局完全解析(附实战项目截图)
android
用户50875321684418 小时前
Android 资源类型全解析:深入理解四种常用布局
android
XiaoLeisj19 小时前
Android 文件存储实战:从应用私有目录读写到网络文件落盘与公共存储接入
android·java·网络·文件操作
恋猫de小郭19 小时前
Android Studio Panda 2 ,支持 AI 用 Vibe Coding 创建项目
android·前端·flutter