公司动态
Kotlin 协程 - 协程作用域 CoroutineScope
一、概念协程作用域 CoroutineScope 是一个接口没有任何抽象方法需要实现仅仅维护一个成员变量 CoroutineContext协程上下文将作为初始上下文对象传递给被创建的协程不同的实现类或作用域函数本质上的区别是持有的协程上下文不同配置不同。实现类(对象)提供环境核心库GlobalScope工厂函数MainScope( )CoroutineScope( )平台支持ViewModel.viewModelScopeLifecycleOwner.lifecycleScope扩展函数创建任务协程构建器launch( )async( )作用域函数业务代码普通函数runBlocking( )挂起函数coroutineScope( )supervisorScope( )withContext( )runInterruptible( )withTimeout( )withTimeoutOrNull( )二、普通函数 runBlocking()用于把阻塞式的普通函数改造成协程环境会阻塞当前线程直到其内部所有协程执行完毕。由于会阻塞线程在开发中不会使用一般用于改造main函数用来测试代码单元测试一般使用runTest。public actual fun T runBlocking(context: CoroutineContext, //不指定Dispatchers的话默认是BlockingEventLoop不是Defaultblock: suspend CoroutineScope.() - T): Tfun main(): Unit runBlocking { launch { println(...) } }三、实现类3.1 核心库3.1.1 GlobalScope单例对象不推荐使用。全局协程作用域不绑定到任何Job上无法取消通过它启动的子协程不会阻塞其所在线程可以一直运行到APP停止相当于守护线程不会阻止JVM结束运行子协程运行在自己的调度器上不会继承上下文与父协程没有联系因此所有开启的子协程都需要分别手动来管理容易造成内存泄漏和CPU冗余使用例如当Activity销毁后协程还在执行耗时操作占用资源。3.1.2 ContextScope上下文作用域intermal修饰未对外暴露根据指定的上下文创建协程作用域。使用工厂函数 MainScope()、CoroutineScope() 传入上下文对象参数获取到的就是 ContextScope 实例。3.2 工厂函数3.2.1 mainScope()默认上下文使用 SupervisorJob()Dispatchers.Main 的协程作用域。该调度器会绑定到主线程在Android中就是 UI Thread在 onDestroy() 中调用 scope.cancel() 关闭协程。可用于主动控制协程的生命周期对Android开发意义在于避免内存泄漏。public fun MainScope(): CoroutineScope ContextScope(SupervisorJob() Dispatchers.Main)class AndroidActivity{ private val scope MianScope() scope.launch{ launch{ coroutineContext //父协程的上下文 currentCoroutineContext() //当前协程的上下文 } } override fun onDestroy(){ scope.cancel() //手动在生命周期里释放资源 } }3.2.2 CoroutineScope()根据自定义上下文创建协程作用域如果上下文中没有 Job 会自动创建一个用于结构化并发。CoroutineScope是一个只包含 coroutineContext 属性的接口虽然我们可以创建一个实现类但这不是一个流行的做法而且存在不小心在其它地方取消作用域。通常我们会更喜欢通过对象来启动协程最简单的办法是使用 CoroutineScope() 工厂函数它用传入的上下文来创建作用域。public fun CoroutineScope(context: CoroutineContext): CoroutineScope ContextScope(if (context[Job] ! null) context else context Job())3.3 Android平台支持把所有耗时操作都定义为挂起函数一般情况下都是在 UI 或 ViewModel 中调用这两处都提供了协程作用域对象这样当生命周期结束的时候可以取消所有任务避免内存泄漏。当脱离这两个环境时就需要自定义作用域。3.3.1 ViewModel.viewModelScope()是ViewModelKTX 提供的扩展属性。使用的上下文是SupervisorJob() Dispatchers.Main.immediateViewModel销毁时协程作用域会自动被 cancel避免造成协程泄漏内存泄漏。public val ViewModel.viewModelScope: CoroutineScope3.3.2 LifecycleOwner.lifecycleScope()是 LifeCycleKTX 提供的扩展属性。使用的上下文是SupervisorJob() Dispatchers.Main.immediate在 Activity/Fragment 销毁时协程作用域会自动被 cancel避免造成协程泄漏内存泄漏。public val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope3.3.3 自定义应用作用域相比于全局作用域 GlobalScope应用作用域 AppScope 可以在需要时取消所有协程可用于处理比单个屏幕声明周期更长的操作或独立于 UI 和 ViewModel 的单例工具类中如单例工具类中需要从DataStore中初始化状态而DataStore的读取是挂起函数。class App : Application() { private val exceptionHandler CoroutineExceptionHandler {context, throwable - //异常处理 } companion object { lateinit var appScope: CoroutineScope private set } override fun onCreate() { super.onCreate() appScope CoroutineScope(SupervisorJob() Dispatchers.Default exceptionHandler) } override fun onTerminate() { super.onTerminate() appScope.cancel() // 清理取消 } }四、扩展函数4.1 协程构建器 CoroutineBuilderlaunch() 和 async() 是 CoroutineScope 接口的扩展函数继承了它的 coutineContext 来自动传播其上下文元素和可取消性。挂起函数需要相互传递 Continuation每个挂起函数都要由另一个挂起函数或协程调用这一切都是从协程构建器创建协程开始的即作用域函数只能创建子协程协程构建器能创建根协程或子协程因为它通过实例调用可以存在于普通函数中。挂起函数创建的子协程是串行运行协程构建器创建的子协程是并行运行。参数 context指定协程上下文。默认为空的上下文。参数 start指定协程启动模式。默认为可以立刻被调度的状态。参数 block协程执行体即要做的任务。launch( )无需产生值public fun CoroutineScope.launch(context: CoroutineContext EmptyCoroutineContext,start: CoroutineStart CoroutineStart.DEFAULT,block: suspend CoroutineScope.() - Unit): Job返回一个Job实例用来管理协程的生命周期。async( )需要产生值public fun T CoroutineScope.async(context: CoroutineContext EmptyCoroutineContext,start: CoroutineStart CoroutineStart.DEFAULT,block: suspend CoroutineScope.() - T): DeferredT返回一个Deferred实例Job的子类通过 await() 拿到执行的结果包括异常。由于 await() 是挂起函数只能在协程作用域中调用因此不要用 async() 做根协程拿不到值就相当于 launch()。通常用于在协程作用域中构建并发子协程合并结果。4.1.1 协程启动模式 CoroutineStartCoroutineStart.DEFAULT协程创建后立即开始调度不一定此时就被线程执行了在被线程执行前如果协程被取消其将直接进入取消响应状态CoroutineStart.LAZY只要协程被需要时包括主动调用 start()、join()、await()才会开始调度。如果调度前被取消协程将进入异常结束状态。CoroutineStart.ATOMIC协程创建后立即开始调度内部代码执行到第一个挂起点之前不响应取消操作内部第一个挂起函数之前的代码一定执行。CoroutineStart.UNDISPATCHED协程被创建后立即在当前函数调用栈中执行所处的函数在哪个线程就是哪个即便该协程通过Dispatcher指定了运行的线程直到内部代码执行到第一个挂起点挂起函数运行完后之后的代码就是在Dispatcher指定的线程中运行了。//ATOMIC模式 val job launch(start CoroutineStart.ATOMIC) { //这里的代码一定会执行 delay(10000) //第一个挂起点 println(Job 完成) } delay(1000) job.cancel() //取消上面的job println(main 结束) //LAZY模式 val deferred async(start CoroutineStart.LAZY) { 27 } //此处执行一些计算...之后才需要拿到 deferred的值 deferred.await() //如果在这之前调用cancel()取消就直接抛异常JobCancellationException //UNDISPATCHED模式 launch(context Dispatchers.IO, start CoroutineStart.UNDISPATCHED) { println(挂起之前 Thread.currentThread().name) //打印main delay(10) println(挂起之后 Thread.currentThread().name) //打印DefaultDispatcher-worker-1 }五、作用域函数都是挂起函数不会阻塞线程。由于挂起需要协程环境只能由其它挂起函数或构建器调用因此只能用来创建子协程。形参上将Lambda的接收者指定为CoroutineScope因此可以在内部调用构建器开启子协程通常被用于包装函数一个作用域包装一堆调用尤其是withContext() 。异常结束自身supervisorScope()异常连锁反应指定上下文withContext()继承上下文coroutineScope()runInterruptible()限制执行时间withTimeout()withTimeoutOrNull()注意无法使用withContext(SupervisorJob())替代supervisorScope因为withContext仍会使用常规JobSupervisorJob只是它的父级Job。需要同时使用多个协程作用域函数的功能需要嵌套使用。suspend fun main() runBlocking { val scope CoroutineScope(Dispatchers.Default) scope.launch { delay(1000) println(协程1) } scope.launch { delay(1000) println(协程2) } // scope.cancel() //作用域被取消里面的子协程都会被取消 //delay是因为scope作用域自定义了上下文调度器没有继承父协程runBlicking的。 //它指定在了其它线程执行自己也没有挂起特性主线程会发现没有任务了会直接结束。 //也说明了协程不会阻塞线程。 delay(2000) }5.1 coroutineScope()异常会连锁取消子协程、兄弟协程和父协程。上下文继承上下文使用场景经常被用来包装一个挂起函数的主体。多用于并行分解任务逻辑。public suspend fun R coroutineScope(block: suspend CoroutineScope.() - R): Rsuspend fun main() runBlocking { //将main()函数内部改为支持协程式编写内部全部子协程运行完才会退出 //使用coroutineScope会挂起父协程runBlocking不然因为协程不会阻塞线程runBlocking会直接结束 coroutineScope { //这里换成supervisorScope子协程1就会执行 val job1 launch { delay(5000) println(子协程1) } val job2 launch { delay(1000) println(子协程2) throw Exception() //子协程1不会执行 } } }5.2 supervisorScope()异常不会影响兄弟协程和父协程。上下文使用 supervisorJob 而不是基于父协程构建自定义 Job 意味着失去结构化并发父协程的取消不会连带取消该子协程该子协程的异常不会向上传播。它是一个独立的作用域是根协程开启的子协程是顶级协程需要自己处理异常不会像 coroutineScope() 那样抛出。使用场景主要用于启动多个独立任务。public suspend fun R supervisorScope(block: suspend CoroutineScope.() - R): R5.3 withContext()异常会连锁取消子协程、兄弟协程和父协程。上下文指定上下文使用场景经常用来指定协程执行的线程和启动模式。这样在封装函数的时候里面的业务代码就会被执行在正确的线程中如 CPU密集型计算、IO、更新UIpublic suspend fun T withContext(context: CoroutineContext,block: suspend CoroutineScope.() - T): Tsuspend fun getSum(num1: Int, num2: Int): Int withContext(Despatcher.DEFAULT) { num1 num2 } suspend fun getData(): UserBean withContext(Despatcher.IO) { Retrofit.apiService.getData() }5.4 runInterruptible()异常会连锁取消子协程、兄弟协程和父协程。上下文指定上下文使用场景底层使用 withContext() 实现但会检查 Job 状态一旦协程取消会调用 Thread.interrupt() 来释放线程用来包装那些不支持挂起的 Java IO操作避免作用域中没有挂起点不会响应取消。public suspend fun T runInterruptible(context: CoroutineContext EmptyCoroutineContext,block: () - T): Tsuspend fun readFile() runInterruptible(Dispatchers.IO) { Thread.sleep(5000) File(data.txt).readText() }5.5 withTimeout()异常会连锁取消子协程、兄弟协程和父协程。上下文继承上下文使用场景超时未执行完会抛异常并返回一个值。超时抛出的TimeoutCancellationException是CancellationException子类因此不会影响其他协程。public suspend fun T withTimeout(timeout: Duration,block: suspend CoroutineScope.() - T): TwithTimeout(1000) { println() }5.6 withTimeoutOrNull()异常子协程异常会连锁取消其它子协程和自己。上下文继承上下文使用场景超时未执行完不抛异常返回null。用来包装那些出现异常后会一直等待的操作例如网络操作等待结果超过5s后不太可能会收到结果了。public suspend fun T withTimeoutOrNull(timeMillis: Long,block: suspend CoroutineScope.() - T): T?withTimeoutOrNull(1000) { println() } ?: 值为空六、创建协程的区别协程构建器和协程作用域函数中都包含了形参 block: suspend CoroutineScope.() - Unit。在这个带接收者的 lambda 里写代码可以通过属性 coroutineContext 拿到协程上下文能调用协程构建器去创建子协程也能调用挂起函数去挂起当前协程。全局GlobalScope阻塞runBlocking( )生命感知MainScope( )ViewModel.viewModelScopeLifecycleOwner.lifecycleScope自定义CoroutineScope( )挂起异常结束自身supervisorScope( )异常连锁反应指定上下文withContext( )继承上下文coroutineScope( )限制执行时间withTimeout( )withTimeoutOrNull( )通过协程构建器创建通过协程作用域函数创建①通过协程作用域实例调用创建的可能是根协程在普通函数中也可能是子协程在其它协程中②直接调用创建的只能是子协程必须存在父协程即外部有上面说的block包裹。创建的只能是子协程。挂起函数“挂起恢复”的特性只能在协程环境下实现因此只能在其它挂起函数或协程中调用一定存在父协程。携带来自CoroutineScope的协程上下文异常通过Job传递给父协程构建器是并发执行的但可调用 join()、await()携带Continuation的协程上下文异常像普通函数那样抛出挂起函数是阻塞的按顺序执行tihs是被创建出来的协程因为调用block的是被创建出来的协程协程都是 AbstractCoroutine 的子类而它实现了CoroutineScope并不是调用它的协程作用域对象。因此在block中创建子协程①通过协程构建器能直接通过 coroutineScope 属性拿到当前协程的上下文继承从而形成父子关系具备传播取消和异常。②通过作用域函数父协程被挂起时会返回一个 Continuation 续体对象给该挂起函数这个续体就是父协程协程都是 AbstractCoroutine 的子类而它实现了Continuation从而获取并继承上下文形成父子关系。③通过协程作用域对象调用协程构建器不是子协程因为没有上下文继承关系无法通过Job传播取消和异常。GlobalScope是EmptyCoroutineContext而MainScop、ViewModelScope、LifecyleScope都是指定的SupervisorJob()至于CoroutineScope()虽然构造可以传入父协程上下文构建内外相同的作用域何必多此一举。GlobalScope.launch { //不是子协程外层的取消不会取消它 CoroutineScope(Dispatchers.IO).launch { } //子协程虽然创建了新的作用域对象但是通过coroutineContext获取了父协程上下文间接继承 CoroutineScope(coroutineContext).launch { } //不是子协程Job会替换掉父协程中的Job CoroutineScope(coroutineContext Job()).launch { } }多任务-串行launch 多个 withContext多任务-并行launch 多个 asynclaunch 多个 launch七、多个协程作用域之间的关系在处理过程中需要执行一个额外的非必要操作例如收集数据分析若是存在于挂起函数中则会额外等待这个操作执行完若这个操作发生异常还会波及正常的业务这个时候最好是放在一个单独的作用域里启动它。一般通过构造单元测试、控制此作用域或者函数注入如果是为了调用一些函数使用SupervisorJob如果是收集异常信息使用CoroutineExceptionHandler。在结构化并发中由于作用域存在嵌套使用因此有多种情况类型场景举例异常传播特征顶级作用域1.根协程之间。2.GlobalScope嵌套GlobalScope彼此独立互不影响。3.A和B是两个作用域对象A开启的作用域中B开启了作用域两个作用域彼此独立互不影响。4.supervisorScope() 或 supervisorJob 由于使用了新的 Job相当于是一个独立的根协程与外部互不影响。不向外传播。协同作用域外层有父协程且自身非另外的作用域对象开启。双向传播。主从作用域外层有父协程自身是supervisorScope()或supervisorJob。与内部直接子协程主从与外部协同。向下单向传播。