Kotlinkotlin

Kotlin 之 协程

2022-03-13  本文已影响0人  zcwfeng

Job: 控制协程的生命周期

CoroutineDispatcher: 向合适的线程分发任务;
CoroutineName: 协程的名称,调试的时候很有用;
CoroutineExceptionHandler: 处理未被捕捉的异常。

Job生命周期.png

suspend

是一个为协程的方法标记,这里是异步操作,提醒开发者

MainScope 作用域 使用例子

/MainScope/

注意防止内存泄漏需要调用cancel方法

CoroutineScope.launch() 一般用在不需要返回结果的地方,如上所示

//1. 声明
    val mScope = MainScope()
//2. delay 本身就是suspend修饰的方法
    val job1 = mScope.launch {
            delay(1000)
        }
//3. withContext 同样是suspend修饰
val job2 = mScope.launch(Dispatchers.IO) {
            withContext(Dispatchers.Main) {
                Log.e("zcwfeng","update UI")
            }
        }

mScope.launch(Dispatchers.IO) {
            val userInfo = getUserInfo()
            Log.e("zcwfeng","userInfo $userInfo")
        }

private suspend fun getUserInfo() =
        withContext(Dispatchers.IO) {
            delay(2000)
            "Kotlin"
        }

//4. 结束要调用cancel防止内存泄漏
override fun onDestroy() {
        super.onDestroy()
        mScope.cancel()
    }

源码分析

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

这个是CoroutineScope 的一个扩展函数,3个参数含义:

  1. CoroutineContext 协程上下文
  2. 协程的启动模式
  3. 协程执行具体逻辑的lambda
  4. 返回一个Job

/CoroutineContext/

是一个有索引的Element集合。数据机构介于set和map中间

CoroutineContext 核心

  1. get 方法可以在数据结构Element
  2. 操作符plus 重载 (返回一个新的 context 对象,新的对象里面包含了两个里面的所有 Element,如果遇到重复的(Key 一样的),那么用+号右边的 Element 替代左边的。但是这里面注意 + 是有顺序)
  3. public operator fun plus,提供遍历当前 context 中所有 Element 的能力
  4. fun minusKey

CoroutineScope.async() 一般用于需要返回结果和并发操作

返回结果的例子

fun asyncTest(){
        mScope.launch {
            val deferred = async(Dispatchers.IO) {
                delay(2000)
                "asyncTest"
            }
            Log.e("zcwfeng",deferred.await())
        }
    }

并发例子,同时请求5个接口 并且将返回值拼接起来

