Kotlin协程-CoroutineScope协程效果域

Kotlin协程系列:

  • 协程的根本运用
  • 协程的上下文了解
  • 协程的效果域办理(本文)
  • 协程的常见进阶运用

前文中咱们演示了协程的根本运用,和协程的上下文环境,而之前的示例咱们八成都是运用的是 GlobalScope 来发动的协程,难道没有其他办法发动协程了吗?当然有的,这一期就首要介绍一下协程效果域的概率与常见的一些协程效果域。

协程的效果域 Scope 涉及到 代码运转规模,协程的生命周期,主动办理协程的生命周期等。下面咱们先看看协程的生命周期与效果规模

一、协程的生命周期

当咱们创立一个协程的时分,会回来一个Job目标,不管是经过回来值办理,仍是经过 launch 的结构办法的方法办理,其实是相同的。

咱们经过Job就能够获取当时协程的运转状况,还能够随时撤销协程。

协程的状况查询

  • isActive
  • isCompleted
  • isCancelled

常用的协程操作:

  • cancel 用于Job的撤销,撤销协程
  • start 用于发动一个协程,让其抵达Active状况
  • invokeOnCompletion 增加一个监听,当工作完结或者反常时会调用
  • join 堵塞并等候当时协程完结

协程不是默许创立就发动了吗? 怎样还有一个 start 办法 。

其实协程默许是发动的,可是咱们能够创立一个懒加载的协程,手动start才开启协程。

val job = GlobalScope.launch(start = CoroutineStart.LAZY) {
    YYLogUtils.w("履行在协程中...")
    delay(1000L)
    YYLogUtils.w("履行结束...")
}
job.start()

协程的撤销,咱们之前也讲到过,一般咱们能够手动的调用 calcel 或者在onDestory的时分调用 calcel:

var job: Job = Job()
...
 GlobalScope.launch(job) {
   YYLogUtils.w("履行在协程中...")
    delay(1000L)
    YYLogUtils.w("履行结束...")
}
override fun onDestroy() {
    job.cancel()
    super.onDestroy()
}

协程履行完的回调 invokeOnCompletion 也是咱们常用的监听,在正常履行结束,或者反常履行结束都会回调这个办法。

      val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
            YYLogUtils.e(throwable.message ?: "Unkown Error")
        }
        val job = GlobalScope.launch(Dispatchers.Main + exceptionHandler) {
            YYLogUtils.w("履行在另一个协程中...")
            delay(1000L)
            val num = 9/0
            YYLogUtils.w("另一个协程履行结束...")
        }
        job.invokeOnCompletion {
            YYLogUtils.w("完结或反常的回调")
        }

没有反常的回调:

Kotlin协程-CoroutineScope协程作用域

参加9/0的反常代码之后的回调:

Kotlin协程-CoroutineScope协程作用域

二、协程的效果域

当咱们创立一个协程的时分,都会需求一个CoroutineScope,它是协程的效果域,咱们一般运用它的launch函数以及async函数去进行协程的创立。

2.1 常用的协程效果域

之前咱们都是经过 GlobalScope 来发动一个协程的,其实这样运用在 Android 开发中并不好。由于是大局的效果域。

在 Android 开发过程中,咱们需求了解一些协程代码运转的规模。而一切的Scope 如GlobalScope 都是 CoroutineScope 的子类,咱们的协程创立都需求这样一个 CoroutineScope 来发动。

一同咱们还有其他的一些效果规模的 CoroutineScope 目标。

  • GlobeScope:大局规模,不会主动结束履行。
  • MainScope:主线程的效果域,大局规模
  • lifecycleScope:生命周期规模,用于activity等有生命周期的组件,在DESTROYED的时分会主动结束。
  • viewModelScope:viewModel规模,用于ViewModel中,在ViewModel被收回时会主动结束

不同的Scope有不同的运用场景,下面我会细心解说。

2.2 coroutineScope vs runBlocking

