Kotlin 协程学习总结

Kotlin 基础精华篇
Kotlin 内联函数let、with、run、apply、also
Kotlin 协程学习总结


一、协程的使用与说明

        val job = GlobalScope.launch(
            context = Dispatchers.Default,
            start = CoroutineStart.DEFAULT,
            block = {
                val result1 = doTask("1", 2000)
                val result2 = async { doTask("1", 2000) }
                withContext(Dispatchers.Main) {
                    logMessage("result is ${result1}, ${result2.await()}")
                }
            }
        )

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
}
1、CoroutineScope

协程范围,即协程内的代码运行的时间周期范围,如果超出了指定的协程范围,协程会被取消执行。

CoroutineScope
协程范围接口
CoroutineContext:协程的上下文

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
}

ContextScope
协程范围实现类

internal class ContextScope(context: CoroutineContext) : CoroutineScope

GlobalScope
全局作用域,实现了 CoroutineScope接口 同时执行了一个空的上下文对象的协程作用域。
其创建的协程没有父协程,通常用于启动顶级协程,这些协程在整个应用程序生命周期内运行。
直接使用 GlobalScope 的 async 或者 launch 方法是强烈不建议的。程序代码通常应该使用自定义的协程作用域。

MainScope
实现了 CoroutineScope接口 同时是通过调度器调度到了主线程的协程作用域

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

如:

class TestLifecycleActivity : AppCompatActivity(), CoroutineScope by MainScope() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.test_fargment)
        testCoroutine()
    }

    private fun testCoroutine() {
        logMessage("launchBefore")
        launch {
            //do task
            logMessage("taskBefore")
            val result1 = async { doTask("1", 20 * 1000) }
            val result2 = async { doTask("2", 20 * 1000) }
            val result3 = doTask("3", result1.await() + result2.await())
            logMessage("taskAfter")

            //切换主线程输出结果
            withContext(Dispatchers.Main) {
                logMessage("result is ${result1.await()}, ${result2.await()}, $result3")
            }
        }
        logMessage("launchAfter")
    }

    override fun onDestroy() {
        super.onDestroy()
        logMessage("onDestroy - cancel")
        cancel()
    }

    //...
}
日志输出:
    E/Coroutine: 16:49:50 983 *** ThreadName : main *** launchBefore
    E/Coroutine: 16:49:50 994 *** ThreadName : main *** launchAfter
    E/Coroutine: 16:49:51 243 *** ThreadName : main *** taskBefore
    E/Coroutine: 16:50:02 029 *** ThreadName : main *** onDestroy - cancel

自定义CoroutineScope

private val customScope = CoroutineScope(Dispatchers.IO + SupervisorJob())

viewModelScope
添加如下依赖:

def lifecycle_version = "2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"

使用:

class TestViewModel : androidx.lifecycle.ViewModel() {
    fun testCoroutine() {
        viewModelScope.launch {
            //do task
        }
    }
}

而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]
 */
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))
        }

lifecycleScope
添加如下依赖:

def lifecycle_version = "2.2.0"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"

使用:

        lifecycleScope.launch {  }
        lifecycleScope.launchWhenCreated {  }
        lifecycleScope.launchWhenResumed {  }
        lifecycleScope.launchWhenStarted {  }

viewModelScope是怎么定义的呢


/**
 * [CoroutineScope] tied to this [LifecycleOwner]'s [Lifecycle].
 *
 * This scope will be cancelled when the [Lifecycle] is destroyed.
 *
 * This scope is bound to
 * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate].
 */
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]
 */
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
            }
        }
    }
2、launch

是CoroutineScope的一个扩展函数。
默认启动通过launch启动一个协程的时候包含一个继承自作用域的CoroutineContext,和一个默认的启动模式,调度器和要执行的协程体,之后返回一个Job。同时内部的job将成为外部job 的子job,当一个父协程被取消的时候,所有它的子协程也会被递归的取消。

lauch 与 runBlocking 都能 开启一个协程,但 lauch 是非阻塞的,runBlocking 是阻塞的。