等待各job执行完 将结果合并,模拟时间都是5000毫秒 所以当job1执行完时 其他job也均执行完成

 private fun asyncTest2() {
        mScope.launch {
            val job1 = async {
                // 请求1
                delay(5000)
                "1"
            }
            val job2 = async {
                // 请求2
                delay(5000)
                "2"
            }
            val job3 = async {
                // 请求3
                delay(5000)
                "3"
            }
            val job4 = async {
                // 请求4
                delay(5000)
                "4"
            }
            val job5 = async {
                // 请求5
                delay(5000)
                "5"
            }

          
            Log.e(
                "zcwfeng",
                "asyncTest2: ${job1.await()} ${job2.await()} ${job3.await()} ${job4.await()} ${job5.await()}"
            )
        }

源码解析

public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T> {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyDeferredCoroutine(newContext, block) else
        DeferredCoroutine<T>(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

参数同launch,所以我们看下差异的代码 EmptyCoroutinContext


/EmptyCoroutineContext/

某些情况需要一个上下文不持有任何元素,此时就可以使用 EmptyCoroutineContext 对象。
添加这个对象到另一个上下文不会对其有任何影响。



/Job/ 控制协程的声明周期

  wait children
    +-----+ start  +--------+ complete   +-------------+  finish  +-----------+
    | New | -----> | Active | ---------> | Completing  | -------> | Completed |
    +-----+        +--------+            +-------------+          +-----------+
                     |  cancel / fail       |
                     |     +----------------+
                     |     |
                     V     V
                 +------------+                           finish  +-----------+
                 | Cancelling | --------------------------------> | Cancelled |
                 +------------+                                   +-----------+

看一个例子打印

private fun baseJobTest() {
        val coroutineContext = Job() + Dispatchers.Default + CoroutineName("myContext")
        Log.e(TAG,"$coroutineContext,${coroutineContext[CoroutineName]}")
        val newCoroutineContext = coroutineContext.minusKey(CoroutineName)
        Log.e(TAG,"$newCoroutineContext")
    }

或者直接用单元测试

@Test
    fun jobTest(){
        val coroutineContext = Job() + Dispatchers.Default + CoroutineName("myContext")
        println("$coroutineContext,${coroutineContext[CoroutineName]}")
        val newCoroutineContext = coroutineContext.minusKey(CoroutineName)
        println("$newCoroutineContext")
    }

-> 结果:
2022-03-11 11:16:08.570 29740-29761/top.zcwfeng.kotlin I/System.out:
[JobImpl{Active}@fdc8ee0, CoroutineName(myContext), Dispatchers.Default],CoroutineName(myContext)
2022-03-11 11:16:08.570 29740-29761/top.zcwfeng.kotlin I/System.out:
[JobImpl{Active}@fdc8ee0, Dispatchers.Default]

在任务层级中,每个协程都会有一个父级对象,要么是 CoroutineScope 或者另外一个 coroutine。
协程的父级 CoroutineContext 和父级协程的 CoroutineContext 是不一样的,因为有如下的公式:
【父级上下文 = 默认值 + 继承的 CoroutineContext + 参数】

Dispatchers 的种类:【Default,IO,Main,Unconfined】

  1. 一些元素包含默认值: Dispatchers.Default 是默认的 CoroutineDispatcher,以及 "coroutine" 作为默认的 CoroutineName;
  2. 继承的 CoroutineContext 是 CoroutineScope 或者其父协程的 CoroutineContext;
  3. 传入协程 builder 的参数的优先级高于继承的上下文参数,因此会覆盖对应的参数值。

看一个例子:

CoroutineContext 可以使用 " + " 运算符进行合并。由于 CoroutineContext 是由一组元素组成的,所以加号右侧的元素会覆盖加号左侧的元素,进而组成新创建的 CoroutineContext。

比如,(Dispatchers.Main, "name") + (Dispatchers.IO) = (Dispatchers.IO, "name")。

Job 用于处理协程。对于每一个所创建的协程 (通过 launch 或者 async),它会返回一个 Job实例,该实例是协程的唯一标识,并且负责管理协程的生命周期

1. CoroutineScope.launch 函数返回的是一个 Job 对象,代表一个异步的任务。Job 具有生命周期并且可以取消。 Job 还可以有层级关系,一个Job可以包含多个子Job,当父Job被取消后,所有的子Job也会被自动取消;当子Job被取消或者出现异常后父Job也会被取消。

2. 除了通过 CoroutineScope.launch 来创建Job对象之外,还可以通过 Job() 工厂方法来创建该对象。默认情况下,子Job的失败将会导致父Job被取消,这种默认的行为可以通过 SupervisorJob 来修改。

3. 具有多个子 Job 的父Job 会等待所有子Job完成(或者取消)后,自己才会执行完成

Job状态:新创建 (New)、活跃 (Active)、完成中 (Completing)、已完成 (Completed)、取消中 (Cancelling) 和已取消 (Cancelled)。
虽然我们无法直接访问这些状态,但是我们可以访问 Job 的属性: isActive、isCancelled 和 isCompleted。

Job 的常用函数: 这些函数都是线程安全的,所以可以直接在其他 Coroutine 中调用。

如果 Job 是正常执行完成的,则 cause 参数为 null
如果 Job 是正常取消的,则 cause 参数为 CancellationException 对象。这种情况不应该当做错误处理,这是任务正常取消的情形。所以一般不需要在错误日志中记录这种情况。
其他情况表示 Job 执行失败了。

Deferred

通过使用async创建协程可以得到一个有返回值Deferred,Deferred 接口继承自 Job 接口,额外提供了获取 Coroutine 返回结果的方法。由于 Deferred 继承自 Job 接口,所以 Job 相关的内容在 Deferred 上也是适用的。 Deferred 提供了额外三个函数来处理和Coroutine执行结果相关的操作。

源码解析

public interface Deferred<out T> : Job {

   
    public suspend fun await(): T

    
    public val onAwait: SelectClause1<T>

   
    @ExperimentalCoroutinesApi
    public fun getCompleted(): T

    
    @ExperimentalCoroutinesApi
    public fun getCompletionExceptionOrNull(): Throwable?
}


SupervisorJob

Job 是有父子关系的,如果子Job 失败了父Job会自动失败,这种默认的行为可能不是我们期望的。

SupervisorJob是一个顶层函数

@Suppress("FunctionName")
public fun SupervisorJob(parent: Job? = null) : CompletableJob = SupervisorJobImpl(parent)

MainScope 就是 SupervisorScope 和 Main Dispatcher 实现

@Suppress("FunctionName")
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)

Retrofit 请求的一个完整的例子

data class Repo(
    val archive_url: String,
    val archived: Boolean,
    val assignees_url: String,
    val blobs_url: String,
    val branches_url: String,
    val clone_url: String,
    val collaborators_url: String,
    val comments_url: String,
    val commits_url: String,
    val compare_url: String,
    val contents_url: String,
    val contributors_url: String,
    val created_at: String,
    val default_branch: String,
    val deployments_url: String,
    val description: String,
    val disabled: Boolean,
    val downloads_url: String,
    val events_url: String,
    val fork: Boolean,
    val forks: Int,
    val forks_count: Int,
    val forks_url: String,
    val full_name: String,
    val git_commits_url: String,
    val git_refs_url: String,
    val git_tags_url: String,
    val git_url: String,
    val has_downloads: Boolean,
    val has_issues: Boolean,
    val has_pages: Boolean,
    val has_projects: Boolean,
    val has_wiki: Boolean,
    val homepage: String,
    val hooks_url: String,
    val html_url: String,
    val id: Int,
    val issue_comment_url: String,
    val issue_events_url: String,
    val issues_url: String,
    val keys_url: String,
    val labels_url: String,
    val language: String,
    val languages_url: String,
    val license: License,
    val merges_url: String,
    val milestones_url: String,
    val mirror_url: Any,
    val name: String,
    val node_id: String,
    val notifications_url: String,
    val open_issues: Int,
    val open_issues_count: Int,
    val owner: Owner,
    val `private`: Boolean,
    val pulls_url: String,
    val pushed_at: String,
    val releases_url: String,
    val size: Int,
    val ssh_url: String,
    val stargazers_count: Int,
    val stargazers_url: String,
    val statuses_url: String,
    val subscribers_url: String,
    val subscription_url: String,
    val svn_url: String,
    val tags_url: String,
    val teams_url: String,
    val trees_url: String,
    val updated_at: String,
    val url: String,
    val watchers: Int,
    val watchers_count: Int
)



interface Api {
    @GET("users/{user}/repos")
    fun listRepos(@Path("user") user: String): Call<List<Repo>>

    @GET("users/{user}/repos")
    suspend fun listReposKt(@Path("user") user: String): List<Repo>

    @GET("users/{user}/repos")
    fun listReposRx(@Path("user") user: String): Single<List<Repo>>
}

val retrofit = Retrofit.Builder()
            .baseUrl("https://api.github.com")
            .addConverterFactory(GsonConverterFactory.create())
            .build();
 val api = retrofit.create(top.zcwfeng.kotlin.coroutine.Api::class.java)

val scopeme:CoroutineScope = MainScope()//主线程 第一种
scopeme.launch {
            val repos = async { api.listReposKt("zcwfeng") }
            val repos2 = async { api.listReposKt("google") }
            test_tv.text = "${repos.await()[0]?.name}-${repos2.await()[0]?.name}"
        }
scopeme.cancel()

/CoroutineDispatcher/ 任务调度器

Default dispatcher 使用一个共享的后台线程池来运行里面的任务。注意它和IO共享线程池,只不过限制了最大并发数不同。

是和Default共用一个共享的线程池来执行里面的任务。根据同时运行的任务数量,在需要的时候会创建额外的线程,当任务执行完毕后会释放不需要的线程。

CoroutineStart 协程启动模式

协程创建后立即开始调度,在调度前如果协程被取消,其将直接进入取消响应的状态
虽然是立即调度,但也有可能在执行前被取消

协程创建后立即开始调度,协程执行到第一个挂起点之前不响应取消
虽然是立即调度,但其将调度和执行两个步骤合二为一了,就像它的名字一样,其保证调度和执行是原子操作,因此协程也一定会执行

只要协程被需要时,包括主动调用该协程的start、join或者await等函数时才会开始调度,如果调度前就被取消,协程将直接进入异常结束状态

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

这些启动模式的设计主要是为了应对某些特殊的场景。业务开发实践中通常使用DEFAULT和LAZY这两个启动模式就够了

CoroutineScope - 协程作用域

定义协程必须指定其 CoroutineScope 。
CoroutineScope 可以对协程进行追踪,即使协程被挂起也是如此。同调度程序 (Dispatcher) 不同,CoroutineScope 并不运行协程,它只是确保您不会失去对协程的追踪。
为了确保所有的协程都会被追踪,Kotlin 不允许在没有使用 CoroutineScope 的情况下启动新的协程。
CoroutineScope 可被看作是一个具有超能力的 ExecutorService 的轻量级版本。CoroutineScope 会跟踪所有协程,同样它还可以取消由它所启动的所有协程。
这在 Android 开发中非常有用,比如它能够在用户离开界面时停止执行协程
当异步操作比较耗时的时候,或者当异步操作出现错误的时候,需要把这个 Coroutine 取消掉来释放系统资源。
在 Android 环境中,通常每个界面(Activity、Fragment 等)启动的 Coroutine 只在该界面有意义,如果用户在等待 Coroutine 执行的时候退出了这个界面,则再继续执行这个 Coroutine 可能是没必要的。(另外 Coroutine 也需要在适当的 context 中执行,否则会出现错误,比如在非 UI 线程去访问 View)
所以 Coroutine 在设计的时候,要求在一个范围(Scope)内执行,这样当这个 Scope 取消的时候,里面所有的子 Coroutine 也自动取消。所以要使用 Coroutine 必须要先创建一个对应的 CoroutineScope。

CoroutineScope 接口

public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

协程作用域 (总结)

主要用以明确写成之间的父子关系,以及对于取消或者异常处理等方面的传播行为

此时子协程抛出的未捕获异常,都将传递给父协程处理,父协程同时也会被取消

除了三种作用域中提到的行为以外,父子协程之间还存在以下规则:

  1. 父协程被取消,则所有子协程均被取消。由于协同作用域和主从作用域中都存在父子协程关系,因此此条规则都适用。
  2. 父协程需要等待子协程执行完毕之后才会最终进入完成状态,不管父协程自身的协程体是否已经执行完。
  3. 子协程会继承父协程的协程上下文中的元素,如果自身有相同key的成员,则覆盖对应的key,覆盖的效果仅限自身范围内有效。
  1. GlobalScope - 不推荐使用
public object GlobalScope : CoroutineScope {
    /**
     * Returns [EmptyCoroutineContext].
     */
    override val coroutineContext: CoroutineContext
        get() = EmptyCoroutineContext
}

GlobalScope是一个单例实现,源码十分简单,上下文是EmptyCoroutineContext,是一个空的上下文,切不包含任何Job,该作用域常被拿来做示例代码,由于 GlobalScope 对象没有和应用生命周期组件相关联,需要自己管理 GlobalScope 所创建的 Coroutine,且GlobalScope的生命周期是 进程 级别的,所以一般而言我们不推荐使用 GlobalScope 来创建 Coroutine。

  1. runBlocking{} - 主要用于测试
@Throws(InterruptedException::class)
public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    val currentThread = Thread.currentThread()
    val contextInterceptor = context[ContinuationInterceptor]
    val eventLoop: EventLoop?
    val newContext: CoroutineContext
    if (contextInterceptor == null) {
        // create or use private event loop if no dispatcher is specified
        eventLoop = ThreadLocalEventLoop.eventLoop
        newContext = GlobalScope.newCoroutineContext(context + eventLoop)
    } else {
        // See if context's interceptor is an event loop that we shall use (to support TestContext)
        // or take an existing thread-local event loop if present to avoid blocking it (but don't create one)
        eventLoop = (contextInterceptor as? EventLoop)?.takeIf { it.shouldBeProcessedFromContext() }
            ?: ThreadLocalEventLoop.currentOrNull()
        newContext = GlobalScope.newCoroutineContext(context)
    }
    val coroutine = BlockingCoroutine<T>(newContext, currentThread, eventLoop)
    coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
    return coroutine.joinBlocking()
}

运行一个新的协程并且阻塞当前可中断的线程直至协程执行完成,该函数不应从一个协程中使用,该函数被设计用于桥接普通阻塞代码到以挂起风格(suspending style)编写的库,以用于主函数与测试。该函数主要用于测试,不适用于日常开发,该协程会阻塞当前线程直到协程体执行完成。

  1. MainScope() - 可用于开发

该函数是一个顶层函数,用于返回一个上下文是SupervisorJob() + Dispatchers.Main的作用域,该作用域常被使用在Activity/Fragment,并且在界面销毁时要调用fun CoroutineScope.cancel(cause: CancellationException? = null)对协程进行取消,这是官方库中可以在开发中使用的一个用于获取作用域的顶层函数,使用示例在官方库的代码注释中已经给出,上面的源码中也有,使用起来也是十分的方便。

  1. LifecycleOwner.lifecycleScope - 推荐使用
public val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
    get() = lifecycle.coroutineScope

/**
 * [CoroutineScope] tied to this [Lifecycle].
 *
 * This scope will be cancelled when the [Lifecycle] is destroyed.
 *
 * This scope is bound to
 * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate]
 */
public val Lifecycle.coroutineScope: LifecycleCoroutineScope
    get() {
        while (true) {
            val existing = mInternalScopeRef.get() as LifecycleCoroutineScopeImpl?
            if (existing != null) {
                return existing
            }
            val newScope = LifecycleCoroutineScopeImpl(
                this,
                SupervisorJob() + Dispatchers.Main.immediate
            )
            if (mInternalScopeRef.compareAndSet(null, newScope)) {
                newScope.register()
                return newScope
            }
        }
    }

该扩展属性是 Android 的Lifecycle Ktx库提供的具有生命周期感知的协程作用域,它与LifecycleOwner的Lifecycle绑定,Lifecycle被销毁时,此作用域将被取消。这是在Activity/Fragment中推荐使用的作用域,因为它会与当前的UI组件绑定生命周期,界面销毁时该协程作用域将被取消,不会造成协程泄漏,相同作用的还有下文提到的ViewModel.viewModelScope。

  1. ViewModel.viewModelScope - 推荐使用
/**
 * [CoroutineScope] tied to this [ViewModel].
 * This scope will be canceled when ViewModel will be cleared, i.e [ViewModel.onCleared] is called
 *
 * This scope is bound to
 * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate]
 */
public val ViewModel.viewModelScope: CoroutineScope
    get() {
        val scope: CoroutineScope? = this.getTag(JOB_KEY)
        if (scope != null) {
            return scope
        }
        return setTagIfAbsent(
            JOB_KEY,
            CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
        )
    }

该扩展属性和上文中提到的LifecycleOwner.lifecycleScope基本一致,它是ViewModel的扩展属性,也是来自Android 的Lifecycle Ktx库,它能够在此ViewModel销毁时自动取消,同样不会造成协程泄漏。该扩展属性返回的作用域的上下文同样是SupervisorJob() + Dispatchers.Main.immediate

  1. coroutineScope <VS> supervisorScope

supervisorScope


public suspend fun <R> supervisorScope(block: suspend CoroutineScope.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return suspendCoroutineUninterceptedOrReturn { uCont ->
        val coroutine = SupervisorCoroutine(uCont.context, uCont)
        coroutine.startUndispatchedOrReturn(coroutine, block)
    }
}

coroutineScope

public suspend fun <R> coroutineScope(block: suspend CoroutineScope.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return suspendCoroutineUninterceptedOrReturn { uCont ->
        val coroutine = ScopeCoroutine(uCont.context, uCont)
        coroutine.startUndispatchedOrReturn(coroutine, block)
    }
}

首先这两个函数都是挂起函数,需要运行在协程内或挂起函数内。

supervisorScope属于主从作用域,会继承父协程的上下文,它的特点就是子协程的异常不会影响父协程,它的设计应用场景多用于子协程为独立对等的任务实体的时候,比如一个下载器,每一个子协程都是一个下载任务,当一个下载任务异常时,它不应该影响其他的下载任务。

coroutineScope和supervisorScope都会返回一个作用域,它俩的差别就是异常传播,coroutineScope 内部的异常会向上传播,子协程未捕获的异常会向上传递给父协程,任何一个子协程异常退出,会导致整体的退出;
supervisorScope 内部的异常不会向上传播,一个子协程异常退出,不会影响父协程和兄弟协程的运行。

协程的取消和异常

使用SupervisorJob*

    @Test
    fun cancelCourtinTest(){
        mScope.launch(Dispatchers.Default) {
            delay(500)
            Log.e(CoroutinStudyActivity.TAG, "Child 1")
        }

        mScope.launch(Dispatchers.Default) {
            delay(1000)
            Log.e(CoroutinStudyActivity.TAG, "Child 2")
            throw RuntimeException("--> RuntimeException <--")
        }

        mScope.launch(Dispatchers.Default) {
            delay(1500)
            Log.e(CoroutinStudyActivity.TAG, "Child 3")
        }
    }


--------
2022-03-13 17:03:26.671 10779-10802/? E/zcwfeng: Child 1
2022-03-13 17:03:27.169 10779-10802/? E/zcwfeng: Child 2
    
    --------- beginning of crash
2022-03-13 17:03:27.208 10779-10802/? E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-1
    Process: top.zcwfeng.kotlin, PID: 10779
    java.lang.RuntimeException: --> RuntimeException <--
        at coroutines.Demo$cancelCourtinTest$2.invokeSuspend(KotlinCoroutines.kt:25)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
        at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)

