一文读懂kotlin协程常用知识点,安卓开发快速学习

一文读懂kotlin协程常用知识点,安卓开发快速学习,第1张

一文读懂kotlin协程常用知识点,安卓开发快速学习 创建协程

创建协程有三种方式:launch、async、runBlocking

launch

launch 方法签名如下:

public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
//省略
return coroutine
}

launch 是 CoroutineScope 的扩展方法,需要 3 个参数。第一个参数,看字面意思是协程上下文,后边会重点讲到。第二个参数是协程启动模式,默认情况下,协程是创建后立即执行的。第三个参数,官方文档说这个 block 就是协程代码块,所以是必传的。返回的是一个 Job,这个 Job 可以理解为一个后台工作,在 block 代码块执行完成后会结束,也可以通过 Job 的 cancel 方法取消它。

async

async 方法签名如下:

public fun CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred {
//省略
return coroutine
}

同样也是 CoroutineScope 的扩展方法,参数跟 launch 是一模一样的,只是返回参数变成了 Deferred,这个 Deferred 继承于 Job,相当于一个带返回结果的 Job,返回结果可以通过调用它的 await 方法获取。

runBlocking

runBlocking 会阻塞调用他的线程,直到代码块执行完毕。

Log.i(“zx”, “当前线程1-” + Thread.currentThread().name)
runBlocking(Dispatchers.IO) {
delay(2000)
Log.i(“zx”, “休眠2000毫秒后,当前线程” + Thread.currentThread().name)
}
Log.i(“zx”, “当前线程2-” + Thread.currentThread().name)

输出内容

当前线程1-main
休眠2000毫秒后,当前线程DefaultDispatcher-worker-1
当前线程2-main

可以看到,即使协程指定了运行在 IO 线程,依旧会阻塞主线程。runBlocking 主要用来写测试代码,平常不要随意用,所以不再过多介绍。

CoroutineScope 协程作用

launch 和 async 都是 CoroutineScope 的扩展函数,CoroutineScope 又是什么呢,字面意思翻译过来是协程作用域,协程作用域类似于变量作用域,定义了协程代码的作用范围。作用域取消时,作用域中的协程都会被取消。 比如如下代码:

MainScope().launch {
var i = 0

launch(Dispatchers.IO) {
while (true) {
Log.i(“zx”, “子协程正在运行着$i”)
delay(1000)
}
}

while (true) {
i++
Log.i(“zx”, “父协程正在运行着$i”)

if (i>4) {
cancel()
}
delay(1000)
}
}

输出:

父协程正在运行着1
子协程正在运行着1
父协程正在运行着2
子协程正在运行着2
父协程正在运行着3
子协程正在运行着3
父协程正在运行着4
子协程正在运行着4
子协程正在运行着4
父协程正在运行着5

5 秒后,父协程调用 cancel()结束了,子协程也就结束了,并没有继续打印出值。

可以通过 CoroutineScope()来创建协程作用域,这并不是一个构造函数,CoroutineScope 是一个接口,所以没有构造函数,只是函数名与接口名同名而已,源码如下:

@Suppress(“FunctionName”)
public fun CoroutineScope(context: CoroutineContext): CoroutineScope =
ContextScope(if (context[Job] != null) context else context + Job())

源码可见,创建 CoroutineScope 时需要传入 CoroutineContext,这个 CoroutineContext 也是 CoroutineScope 接口中唯一的成员变量。CoroutineScope.kt 这个文件中使用 CoroutineScope()创建了两个 Scope,一个是 MainScope,一个是 GlobalScope。源码如下:

public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

public object GlobalScope : CoroutineScope {
override val coroutineContext: CoroutineContext
get() = EmptyCoroutineContext
}

MainScope 是一个方法,返回了一个运行在主线程的作用域,需要手动取消。GlobalScope 是一个全局作用域,整个应用程序生命周期他都在运行,不能提前取消,所以一般不会使用这个作用域。Android 中,ktx 库提供了一些常用的作用域供我们使用,如 lifecycleScope 和 viewModelScope。在 LifecycleOwner 的所有实现类中,如 Activity 和 Fragment 中都可以直接使用 lifecycleScope,lifecycleScope 会跟随 Activity 或 Fragment 的生命周期,在 Activity 或 Fragment 销毁时,自动取消协程作用域中的所有协程,不用手动管理,不存在内存泄露风险。类似的 viewModelScope 也会随着 viewModel 的销毁而取消。