运用 coroutineScope 构建器声明自己的效果域。它会创立一个协程效果域而且在一切已发动子协程履行结束之前不会结束。runBlocking 与 coroutineScope 的首要差异在于后者在等候一切子协程履行结束时不会堵塞当时线程。

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

该函数被 suspend 润饰,是一个挂起函数,前面咱们说了挂起函数是不会堵塞线程的,它只会挂起协程,而不堵塞线程。

前面咱们说了 runBlocking 是桥接堵塞代码与挂起代码之前的桥梁,其函数自身是堵塞的,可是能够在其内部运转 suspend 润饰的挂起函数。在内部一切子协程运转结束之前,他是堵塞线程的。

2.3 怎样运用 coroutineScope

比方咱们界说一个 suspend 标记的办法,内部履行一个协程,咱们看看 coroutineScope 运用示例:

咱们界说一个 suspend 办法,内部回来 coroutineScope 效果域目标,内部履行的是协程。

    private suspend fun saveSth2Local(coroutineBlock: (suspend CoroutineScope.() -> String)? = null): String? {
        return coroutineScope {
//            coroutineBlock!!.invoke(this)
//            coroutineBlock?.invoke(this)
//            if (coroutineBlock != null) {
//                coroutineBlock.invoke(this)
//            }
            coroutineBlock?.let { block ->
                block()
            }
        }
    }

注释的几种代码都是相同的效果,这么写为了更便利咱们了解。传入的 coroutineBlock 是一个高阶扩展函数,假如对这种写法比较陌生能够看看我的这一篇文章。

那么在运用咱们这一个函数的时分就能够这么运用:

   MainScope().launch {
            YYLogUtils.w("履行在一个协程中...")
            val result = saveSth2Local {
                async(Dispatchers.IO) {
                    "123456"
                }.await()
            }
            YYLogUtils.w("一个协程履行结束... result:$result")
        }

打印成果:

Kotlin协程-CoroutineScope协程作用域

2.4 为什么要用 coroutineScope

有人会持续发问,咱们为什么需求运用 coroutineScope 来完结呢?直接在一个协程中写完不香吗?

其实这么写是为了把一些比较耗时的多个使命拆分为不同的小使命,指定了一个效果域,在此效果域下面假如撤销,就整个效果域都撤销,假如反常则整个效果域内的协程都撤销。简略的说便是更加的灵活。

咱们举例说明一下,比方一个 coroutineScope 下面有多个使命

suspend fun showSomeData() = coroutineScope {
    val data1 = async {
        delay(2000)
        100
    }
    val data2 = async {
        delay(3000)
        20
    }
  val num = withContext(Dispatchers.IO) {
        delay(3000)
        val random = Random(10)
        data1.await() + data2.await() + random.nextInt(100)
    }
    YYLogUtils.w("num:"+num)
}

上面的代码意思便是data1 和 data2 并发,等它们完结之后获取到 num 打印出来。

为什么这么写,便是很明显的用到效果域的一个概念,假如data1 data2 失利,那么 withContext 就不会履行了,假如 random 反常,那么整个协程效果域内的使命都会撤销。

这便是效果域的效果!

2.5 父子协程效果域

当一个协程在其它协程在中发动的时分,那么咱们能够了解它是子协程吗,包裹它的便是它的父协程, 它将经过 CoroutineScope.coroutineContext 来承继了父协程的上下文,而且这个新协程的 Job 将会成为父协程作业的子作业。当一个父协程被撤销的时分,一切它的子协程也会被递归的撤销。