MainScope()我们之前提到过了,它的实现就是用了SupervisorJob。执行结果就是Child 2抛出异常后,Child 3正常执行了,但是程序崩了,因为我们没有处理这个异常

// 在Child 2的上下文添加了异常处理
        mScope.launch(Dispatchers.Default + CoroutineExceptionHandler { coroutineContext, throwable ->
            Log.e(CoroutinStudyActivity.TAG, "CoroutineExceptionHandler: $throwable")
        }) {
            delay(1000)
            Log.e(CoroutinStudyActivity.TAG, "Child 2")
            throw RuntimeException("--> RuntimeException <--")
        }

-----------
2022-03-13 17:12:55.591 12389-12445/top.zcwfeng.kotlin E/zcwfeng: Child 1
2022-03-13 17:12:56.088 12389-12445/top.zcwfeng.kotlin E/zcwfeng: Child 2
2022-03-13 17:12:56.091 12389-12445/top.zcwfeng.kotlin E/zcwfeng: CoroutineExceptionHandler: java.lang.RuntimeException: --> RuntimeException <--
2022-03-13 17:12:56.589 12389-12445/top.zcwfeng.kotlin E/zcwfeng: Child 3

异常处理的打印也输出了,这就达到了我们想要的效果。但是要注意一个事情,这几个子协程的父级是SupervisorJob,但是他们再有子协程的话,他们的子协程的父级就不是SupervisorJob了,所以当它们产生异常时,就不是我们演示的效果了。

