创建协程有三种方式:launch、async、runBlocking
launchlaunch 方法签名如下:
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 方法取消它。
asyncasync 方法签名如下:
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 方法获取。
runBlockingrunBlocking 会阻塞调用他的线程,直到代码块执行完毕。
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。
JobJob 可以简单理解为一个协程的引用,创建协程后会返回 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()) {
}
}
是 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 数据时,记得做处理。
CoroutineName 协程名称如果一不小心用错了 Dispatchers.Default 去发 IO 请求会有什么后果呢?猜测结果:由于 Default 调度器并行线程数远小于 IO 调度器,IO 请求的一个特性就是等待时间很长,而实际的处理时间很短,所以会造成大量请求处于等待分配线程的状态中,造成效率低下。实际情况可以写个程序测试一下,这里就不试了。
传入一个 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 包着,这肯定没问题了吧。比如我这样调用
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)