kotlin协程再了解

感觉协程看了很久,仍然是云山雾绕的,一脸懵,直到看了 [码上开学]协程系列视频(扔物线,共3集)扔物线的讲解才感觉有一些明白,以前把协程想的太复杂了,其实它就是一个线程框架,所以还是要先了解概念,再学习怎么用,不然真的很痛苦。
接下来咱们先学习一下Kotlin协程中的概念,CoroutineScope, CoroutineContext, suspend, Job, Deferred,launch, async, withContext, coroutineScope,他们是什么,用来做什么。
然后学习Kotlin协程的使用,应该还有一篇文章学习Kotlin协程的原理,这里先按下不表。

协程就是一个线程框架,suspend就是一个提醒,用来告诉调用者这是一个耗时任务,需要在其他suspend方法或协程中调用。

CoroutineScope

CoroutineScope是协程的作用域,用于管理协程,管理的内容包括协程的启动方式和协程的生命周期。

public interface CoroutineScope {
    /**
     * The context of this scope.
     * Context is encapsulated by the scope and used for implementation of coroutine builders that are extensions on the scope.
     * Accessing this property in general code is not recommended for any purposes except accessing the [Job] instance for advanced usages.
     *
     * By convention, should contain an instance of a [job][Job] to enforce structured concurrency.
     */
    public val coroutineContext: CoroutineContext
}

CoroutineScope被定义为一个接口,拥有类型为CoroutineContext的属性。
创建CoroutineScope推荐使用CoroutineScope()和MainScope()工厂方法。
MainScope()用来创建在主线程执行的CoroutineScope。

public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
class MyAndroidActivity {
      private val scope = MainScope() + CoroutineName("MyAndroidActivity")
 
      override fun onDestroy() {
          super.onDestroy()
          scope.cancel()
      }
  }

GlobalScope是CoroutineScope的子类,它是一个单例类,我们并不推荐使用它。

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

runBlocking()是一个顶层函数,它可以在CoroutineScope中运行协程,但它会阻塞当前线程,直到其内部所有逻辑及子协程逻辑全部执行完成,从源码中可知,它是通过死循环的方式等待逻辑执行完成的,不推荐在开发中使用。

CoroutineContext

CoroutineContext 是协程上下文,表示协程的运行环境,它是一个带索引的集合,集合的元素是Element。


image.png

CoroutineContext是一个接口,声明了几个常用的方法。

public interface Element : CoroutineContext {
        /**
         * A key of this coroutine context element.
         */
        public val key: Key<*>

        public override operator fun <E : Element> get(key: Key<E>): E? =
            @Suppress("UNCHECKED_CAST")
            if (this.key == key) this as E else null

        public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
            operation(initial, this)

        public override fun minusKey(key: Key<*>): CoroutineContext =
            if (this.key == key) EmptyCoroutineContext else this
    }

Element也是一个接口,它是CoroutineContext的子接口。
咱们平时用到的Job,Dispatcher,CoroutineName等都是CoroutineContext的子类。

suspend

suspend就是一个提醒,用来告诉调用者这是一个耗时任务,需要在其他suspend方法或协程中调用

fun main() {
    runBlocking {
        launch {
            printHello()
            println("world thread: ${Thread.currentThread()}")
        }
    }
}

private suspend fun printHello() {
    withContext(Dispatchers.IO) {
        delay(1000)
        println("hello, thread: ${Thread.currentThread()}")
    }
}

输出结果:
hello, thread: Thread[DefaultDispatcher-worker-1,5,main]
world thread: Thread[main,5,main]

从输出结果得知,在协程中等待挂起函数结束之后才继续执行下边的操作,挂起函数我们切到IO线程执行,挂起函数执行完成之后自动恢复到刚才的位置,继续执行,所以我们可以使用挂起函数取代回调函数。

Job

调用launch函数创建协程时返回值就是Job,返回的Job代表刚创建的协程,Job有生命周期的概念,它会存在多种不同的状态。


image.png
public interface Job : CoroutineContext.Element {}

Job是一个接口,它是CoroutineContext.Element的子接口。


image.png

其他还有一些扩展函数,比如

public suspend fun Job.cancelAndJoin() {
    cancel()
    return join()
}