目前已经有好几个地方出现了 CoroutineContext:启动协程时 launch 或者 async 方法需要 CoroutineContext,创建协程作用域时需要 CoroutineContext,协程作用域中有且只有一个成员变量也是 CoroutineContext,如下源码所示:

public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}

如此看来,CoroutineContext 必定很重要。

CoroutineContext 协程上下文

CoroutineContext 保存了协程的上下文,是一些元素的集合(实际并不是用集合 Set 去存储),集合中每一个元素都有一个唯一的 key。通俗来讲,CoroutineContext 保存了协程所依赖的各种设置,比如调度器、名称、异常处理器等等。

CoroutineContext 源码如下:

public interface CoroutineContext {

public operator fun get(key: Key): E?

public fun fold(initial: R, operation: (R, Element) -> R): R

public operator fun plus(context: CoroutineContext): CoroutineContext =
if (context === EmptyCoroutineContext) this else
context.fold(this) { acc, element ->
//省略
}

public fun minusKey(key: Key<*>): CoroutineContext

public interface Key

public interface Element : CoroutineContext {
//省略
}
}

CoroutineContext 里有一个接口 Element,这个 Element 就是组成 CoroutineContext 的元素,最重要的是 plus *** 作符函数,这个函数可以把几个 Element 合并成为一个 CoroutineContext,由于是 *** 作符函数,所以可以直接用+调用。比如:

var ctx = Dispatchers.IO + Job() + CoroutineName(“测试名称”)
Log.i(“zx”, ctx.toString())

输出

[JobImpl{Active}@31226a0, CoroutineName(测试名称), Dispatchers.IO]

共有哪几种元素呢?来看看 Element 的子类吧。Element 有这么几个子类(子接口):Job、CoroutineDispatcher、CoroutineName、CoroutineExceptionHandler。

Job

Job 可以简单理解为一个协程的引用,创建协程后会返回 Job 实例,可以通过 Job 来管理协程的生命周期。Job 是 CoroutineContext 元素的一种,可以传入 CoroutineScope 用来使协程有不同的特性。主要关注Job()、SupervisorJob()这两个创建 Job 的函数以及Deferred这个 Job 的子接口。

Job()

创建一个处于活动状态的 Job 对象,可以传入父 Job,这样当父 Job 取消时就可以取消该 Job 以及他的子项。 该 Job 的
任何子项失败都会立即导致该 Job 失败,并取消其其余子项。这个很好理解,例如:

CoroutineScope(Dispatchers.IO + Job()+MyExceptionHandler()).launch {
var index = 0
launch {
while (true) {
index++
if (index > 3) {
throw Exception(“子协程1异常了”)
}
Log.i(“zx”, “子协程1正在运行”)
}
}

launch {
while (true) {
Log.i(“zx”, “子协程2正在运行”)
}
}
}

子协程 1 异常了,就会导致整个 Job 失败,子协程 2 也不会继续运行。

SupervisorJob()

创建一个处于活动状态的 Job 对象。 该 Job 的子项之间彼此独立,互不影响,子项的失败或取消不会导致主 Job 失败,也不会影响其他子项。

CoroutineScope(Dispatchers.IO + SupervisorJob() + MyExceptionHandler()).launch {
launch {
while (true) {
index++
if (index > 3) {
throw Exception(“子协程1异常了”)
}
Log.i(“zx”, “子协程1正在运行”)
}
}

launch {
while (true) {
Log.i(“zx”, “子协程2正在运行”)
}
}
}

同样的代码,把 Job()换成 SupervisorJob()后,可以发现子协程 2 会一直运行,并不会因为子协程 1 异常而被取消。

我们常见的 MainScope、viewModelScope、lifecycleScope 都是用 SupervisorJob()创建的,所以这些作用域中的子协程异常不会导致根协程退出。 kotlin 提供了一个快捷函数创建一个使用 SupervisorJob 的协程,那就是 supervisorScope。例如:

