IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> kotlin 协程异常处理机制颠覆三观 -> 正文阅读

[移动开发]kotlin 协程异常处理机制颠覆三观

转载请标明出处:http://blog.csdn.net/zhaoyanjun6/article/details/121938761
本文出自【赵彦军的博客】


协程的异常处理与 OKhttpRxJava这些框架的处理方式都不太一样,因为异步代码的异常处理,往往是比较麻烦的,而到了同步化处理的协程框架下,异常就变得比较容易进行管理了。

要完全理解协程的异常,我们需要先理解协程的树形结构和结构化并发,在这基础上,就能很容易的理解协程是如果管理异常的了。

协程树与结构化并发

在协程作用域中,可以创建一个协程,同时,一个协程中还可以继续创建协程,所以这就形成了一个树形结构。借助这样的树形结构,协程可以很容易的控制结构化并发,父协程可以控制子协程的生命周期,而子协程可以从父协程继承协程上下文。

在代码中,可以通过coroutineScope {}来显示的创建一个协程作用域,它和测试时常用的runBlocking {}一样,都是协程的作用域构建器。

协程作用域的cancel

借助协程作用域的管理,我们可以轻松的控制该协程作用域下的所有协程,一旦取消一个协程作用域,那么这个协程作用域下的所有协程都将被取消。

val job1 = scope.launch {...} 
val job2 = scope.launch {...} 

scope.cancel()

如上所示,调用scopecancel之后,job1job2都将被取消。

而如果只想取消某个单独的协程,那么可以通过该协程的句柄Job对象来取消。

val job1 = scope.launch {}
val job2 = scope.launch {}

job1.cancel()

如上所示,这样就只取消了Job1的协程,而Job2不受影响。

这就是协程结构化并发的两个特点:

  • 取消一个协程作用域,将取消该协程作用域下的所有子协程
  • 被取消的子协程,不会影响其它同级的协程

在Android开发中,大部分场景下我们不需要考虑协程的cancel,借助ViewModelScopeLifecycleScopeMainScope这些场景的协程作用域,我们可以很方便的避免内存泄漏,在cancel时结束所有的子协程。

协程的cancel状态

协程的 cancel 与线程的 cancel 类似,协程一旦开始执行(代码占用CPU),只有执行完毕才会被 cancel,当协程调用 cancel,只是将协程的Job 生命周期设置为了 Canceling,直到协程执行完毕才会被置为 Canceled 。

如果一定要及时取消掉协程的执行,那么可以和线程做类似的操作,在协程代码内及时判断协程的状态来控制代码的执行。

所以,协程推荐开发者在使用协程时,以协作的方式来使用,即随时判断当前协程的生命周期,避免浪费计算资源。

协程提供了两种方式来进行协作式的 cancel

  • Job.isActive或者ensureActive()
  • yield

ensureActive()Job.isActive的封装实现,借助这个方法,就是在协程内代码执行前,对当前协程的状态进行一次判断。

清理

通常, 当协程被取消时, 需要做一些清理工作, 此时, 可以把协程中运行的代码用try {} fininaly {}块包住, 这样当协程被取消时, 会执行 fininaly 块中的清理工作。但是fininaly块中不能直接调用挂起函数,否则会抛出CancellationException异常,因为它已经被取消了,而你又要在fininaly块中执行挂起函数把它挂起,显然与要求矛盾。然而,如果非要这么做,也不是不可以,当你需要挂起一个被取消的协程,你可以将相应的代码包装在withContext(NonCancellable) {}中,并使用withContext函数以及NonCancellable上下文,代码如下所示。

fun main() = runBlocking {
    val job = launch {
        try {
            repeat(1000) { i ->
                println("job: I'm sleeping $i ...")
                delay(500L)
            }
        } finally {
            withContext(NonCancellable) {  // 重点注意这里
                println("job: I'm running finally")
                delay(1000L) // 这里调用了挂起函数!
                println("job: And I've just delayed for 1 sec because I'm non-cancellable")
            }
        }
    }
    delay(1300L) // 延迟一段时间
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // 取消该作业并等待它结束
    println("main: Now I can quit.")
}
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
job: I'm running finally
job: And I've just delayed for 1 sec because I'm non-cancellable
main: Now I can quit.

协程的返回值

协程获取返回值有两种方式:

  • launch返回的Job实例可以调用Join方法(Join函数会挂起协程直到协程执行完成)
  • async返回的Deferred实例(Job 的子类)可以调用await方法

如果在调用Join后再调用cancel,那么协程将在执行完成后被Cancel,如果先cancel再调用Join,那么协程也将执行完成

协程异常的处理

当协程作用域中的一个协程发生异常时,此时的异常流程如下所示:

  • 发生异常的协程被cancel
  • 异常传递到它的父协程
  • 父协程 cancel(取消其所有子协程)
  • 将异常在协程树上进一步向上传播

这种行为实际上是符合协程结构化并发的规则的,但是在实际使用中,这种结构化的异常处理,会让异常的处理有些暴力,大部分场景下,业务需求都是希望异常不影响正常的业务流程。

结构化并发的异常处理

所以,协程提出了 SupervisorJob 的新概念,它是Job的子类。

SupervisorJob 的作用就是将协程中的异常「掐死」在协程内部,切断其向上传播的路径。使用 SupervisorJob 后,子协程的异常退出不会影响到其他子协程,同时 SupervisorJob 也不会传播异常而是让异常发生的协程自己处理。

SupervisorJob 可以在创建 CoroutineScope 的时候作为参数传进来,也可以使用 supervisorScope 来创建一个自定义的协程作用域,所以SupervisorJob 只有下面两种使用方式。

  • supervisorScope{}
  • CoroutineScope(SupervisorJob())