runBlocking中调用launch()会在当前线程中执行协程,也就是说在runBlocking中不管开启多少个子协程,实际上它们都是使用runBlocking所在的线程执行任务,所以会出现线程被霸占的情况。

        runBlocking {
            GlobalScope.launch {
                //do task
                logMessage("taskBefore")
                val result1 = async { doTask("1", 1000) }
                val result2 = async { doTask("2", 1000) }
                val result3 = doTask("3", result1.await() + result2.await())
                logMessage("taskAfter")
            }
        }
日志输出:(都在main线程顺序执行)
    E/Coroutine: 19:42:37 140 *** ThreadName : main *** launchBefore
    E/Coroutine: 19:42:37 157 *** ThreadName : main *** taskBefore
    E/Coroutine: 19:42:38 164 *** ThreadName : main *** task1 - 1000
    E/Coroutine: 19:42:38 166 *** ThreadName : main *** task2 - 1000
    E/Coroutine: 19:42:40 168 *** ThreadName : main *** task3 - 2000
    E/Coroutine: 19:42:40 168 *** ThreadName : main *** taskAfter
3、CoroutineContext

协程上下文,可以指定协程运行的线程。
默认与指定的CoroutineScope中的coroutineContext保持一致,比如GlobalScope默认运行在一个后台工作线程内。也可以通过显示指定参数来更改协程运行的线程,Dispatchers提供了几个值可以指定:Dispatchers.Default、Dispatchers.Main、Dispatchers.IO、Dispatchers.Unconfined。

Dispatchers.Default
CPU密集型任务,如列表排序、JSON转换等

Dispatchers.Main
主线程,和UI交互,执行轻量任务

Dispatchers.IO
常用于网络请求和文件访问

Dispatchers.Unconfined
不限制任何制定线程,一般不用

4、CoroutineStart

协程的启动模式。
默认CoroutineStart.DEFAULT是指协程立即执行,除此之外还有CoroutineStart.LAZY、CoroutineStart.ATOMIC、CoroutineStart.UNDISPATCHED。

CoroutineStart.DEFAULT
立即执行协程体

CoroutineStart.LAZY
懒汉式启动。launch后并不会有任何调度行为,协程体也不会进入执行状态,直到需要它执行的时候,即需要它的运行结果的时候,launch调用后会返回一个 Job实例:
调用 Job.start,主动触发协程的调度执行
调用 Job.join,隐式的触发协程的调度执行

CoroutineStart.ATOMIC
立即执行协程体,但在开始运行后无法取消,无视 job.cancel()

CoroutineStart.UNDISPATCHED
立即在当前线程执行协程体,直到第一个 suspend 调用。
协程在这种模式下会直接开始在当前线程下执行,直到第一个挂起点,这听起来有点儿像前面的 ATOMIC,不同之处在于 UNDISPATCHED 不经过任何调度器即开始执行协程体。当然遇到挂起点之后的执行就取决于挂起点本身的逻辑以及上下文当中的调度器了。

5、block

协程主体,也就是要在协程内部运行的代码。

6、Job

返回值,对当前创建的协程的引用。可以通过Job的start、cancel、join等方法来控制协程的启动和取消。

isActive 协程是否存活(注意懒启动)
isCancelled 协程是否取消
isCompleted 协程是否完成
cancel() 取消协程
start() 启动协程
join() 阻塞等候协程完成
cancelAndJoin() 取消并等候协程完成handler: CompletionHandler) 监听协程的状态回调
attachChild(child: ChildJob) 附加一个子协程到当前协程上
7、async、await

是CoroutineScope的一个扩展函数,用于开启一个新的子协程,与 launch 函数一样可以设置启动模式,不同的是它的返回值为 Deferred,Deferred是Job的子类,但是通过Deferred.await()可以得到一个返回值。

await() 只有在 async 未执行完成返回结果时,才会挂起协程。若 async 已经有结果了,await() 则直接获取其结果并赋值给变量,此时不会挂起协程。

8、suspend

suspend fun 挂起函数,即该函数是一个耗时操作,须放在协程中执行。

挂起函数只能在协程中和其他挂起函数中调用,不能在其他地方使用。

suspend函数会将整个协程挂起,而不仅仅是这个suspend函数,也就是说一个协程中有多个挂起函数时,它们是顺序执行的。

9、withContext

withContext 与 async 都可以返回耗时任务的执行结果。 通常也会使用withContext实现线程切换。