CoroutineScope(Dispatchers.IO).launch {
supervisorScope {
//这里的子协程代码异常不会导致父协程退出。
}
}

等同于

CoroutineScope(Dispatchers.IO).launch {
launch(SupervisorJob()) {

}
}

Deferred

是 Job 的子接口,是一个带有返回结果的 Job。async 函数创建的协程会返回一个 Deferred,可以通过 Deferred 的await()获取实际的返回值。async 与 await 类似于其他语言(例如 Javascript)中的 async 与 await,通常用来使两个协程并行执行。 例如如下代码

suspend fun testAsync1(): String = withContext(Dispatchers.Default)
{
delay(2000)
“123”
}

suspend fun testAsync2(): String = withContext(Dispatchers.Default)
{
delay(2000)
“456”
}

lifecycleScope.launch {
val time1 = Date()
val result1 = testAsync1()
val result2 = testAsync2()
Log.i(“zx”, “结果为 r e s u l t 1 + r e s u l t 2 " ) L o g . i ( " z x " , " 耗 时 {result1 + result2}") Log.i("zx", "耗时 result1+result2")Log.i("zx","耗时{Date().time - time1.time}”)
}

会输出:

结果为123456
耗时5034

如果改为使用 async,让两个协程并行。代码如下:

lifecycleScope.launch {
val time1 = Date()
val result1 = async { testAsync1() }
val result2 = async { testAsync2() }
Log.i(“zx”, “结果为 r e s u l t 1. a w a i t ( ) + r e s u l t 2. a w a i t ( ) " ) L o g . i ( " z x " , " 耗 时 {result1.await() + result2.await()}") Log.i("zx", "耗时 result1.await()+result2.await()")Log.i("zx","耗时{Date().time - time1.time}”)
}

输出

结果为123456
耗时3023

总耗时为两个并行协程中耗时较长的那个时间。

CoroutineDispatcher 调度器

指定了协程运行的线程或线程池,共有 4 种。

Dispatchers.Main 运行在主线程,Android 平台就是 UI 线程,是单线程的。Dispatchers.Default 默认的调度器,如果上下文中未指定调度器,那么就是 Default。适合用来执行消耗 CPU 资源的计算密集型任务。它由 JVM 上的共享线程池支持。 默认情况下,此调度器使用的最大并行线程数等于 CPU 内核数,但至少为两个。Dispatchers.IO IO 调度器,使用按需创建的线程共享池,适合用来执行 IO 密集型阻塞 *** 作,比如 http 请求。此调度器默认并行线程数为内核数和 64 这两个值中的较大者。Dispatchers.Unconfined 不限于任何特定线程的协程调度器,不常用。

需要注意的是 Default 和 IO 都是运行在线程池中,两个子协程有可能是在一个线程中,有可能不是一个线程中。例如如下代码:

CoroutineScope(Dispatchers.IO).launch {
launch {
delay(3000)
Log.i(“zx”, “当前线程1-” + Thread.currentThread().name)
}

launch {
Log.i(“zx”, “当前线程2-” + Thread.currentThread().name)
}
}

输出

当前线程2-DefaultDispatcher-worker-2
当前线程1-DefaultDispatcher-worker-5

所以,如果涉及线程的 ThreadLocal 数据时,记得做处理。

如果一不小心用错了 Dispatchers.Default 去发 IO 请求会有什么后果呢?猜测结果:由于 Default 调度器并行线程数远小于 IO 调度器,IO 请求的一个特性就是等待时间很长,而实际的处理时间很短,所以会造成大量请求处于等待分配线程的状态中,造成效率低下。实际情况可以写个程序测试一下,这里就不试了。

CoroutineName 协程名称

传入一个 String 作为协程名称,一般用于调试时日志输出,以区分不同的调度器。

CoroutineExceptionHandler 异常处理器

用于处理协程作用域内所有未捕获的异常。实现 CoroutineExceptionHandler 接口就好了,代码如下:

class MyExceptionHandler : CoroutineExceptionHandler {
override val key: CoroutineContext.Key<*>
get() = CoroutineExceptionHandler

override fun handleException(context: CoroutineContext, exception: Throwable) {
Log.i(“zx”, “ c o n t e x t [ C o r o u t i n e N a m e ] 中 发 生 异 常 , {context[CoroutineName]}中发生异常, context[CoroutineName]中发生异常,{exception.message}”)
}
}