使用supervisorScope

@Test
    fun useSupervisorScopeTest(){
        val scope = CoroutineScope(Job() + Dispatchers.Default)
        scope.launch(CoroutineExceptionHandler { coroutineContext, throwable ->
            Log.e(
                CoroutinStudyActivity.TAG,
                "CoroutineExceptionHandler: $throwable"
            )
        }) {
            supervisorScope {
                launch {
                    delay(500)
                    Log.e(CoroutinStudyActivity.TAG, "Child 1 ")
                }
                launch {
                    delay(1000)
                    Log.e(CoroutinStudyActivity.TAG, "Child 2 ")
                    throw  RuntimeException("--> RuntimeException <--")
                }
                launch {
                    delay(1500)
                    Log.e(CoroutinStudyActivity.TAG, "Child 3 ")
                }
            }
        }
    }
-----------
2022-03-13 17:20:00.798 13771-13807/top.zcwfeng.kotlin E/zcwfeng: Child 1 
2022-03-13 17:20:01.296 13771-13807/top.zcwfeng.kotlin E/zcwfeng: Child 2 
2022-03-13 17:20:01.299 13771-13807/top.zcwfeng.kotlin E/zcwfeng: CoroutineExceptionHandler: java.lang.RuntimeException: --> RuntimeException <--
2022-03-13 17:20:01.797 13771-13807/top.zcwfeng.kotlin E/zcwfeng: Child 3 