多的不说,代码运转演示一番:

   val job = CoroutineScope(Dispatchers.Main).launch {
                async(Dispatchers.IO) {
                    YYLogUtils.w("切换到一个协程1")
                    delay(5000)
                    YYLogUtils.w("协程1履行结束")
                }
                launch {
                    YYLogUtils.w("切换到一个协程2")
                    delay(2000)
                    YYLogUtils.w("协程2履行结束")
                }
                GlobalScope.launch {
                    YYLogUtils.w("切换到一个协程3")
                    delay(3000)
                    YYLogUtils.w("协程3履行结束")
                }
                MainScope().launch {
                    YYLogUtils.w("切换到一个协程4")
                    delay(4000)
                    YYLogUtils.w("协程4履行结束")
                }
            }
    override fun onDestroy() {
        super.onDestroy()
        job.cancel()
    }           

这么写咱们应该都能看懂了,在一个父协程中并发发动4个子协程,毫无疑问,他们的履行次序为:

Kotlin协程-CoroutineScope协程作用域

没缺点,可是咱们发动完四个子协程之后,咱们把父协程cancel掉,咱们看能正常的封闭子协程吗?

Kotlin协程-CoroutineScope协程作用域

此刻咱们封闭父协程,看看是否能成功的封闭子协程

Kotlin协程-CoroutineScope协程作用域

成果便是,确实父协程封闭能封闭子协程,可是又不能彻底封闭。

由于运用 GlobalScope MainScope 来发动一个协程时,则新协程的作业没有父作业。 因此它与这个发动的效果域无关且独立运作,与它的父协程没有相关上。

那咱们能不能让不能撤销的子协程强行跟父协程相关上?还有这操作?看我操作:

   val job = CoroutineScope(Dispatchers.Main).launch {
                async(Dispatchers.IO) {
                    YYLogUtils.w("切换到一个协程1")
                    delay(5000)
                    YYLogUtils.w("协程1履行结束")
                }
                launch {
                    YYLogUtils.w("切换到一个协程2")
                    delay(2000)
                    YYLogUtils.w("协程2履行结束")
                }
                GlobalScope.launch {
                    YYLogUtils.w("切换到一个协程3")
                    delay(3000)
                    YYLogUtils.w("协程3履行结束")
                }
                MainScope().launch(job!!) {
                    YYLogUtils.w("切换到一个协程4")
                    delay(4000)
                    YYLogUtils.w("协程4履行结束")
                }
            }
    override fun onDestroy() {
        super.onDestroy()
        job.cancel()
    } 

例如咱们把 MainScope 发动的时分传入上下文环境,已然它不能承继父协程的上下文,咱们手动的设置给它,行不行?

看下Log:

Kotlin协程-CoroutineScope协程作用域

此刻咱们封闭父协程,看看是否能成功的封闭子协程

能够看到 MainScope 所在的协程就能够跟从父协程一同撤销了。

这么说,咱们应该能了解透彻了吧。下面咱们就一同看看 MainScope 和 GlobalScope 是什么鬼,怎样就不能持续父协程的上下文了呢?,它们之间又有什么差异?

三、MainScope vs GlobalScope

都是大局的效果域,可是他们有差异。假如不做处理他们都是运转在大局无法撤销的,可是GlobalScope是无法撤销的,MainScope是能够撤销的

GlobalScope 的源码如下:

public object GlobalScope : CoroutineScope {
    /**
     * Returns [EmptyCoroutineContext].
     */
    override val coroutineContext: CoroutineContext
        get() = EmptyCoroutineContext
}

能够看到它的上下文目标是 EmptyCoroutineContext 目标,并没有Job目标,所以咱们无法经过 Job 目标去cancel 此协程。所以他是无法撤销的进程级其他协程。除非有特殊的需求,咱们都不运用此协程。

MianScope 的源码如下:

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

能够看到它的上下文目标是 SupervisorJob + 主线程构成的。假如对 + 号不了解,能够看本系列的第二篇。所以咱们说它是一个能够撤销的大局主线程协程。

依照上面的代码,咱们就能这么举例说明:

var mainScope= MainScope()
mainScope.launch {
            YYLogUtils.w("履行在一个协程中...")
            val result = saveSth2Local {
                async(Dispatchers.IO) {
                    "123456"
                }.await()
            }
            YYLogUtils.w("一个协程履行结束... result:$result")
        }