一般来说,多个 withContext 任务是串行的, 且withContext 可直接返回耗时任务的结果。 多个 async 任务是并行的,async 返回的是一个Deferred<T>,需要调用其await()方法获取结果。

二、协程的执行测试

两个辅助方法

    private suspend fun doTask(msg: String, delayTime: Long = 0) =
        (if (delayTime < 1000) (1000 + Math.random() * 1000).toLong() else delayTime).apply {
            //延时
            delay(this)
            logMessage("task$msg - $this")
        }

    private fun logMessage(msg: String) {
        currentTime.time = System.currentTimeMillis()
        //日志输出当前时间、线程名、自定义信息
        Log.e(
            "Coroutine",
            "${dateFormat.format(currentTime)} *** ThreadName : ${Thread.currentThread().name} *** $msg"
        )
    }
1、顺序执行:task3依赖task2的结果,task2依赖task1的结果
    private fun testCoroutine() {
        logMessage("launchBefore")
        GlobalScope.launch {
            //do task
            logMessage("taskBefore")
            val result1 = doTask("1")
            val result2 = doTask("2", result1 - 1000)
            val result3 = doTask("3", result2 - 1000)
            logMessage("taskAfter")
            //输出结果
            withContext(Dispatchers.Main) {
                logMessage("result is ${result1}, ${result2}, $result3")
            }
        }
        logMessage("launchAfter")
    }
日志输出:
    E/Coroutine: 11:26:57 530 *** ThreadName : main *** launchBefore
    E/Coroutine: 11:26:57 590 *** ThreadName : main *** launchAfter
    E/Coroutine: 11:26:57 591 *** ThreadName : DefaultDispatcher-worker-1 *** taskBefore
    E/Coroutine: 11:26:59 475 *** ThreadName : DefaultDispatcher-worker-1 *** task1 - 1876
    E/Coroutine: 11:27:01 413 *** ThreadName : DefaultDispatcher-worker-1 *** task2 - 1936
    E/Coroutine: 11:27:03 386 *** ThreadName : DefaultDispatcher-worker-1 *** task3 - 1971
    E/Coroutine: 11:27:03 386 *** ThreadName : DefaultDispatcher-worker-1 *** taskAfter
    E/Coroutine: 11:27:03 394 *** ThreadName : main *** result is 1876, 1936, 1971

由以上日志输出可知:

  1. 协程 launch 中执行耗时任务没有阻塞主线程(launchAfter先于协程输出)
  2. task1、task2、task3 顺序执行 (执行时间时间线)
2、异步执行:task3依赖task1、task2的结果,task1、task2异步执行
    private fun testCoroutine() {
        logMessage("launchBefore")
        GlobalScope.launch {
            //do task
            logMessage("taskBefore")
            val result1 = async { doTask("1", 2000) }
            val result2 = async { doTask("2", 2000) }
            val result3 = doTask("3", result1.await() + result2.await())
            logMessage("taskAfter")
            //输出结果
            withContext(Dispatchers.Main) {
                logMessage("result is ${result1.await()}, ${result2.await()}, $result3")
            }
        }
        logMessage("launchAfter")
    }
日志输出:
    E/Coroutine: 12:22:50 202 *** ThreadName : main *** launchBefore
    E/Coroutine: 12:22:50 264 *** ThreadName : main *** launchAfter
    E/Coroutine: 12:22:50 266 *** ThreadName : DefaultDispatcher-worker-1 *** taskBefore
    E/Coroutine: 12:22:52 279 *** ThreadName : DefaultDispatcher-worker-2 *** task1 - 2000
    E/Coroutine: 12:22:52 279 *** ThreadName : DefaultDispatcher-worker-3 *** task2 - 2000
    E/Coroutine: 12:22:56 282 *** ThreadName : DefaultDispatcher-worker-2 *** task3 - 4000
    E/Coroutine: 12:22:56 283 *** ThreadName : DefaultDispatcher-worker-2 *** taskAfter
    E/Coroutine: 12:22:56 290 *** ThreadName : main *** result is 2000, 2000, 4000

async { } 异步执行
await() 获取异步执行结果



更多资料参考:
将 Kotlin 协程与架构组件一起使用
超长文,带你全面了解Kotlin的协程
Kotlin协程核心库分析
Kotlin实战指南十四:协程启动模式

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