然后用+拼接并设置给作用域。

CoroutineScope(Dispatchers.IO + CoroutineName(“父协程”) + MyExceptionHandler()).launch {
launch(CoroutineName(“子协程1”) + MyExceptionHandler()) {
throw Exception(“完蛋了,异常了”)
}
}

输出内容为

CoroutineName(父协程)中发生异常,完蛋了,异常了

不对呀,明明是子协程 1 抛出的异常,为什么输出的是父协程抛出的异常呢?原来,异常规则就是子协程会将异常一级一级向上抛,直到根协程。那什么是根协程呢?跟协程简单来讲就是最外层协程,还有一个特殊的规则就是,使用 SupervisorJob 创建的协程也视为根协程。比如如下代码:

CoroutineScope(Dispatchers.IO + CoroutineName(“父协程”) + MyExceptionHandler()).launch {
launch(CoroutineName(“子协程1”) + MyExceptionHandler() + SupervisorJob()) {
throw Exception(“完蛋了,异常了”)
}
}

输出内容为

CoroutineName(子协程1)中发生异常,完蛋了,异常了

说起处理异常,大家肯定想到 try / catch,为什么有了 try / catch,协程里还要有一个 CoroutineExceptionHandler 呢?或者说 CoroutineExceptionHandler 到底起什么作用,什么时候用 CoroutineExceptionHandler 什么时候用 try / catch 呢?官方文档是这么描述 CoroutineExceptionHandler 的用于处理未捕获的异常,是用于全局“全部捕获”行为的最后一种机制。 你无法从CoroutineExceptionHandler的异常中恢复。 当调用处理程序时,协程已经完成。,这段文字描述的很清楚了,这是全局(这个全局是指根协程作用域全局)的异常捕获,是最后的一道防线,此时协程已经结束,你只能处理异常,而不能做其他的 *** 作。举个例子吧

CoroutineScope(Dispatchers.IO + CoroutineName(“父协程”) + MyExceptionHandler()).launch {
val test = 5 / 0
Log.i(“zx”, “即使异常了,我也想继续执行协程代码,比如:我要通知用户,让用户刷新界面”)
}

协程体中第一行 5/0 会抛出异常,会在 CoroutineExceptionHandler 中进行处理,但是协程就会直接结束,后续的代码不会再执行,如果想继续执行协程,比如d出 Toast 通知用户,这里就做不到了。换成 try / catch 肯定就没有问题了。

CoroutineScope(Dispatchers.IO + CoroutineName(“父协程”) + MyExceptionHandler()).launch {
try {
val test = 5 / 0
} catch (e: Exception) {
Log.i(“zx”, “我异常了”)
}
Log.i(“zx”, “继续执行协程的其他代码”)
}

那既然如此,我直接把协程中所有代码都放在 try / catch 里,不用 CoroutineExceptionHandler 不就行了?听起来好像没毛病,那我们就试试吧

inline fun AppCompatActivity.myLaunch(
crossinline block: suspend CoroutineScope.() -> Unit
) {
CoroutineScope(Dispatchers.IO).launch {
try {
block()
} catch (e: Exception) {
Log.e(“zx”, “异常了,” + e.message)
}
}
}

做了一个封装,只要是调用封装的 myLaunch 函数,那所有的协程代码都被 try / catch 包着,这肯定没问题了吧。比如我这样调用

“我异常了”)
}
Log.i(“zx”, “继续执行协程的其他代码”)
}

那既然如此,我直接把协程中所有代码都放在 try / catch 里,不用 CoroutineExceptionHandler 不就行了?听起来好像没毛病,那我们就试试吧

inline fun AppCompatActivity.myLaunch(
crossinline block: suspend CoroutineScope.() -> Unit
) {
CoroutineScope(Dispatchers.IO).launch {
try {
block()
} catch (e: Exception) {
Log.e(“zx”, “异常了,” + e.message)
}
}
}

做了一个封装,只要是调用封装的 myLaunch 函数,那所有的协程代码都被 try / catch 包着,这肯定没问题了吧。比如我这样调用

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5710316.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存