override fun onDestroy() {
    super.onDestroy()
     mainScope.cancel()
}

四、viewModelScope

viewModelScope 只能在ViewModel中运用,绑定ViewModel的生命周期。运用的时分需求导入依靠:

implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'

源码如下:

private const val JOB_KEY = "androidx.lifecycle.ViewModelCoroutineScope.JOB_KEY"
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))
        }
internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
    override val coroutineContext: CoroutineContext = context
    override fun close() {
        coroutineContext.cancel()
    }
}

能够看到viewModelScope运用 SupervisorJob 而不是用 Job。 为了 ViewModel 能够撤销协程,需求完结 Closeable 接口 viewModelScope 默许运用 Dispatchers.Main, 便利 Activity 和 Fragment 更新 UI

private final Map<String, Object> mBagOfTags = new HashMap<>();
<T> T getTag(String key) {
    synchronized (mBagOfTags) {
        return (T) mBagOfTags.get(key);
    }
}
<T> T setTagIfAbsent(String key, T newValue) {
    T previous;
    synchronized (mBagOfTags) {
        previous = (T) mBagOfTags.get(key);
        if (previous == null) {
            mBagOfTags.put(key, newValue);
        }
    }
    T result = previous == null ? newValue : previous;
    if (mCleared) {
        closeWithRuntimeException(result);
    }
    return result;
}
@MainThread
final void clear() {
    mCleared = true;
    if (mBagOfTags != null) {
        for (Object value : mBagOfTags.values()) {
            closeWithRuntimeException(value);
        }
    }
    onCleared();
}
private static void closeWithRuntimeException(Object obj) {
    if (obj instanceof Closeable) {
        try {
            ((Closeable) obj).close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

类经过 HashMap 存储 CoroutineScope 目标,撤销的时分, 在 clear() 办法中遍历调用 closeWithRuntimeException 撤销了viewModelScope 的协程。

代码能够说是简略又明晰

运用的时分和 GlobalScope 的运用相同的,需求注意的是无需咱们手动的cancel了。

运用的时分平替 GlobalScope 即可完结:


    viewModelScope.launch{
        YYLogUtils.w("履行在另一个协程中...")
        delay(1000L)
        YYLogUtils.w("另一个协程履行结束...")
    }

五、lifecycleScope

lifecycleScope只能在Activity、Fragment中运用,会绑定Activity和Fragment的生命周期。运用的时分需求导入依靠:

 implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'

它的根本运用和 viewModelScope 是相同的。可是它多了生命周期的的一些感知。

例如在Resume的时分发动协程:

fun launchWhenResumed(block: suspend CoroutineScope.() -> Unit): Job = launch {
    lifecycle.whenResumed(block)
}

怎样完结的,咱们能够看看 LifecycleController 的源码:

@MainThread
internal class LifecycleController(
    private val lifecycle: Lifecycle,
    private val minState: Lifecycle.State,
    private val dispatchQueue: DispatchQueue,
    parentJob: Job
) {
    private val observer = LifecycleEventObserver { source, _ ->
        if (source.lifecycle.currentState == Lifecycle.State.DESTROYED) {
            // cancel job before resuming remaining coroutines so that they run in cancelled
            // state
            handleDestroy(parentJob)
        } else if (source.lifecycle.currentState < minState) {
            dispatchQueue.pause()
        } else {
            dispatchQueue.resume()
        }
    }
    init {
        // If Lifecycle is already destroyed (e.g. developer leaked the lifecycle), we won't get
        // an event callback so we need to check for it before registering
        // see: b/128749497 for details.
        if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
            handleDestroy(parentJob)
        } else {
            lifecycle.addObserver(observer)
        }
    }
    //...
}

在init初始化的时分,增加LifecycleEventObserver监听,对生命周期进行了判别,当大于当时状况的时分,也便是生命周期履行到当时状况的时分,会调用dispatchQueue.resume()履行队列,也便是协程开始履行。

相比 viewModelScope 相同的无需咱们手动的撤销,可是它又多了一些Activity,Fragment的生命周期感知。

运用的时分应该也是很好了解的:


    lifecycleScope.launch{
        YYLogUtils.w("履行在另一个协程中...")
        delay(1000L)
        YYLogUtils.w("另一个协程履行结束...")
    }
    lifecycleScope.launchWhenResumed {
        YYLogUtils.w("履行在另一个协程中...")
        delay(3000L)
        YYLogUtils.w("另一个协程履行结束...")
    }

六、自界说协程效果域

咱们有了 lifecycleScope 和 viewModelScope 真的是太便利了,不管是在UI页面中,仍是在ViewModel中处理异步逻辑,都是十分的便利。

可是项目中,除了ViewModel 和 Activity / Fragment 之外,还有其他的UI布局,比方PopupWindow Dialog。那怎样办?

有人说把Activity目标当参数传递进去,然后就能运用 lifecycleScope 啦,这…没缺点,可是其实咱们有更好的做法。便是上面咱们讲到的 MainScope() 。

它默许的上下文是咱们的主线程加上一个 SupervisorJob 办理的,比方咱们在Dialog 中就能够经过这样来办理协程效果域啦。

class CancelJobDialog() : DialogFragment(), CoroutineScope by MainScope() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
    }
    @SuppressLint("InflateParams")
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        dialog?.requestWindowFeature(Window.FEATURE_NO_TITLE)
        dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
        return inflater.inflate(R.layout.dialog_cancel_job, container, false)
    }
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val mNoTv = view.findViewById<TextView>(R.id.btn_n)
        val mYesTv = view.findViewById<TextView>(R.id.btn_p)
        mNoTv.click {
            dismiss()
        }
        mYesTv.click {
            doSth()
        }
    }
    private fun doSth() {
       launch{
           YYLogUtils.w("履行在另一个协程中...")
           delay(1000L)
           YYLogUtils.w("另一个协程履行结束...")
        }
         dismiss()
   }
    override fun onDismiss(dialog: DialogInterface) {
        cancel()
        super.onDismiss(dialog)
    }