把supervisorScope 换成coroutineScope 结果就不一样了

2022-03-13 17:22:50.723 14467-14495/top.zcwfeng.kotlin E/zcwfeng: Child 1 
2022-03-13 17:22:51.220 14467-14495/top.zcwfeng.kotlin E/zcwfeng: Child 2 
2022-03-13 17:22:51.227 14467-14495/top.zcwfeng.kotlin E/zcwfeng: CoroutineExceptionHandler: java.lang.RuntimeException: --> RuntimeException <--

处理协程异常

SupervisorJob 只有作为 supervisorScope 或 CoroutineScope(SupervisorJob()) 的一部分时,才会按照上面的描述工作。

private fun baseCoroutinExecptionHandler1() {

        // Scope 控制我的应用中某一层级的协程
//        val scope = CoroutineScope(SupervisorJob())
        val scope = CoroutineScope(Job())
        scope.launch(SupervisorJob()) {
            supervisorScope {
                launch(CoroutineExceptionHandler { _, _ ->
                    Log.e(TAG, "Exception Handler supervisorScope...... Exception...")
                }) {
                    Log.e(TAG, "Exception Handler supervisorScope......child1")
                    throw RuntimeException("Test Exception")
                }

                launch {
                    Log.e(TAG, "Exception Handler supervisorScope......child2")
                }
            }
        }

不加入这个代码块supervisorScope{}和加入supervisorScope代码块是不一样的

 supervisorScope {
  ......
}

看如下代码:

给您下面一段代码,您能指出 Child 1 是用哪种 Job 作为父级的吗?
val scope = CoroutineScope(Job())
scope.launch(SupervisorJob()) {
    // new coroutine -> can suspend
   launch {
        // Child 1
    }
    launch {
        // Child 2
    }
}
job.png

Child 1 的父级 Job 就只是 Job 类型!

Async

private fun baseCoroutinExecptionHandler3() {
        val scope = CoroutineScope(Job())
        scope.launch {
            supervisorScope {
                val deffered = async{
                    throw RuntimeException("Test Exception")
                }
                try {
                    Log.e(TAG,"execption accor before")
                    deffered.await()// 只有await调用才会抛异常
                    Log.e(TAG,"execption accor after")
                } catch (e: Exception) {
                    Log.e(TAG,"async catch execption ${e.message}")
                }
            }
        }
    }

但是

private fun baseCoroutinExceptionHandler4() {

        val scope = CoroutineScope(Job())
        scope.launch {
            val deffered = async {
                // 如果 async 抛出异常,launch 就会立即抛出异常,而不会调用 .await()
                throw RuntimeException("Test Exception")
            }
            try {
                Log.e(TAG,"execption accor before")
                Log.e(TAG,"execption accor after")
            } catch (e: Exception) {
                Log.e(TAG,"async catch execption ${e.message}")
            }
        }
    }

由于 scope 的直接子协程是 launch,如果 async 中产生了一个异常,这个异常将就会被立即抛出。原因是 async (包含一个 Job 在它的 CoroutineContext 中) 会自动传播异常到它的父级 (launch),这会让异常被立即抛出。
结果,打印同时,Android 程序崩溃

2022-03-14 17:43:57.890 26212-27933/top.zcwfeng.kotlin E/zcwfeng: execption accor before
2022-03-14 17:43:57.890 26212-27933/top.zcwfeng.kotlin E/zcwfeng: execption accor after
2022-03-14 17:43:57.903 26212-27933/top.zcwfeng.kotlin E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-1
    Process: top.zcwfeng.kotlin, PID: 26212
    java.lang.RuntimeException: Test Exception
        at top.zcwfeng.CoroutinStudyActivity$baseCoroutinExceptionHandler4$1$deffered$1.invokeSuspend(CoroutinStudyActivity.kt:47)
。。。。。。

CoroutineExceptionHandler 的规则

/**
     * 以下的条件被满足时,异常就会被捕获:
     * 1. 异常是被自动抛出异常的协程所抛出的 (使用 launch,而不是 async 时)
     * 2. 在 CoroutineScope 的 CoroutineContext 中或在一个根协程
     *              (CoroutineScope 或者 supervisorScope 的直接子协程) 中
     */
    val handlerException = CoroutineExceptionHandler {
            context, exception -> println("Caught $exception")
    }

做个测试,if(boolean) 代码测试

private fun baseCoroutinExceptionHandler5() {
        val scope = CoroutineScope(Job())

        if(false) {
// <1> 在下面的代码中,异常会被 handler 捕获 
            scope.launch(handlerException) {
                launch {
                    throw Exception("Failed coroutine")
                }
            }
        } else {
// <2> 导致崩溃,异常没有捕获
            scope.launch {
                launch(handlerException) {
                    throw Exception("Failed coroutine")
                }
            }
        }

<2> 处
异常不会被捕获的原因是因为 handler 没有被安装给正确的 CoroutineContext。内部协程会在异常出现时传播异常并传递给它的父级,由于父级并不知道 handler 的存在,异常就没有被抛出

想要避免取消操作在异常发生时被传播,记得使用 SupervisorJob;反之则使用 Job。 没有被捕获的异常会被传播,捕获它们以保证良好的用户体验!
查看Google获得更多的细节。

上一篇下一篇

猜你喜欢

热点阅读