也都比较常用。
此外,还有CompletableJob,它带complete()方法,能主动完成Job。
任意子Job失败会导致此Job失败,并取消其他子Job,但SupervisorJob不会这样,SupervisorJob的子Job能独立的失败。
说到Job,就不得不一块说一下Deferred

Deferred

Deferred 是带返回值的Job

public interface Deferred<out T> : Job

image.png

await()方法可以等待Deferred结束并返回结果,但是请注意,此方法可能抛出CancellationException异常,在这两种情况下会抛出此异常。

  • 调用await的协程被取消;
  • Deferred获取完成结果是抛出CancellationException异常。
     try {
        deferred.await()
     } catch (e: CancellationException) {
        currentCoroutineContext().ensureActive() // throws if the current coroutine was cancelled
        processException(e) // if this line executes, the exception is the result of `await` itself
     }

调用此方法时我们可以添加try-catch
getCompleted()用来获取完成的结果,但是如果Deferred还没完成,此方法会抛出IllegalStateException异常。
同样,Deferred也有对应的CompletableDeferred,比较常用的方法就是complete,completeWith。

launch

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
}

launch是CoroutineScope的扩展方法,它的返回值是Job,launch用来启动一个新的协程。

async

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
}

async同样是CoroutineScope的扩展方法,它的返回值是Deferred,async也用来启动一个新的协程。

withContext

public suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T
): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return suspendCoroutineUninterceptedOrReturn sc@ { uCont ->
        // compute new context
        val oldContext = uCont.context
        // Copy CopyableThreadContextElement if necessary
        val newContext = oldContext.newCoroutineContext(context)
        // always check for cancellation of new context
        newContext.ensureActive()
        // FAST PATH #1 -- new context is the same as the old one
        if (newContext === oldContext) {
            val coroutine = ScopeCoroutine(newContext, uCont)
            return@sc coroutine.startUndispatchedOrReturn(coroutine, block)
        }
        // FAST PATH #2 -- the new dispatcher is the same as the old one (something else changed)
        // `equals` is used by design (see equals implementation is wrapper context like ExecutorCoroutineDispatcher)
        if (newContext[ContinuationInterceptor] == oldContext[ContinuationInterceptor]) {
            val coroutine = UndispatchedCoroutine(newContext, uCont)
            // There are changes in the context, so this thread needs to be updated
            withCoroutineContext(coroutine.context, null) {
                return@sc coroutine.startUndispatchedOrReturn(coroutine, block)
            }
        }
        // SLOW PATH -- use new dispatcher
        val coroutine = DispatchedCoroutine(newContext, uCont)
        block.startCoroutineCancellable(coroutine, coroutine)
        coroutine.getResult()
    }
}

withContext就是用来切线程的,它是一个挂起函数,它会在指定线程执行操作,挂起当前协程并返回结果。

private suspend fun printHello() {
    withContext(Dispatchers.IO) {
        delay(1000)
        println("hello, thread: ${Thread.currentThread()}")
    }
}

一般的用法是这样的。

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)
    }
}

coroutineScope是一个挂起函数,它会创建一个新的协程作用域,但它不会启动协程,它会挂起当前协程,但不会阻塞所在的线程。

  suspend fun showSomeData() = coroutineScope {
     val data = async(Dispatchers.IO) { // <- extension on current scope
       ... load some UI data for the Main thread ...
     }
 
     withContext(Dispatchers.Main) {
          doSomeWork()
          val result = data.await()
         display(result)
      }
  }

一般的用法是这样的。
作为对比,这里提一下supervisorScope()函数,coroutineScope中的任意子协程失败,会导致其他子协程取消,但supervisorScope不会这样,supervisorScope的子协程失败,不会导致其他子协程取消。

请阅读 kotlin协程
参考:

  1. 可能是最全的Kotlin协程讲解
  2. 【kotlin 协程】万字协程 一篇完成kotlin 协程进阶
  3. [码上开学]协程系列视频(扔物线,共3集)
  4. Android 上的 Kotlin 协程
  5. Coroutines guide
  6. 在 Android 应用中使用 Kotlin 协程
  7. 协程简介
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,277评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,689评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,624评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,356评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,402评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,292评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,135评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,992评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,429评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,636评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,785评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,492评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,092评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,723评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,858评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,891评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,713评论 2 354

推荐阅读更多精彩内容