但是要注意的是,不论是SupervisorJob还是Job,如果协程内部发生异常,这个异常是肯定会被抛出的,只是是否会崩溃。

这里有个误区,那就是大家不要以为使用SupervisorJob之后,协程就不会崩溃,不管你用什么Job,该崩溃的还是要崩溃的,它们的差别在于是否会影响到别的协程,例如下面这个例子。

val coroutineScope = CoroutineScope(Job())
coroutineScope.launch {
    throw Exception("test")
}
coroutineScope.launch {
    Log.d("xys", "test")
}

使用Job的时候,第二个协程是无法执行的,但你改为SupervisorJob()之后,第二个协程就可以执行了,因为第一个协程的崩溃,并没有影响到第二个协程的执行。

所以说,SupervisorJob的目的是为了在结构化并发中找到一个特殊处理的方式,并没有将异常隐藏起来。

SupervisorJob最多的使用场景就是多协程的并发处理,让某个协程的异常不干扰其它正常的协程。而CoroutineScope也很有用,因为你可以在一个协程发生异常时,取消其关联的所有协程,做为统一的处理。
从异常流动方向上来看,coroutineScope是双向的,而supervisorScope则是单向的。

平时常见的MainScope,就是使用的SupervisorJob,所以MainScope中的子协程之间互相不会影响。

协程的异常处理

前面我们说了,协程中的异常是一定会抛出的,所以在一个协程内部,我们到底怎么处理异常呢?

launch:通过launch启动的异常可以通过try catch来进行异常捕获,或者使用协程封装的拓展函数runCatching来捕获,其内部也是使用的try catch。

async:async的异常处理比较麻烦,我们下面详细的说下。

首先,当async被用作构建根协程(由协程作用域直接管理的协程)时,异常不会主动抛出,而是在调用.await()时抛出。

来看下这个例子:

MainScope().launch {
    supervisorScope {
        val deferred = async {
            throw Exception("test")
        }
        try {
            deferred.await()
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
}

执行这个例子后,异常将被捕获,从上面的代码可以看出,异常只会发生在执行await的时候,调用async是不会发生异常的,不过,细心的朋友可能发现了,这里使用的是supervisorScope,如果我们改成coroutineScope呢?

执行代码后我们会发现,异常并没有被捕获,这就是我们前面说到的SupervisorJob和Job的区别。

再看一个例子:

ainScope().launch {
    try {
        async {
            throw Exception("test")
        }
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

我们去掉了supervisorScope,所以async的父协程是Job,所以这个时候,即使是调用async,也会发生异常,同时也不会被捕获。

综上,async的异常,只能在supervisorScope中,使用try catch进行捕获。

CoroutineExceptionHandler

CoroutineExceptionHandler 类似 Android 中的全局异常处理,当异常在协程树中传递时,如果没有设置 CoroutineExceptionHandler ,那么异常将被继续传递直到抛出,但如果设置了 CoroutineExceptionHandler ,那么则可以在这里处理未捕获的异常, CoroutineExceptionHandler 的创建如下所示。

val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
    Log.d("xys", "---${coroutineContext}  ${throwable.printStackTrace()}")
}

我们来看下面的这个例子,在父协程中设置CoroutineExceptionHandler,当它的子协程发生异常时,即使不使用try catch,异常也会被捕获。

MainScope().launch(exceptionHandler) {
    async {
        throw Exception("test")
    }
}

但是考虑下这样一个场景,让发生异常的协程使用CoroutineExceptionHandler,代码如下所示。

MainScope().launch {
    async(exceptionHandler) {
        throw Exception("test")
    }
}

很遗憾,这样就不能捕获异常,因为 CoroutineExceptionHandler 属于异常抛出的协程,它本身无法处理。

所以,CoroutineExceptionHandler 的使用也有这样的限制,即CoroutineExceptionHandler 必须在发生异常的父协程中设置,其原因就是协程的结构化并发,异常会传递到父协程中进行处理,所以,这里必须是父协程中设置 CoroutineExceptionHandler 才能生效。

要注意的是,CoroutineExceptionHandler 只是协程处理异常「最后的倔强」,此时协程已经完全Cancel,只是给你个通知,协程异常了,所以这里只能对异常做记录,无法再操作协程。

实战一:停不下来的协程

class MainActivity : AppCompatActivity() {

    private var job: Job? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        job = GlobalScope.launch {
            while (true) {
                Log.d("yyy-", "${System.currentTimeMillis()}")
            }
        }

        //取消
        binding.downloadBtn.setOnClickListener {
            job?.cancel()
        }
    }
}

发现取消协程后,日志还在疯狂输出,根本没有停下来。

实战二:可以停下来的协程

class MainActivity : AppCompatActivity() {

    private var job: Job? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        job = GlobalScope.launch {
            while (isActive) {
                Log.d("yyy-", "${System.currentTimeMillis()}")
            }
        }

        binding.downloadBtn.setOnClickListener {
            job?.cancel()
        }
    }
}

取消协程后,日志停止输出。

或者用 ensureActive() ,也可以达到同样的效果:

class MainActivity : AppCompatActivity() {

    private var job: Job? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        job = GlobalScope.launch {
            while (true) {
                ensureActive()
                Log.d("yyy-", "${System.currentTimeMillis()}")
            }
        }

        binding.downloadBtn.setOnClickListener {
            job?.cancel()
        }
    }
}

ensureActive() 的工作原理是,如果没有停止,就抛出一个 CancellationException
在这里插入图片描述

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-12-15 18:24:43  更:2021-12-15 18:25:08 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 9:25:27-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码