咱们完结一个 CoroutineScope 效果域接口,然后运用委托的特点把 MainScope 的完结给它。这样这个 Dialog 便是一个协程的效果域了。

在内部就能 launch N多个子协程了,注意咱们在 onDismiss 的时分把主协程都撤销掉了,依照咱们前面讲到的父子协程的效果域。那么它内部 launch 的多个子协程就能一同撤销了。

这样就能简略的完结一个自界说的协程效果域了,当然完结自界说协程效果域的办法有多种,这儿仅仅介绍最简略的一种,由于时间与篇幅的原因,后面会再次说到。

总结

上一篇咱们讲了协程的根本运用,把握的便是协程发动的几种方法,切换线程的几种方法,异步与同步的履行,和挂起函数,堵塞与非堵塞的概念。还讲到了协程的上下文,本来调度线程,办理协程的Job等都是上下文的完结。

这一篇咱们进一步了解协程的效果域,本来效果域分这么多类型,了解了父子协程是怎样处理撤销逻辑的,coroutineScope的效果域规模的运用等等。

其实到这儿协程的解说就差不多了,下一篇咱们会讲一下协程的场景运用,网络恳求的运用,自界说协程,与协程的并发与锁等相关用法。

假如咱们有不明白的我更推荐你从系列的第一篇开始看,内部的完结是一步一步层层递进的。

协程的概念与结构比较大,我自己如有解说不到位或错漏的地方,希望同学们能够指出沟通。

假如感觉本文对你有一点点点的启示,还望你能点赞支撑一下,你的支撑是我最大的动力。

Ok,这一期就此完结。

Kotlin协程-CoroutineScope协程作用域

我正在参加技能社区创作者签约计划招募活动,点击链接报名投稿。