(译)Android中的Kotlin协程-基础

如果英文较好,建议直接阅读原文

译文

什么是协程

基本上,coroutines是轻量级线程,它使得我们可以用串行的方式写出异步的、非阻塞的代码。

Android中如何导入Kotlin协程

根据Kotlin Coroutines Github repo,我们需要导入kotlinx-coroutines-core和kotlinx-coroutines-android(类似于RxJava的io.reactivex.rxjava2:rxandroid,该库支持Android主线程,同时保证未捕获的异常可以在应用崩溃前输出日志)。如果项目里使用了RxJava,可以导入kotlinx-coroutines-rx2来同时使用RxJava和协程,这个库帮助将RxJava代码转为协程。

添加如下代码导入

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:1.3.2"

记得添加最新的Kotlin版本到根build.gradle:

buildscript {
    ext.kotlin_version = '1.3.50'
    repositories {
        jcenter()
        ...
    }
    ...
}

OK,准备工作已就绪,让我们开始吧~

内容目录

  1. 挂起函数(Suspending functions)
  2. 协程作用域 (Coroutine scope)
    (1) 自定义作用域(CoroutineScope)
    (2) 主作用域(MainScope)
    (3) 全局作用域(GlobalScope)
  3. 协程上下文(Coroutine context)
    (1) 调度器(Dispatchers)
    (2) 协程异常处理器(CoroutineExceptionHandler)
    (3) 任务(Job)
    — (3.1) 父-子层级(Parent-child hierarchies)
    — (3.2) SupervisorJob v.s. Job
  4. 协程构建器 (Coroutine builder)
    (1) launch
    (2) async
  5. 协程体(Coroutine body)

协程基础

先看看协程长啥样:

CoroutineScope(Dispatchers.Main + Job()).launch {
  val user = fetchUser() // A suspending function running in the I/O thread.
  updateUser(user) // Updates UI in the main thread.
}

private suspend fun fetchUser(): User = withContext(Dispatchers.IO) {
  // Fetches the data from server and returns user data.
}

这段代码在后台线程拉取服务器数据,然后回到主线程更新UI.

1. 挂起函数(Suspending functions)

挂起函数是Kotlin协程中的特殊函数,用关键字suspend定义。挂起函数可以中断(suspend)当前协程的执行,这意味着它一直等待,直到挂起函数恢复(resume)。因为这篇博客关注协程的基本概念, Android中的Kotlin协程-挂起函数将会讨论更多细节

我们回过头来看看上面的代码,它可以分为4个部分:

suspend functions.png

2. 协程作用域(Coroutine scope)

为新协程定义一个作用域。每个协程构建器都是CoroutineScope的拓展,继承其coroutineContext以自动传递上下文对象和取消。

所有的协程都在协程作用域里运行,并接受一个CoroutineContext(协程上下文,后文详述)作为参数。有几个作用域我们可以使用:

(1) CoroutineScope

用自定义的协程上下文创建作用域。例如,根据我们的需要,指定线程、父job和异常处理器(the thread, parent job and exception handler):

CoroutineScope(Dispatchers.Main + job + exceptionHandler).launch {
    ...
}

(2) MainScope

为UI组件创建一个主作用域。它使用SupervisorJob(),在主线程运行,这意味着如果它的某个子任务(child job)失败了,不会影响其他子任务。

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

(3) GlobalScope

这个作用域不跟任何任务(job)绑定。它用来启动顶级协程,这些协程可以运行在整个的应用生命周期,且永远不能取消。

3. 协程上下文(Coroutine context)

协程总是运行在某个CoroutineContext类型的上下文中。协程上下文是一系列元素,用来指定线程策略、异常处理器、控制协程生命周期等。可以用+操作符将这些元素组合起来。

有3种最重要的协程上下文:调度器,协程异常处理器,任务(Dispatchers,CoroutineExceptionHandler,Job)

(1) 调度器(Dispatchers)

指定协程在哪个线程执行。协程可以随时用withContext()切换线程。

Dispatchers.Default

使用共享的后台线程缓存池。默认情况下,它使用的最大线程数等于CPU内核数,但至少2个。这个线程看起来会像是Thread[DefaultDispatcher-worker-2,5,main].

Dispatchers.IO

跟Dispatchers.Default共享线程,但它数量受kotlinx.coroutines.io.parallelism限制,默认最多是64个线程或CPU内核数(其中的大值)。跟Dispatchers.Default一样,线程看起来像Thread[DefaultDispatcher-worker-1,5,main].

Dispatchers.Main

等效于主线程。线程看起来像Thread[main,5,main].

Dispatchers.Unconfined

未指定特定线程的协程分发器。协程在当前线程执行,并让协程恢复到对应的suspending function用过的任意线程上。

CoroutineScope(Dispatchers.Unconfined).launch {
    // Writes code here running on Main thread.
    
    delay(1_000)
    // Writes code here running on `kotlinx.coroutines.DefaultExecutor`.
    
    withContext(Dispatchers.IO) { ... }
    // Writes code running on I/O thread.
    
    withContext(Dispatchers.Main) { ... }
    // Writes code running on Main thread.
}

(2) CoroutineExceptionHandler

处理未捕获的异常。

一般的未捕获异常只会从launch构建器创建的协程中抛出. async构建器创建的协程总是捕获所有的异常,并在返回的Deferred对象中表示.

例子1:不能通过外层try-catch捕获IOException()。不能用try-catch包围整个协程作用域,否则应用还是会崩溃。

try {
  CoroutineScope(Dispatchers.Main).launch {
    doSomething()
  }
} catch (e: IOException) {
  //  无法捕获IOException()
  Log.d("demo", "try-catch: $e")
}

private suspend fun doSomething() {
  delay(1_000)
  throw IOException()
}

例子2:用CoroutineExceptionHandler捕获IOException()。除CancellationException外的其他异常,如IOException(),将传递给CoroutineExceptionHandler。

// Handles coroutine exception here.
val handler = CoroutineExceptionHandler { _, throwable ->
  Log.d("demo", "handler: $throwable") // Prints "handler: java.io.IOException"
}

CoroutineScope(Dispatchers.Main + handler).launch {
  doSomething()
}

private suspend fun doSomething() {
  delay(1_000)
  throw IOException()
}

例子3:CancellationException()会被忽略。

如果协程抛出CancellationException,它将会被忽略(因为这是取消运行中的协程的预期机制,所以该异常不会传递给CoroutineExceptionHandler)(译注:不会导致崩溃)

// Handles coroutine exception here.
val handler = CoroutineExceptionHandler { _, throwable ->
  // Won't print the log because the exception is "CancellationException()".
  Log.d("demo", "handler: $throwable")
}

CoroutineScope(Dispatchers.Main + handler).launch {
  doSomething()
}

private suspend fun doSomething() {
  delay(1_000)
  throw CancellationException()
}

例子4:用invokeOnCompletion可以获取所有异常信息。

CancellationException不会传递给CoroutineExceptionHandler,但当该异常发生时,如果我们想打印出某些信息,可以使用invokeOnCompletion来获取。

val job = CoroutineScope(Dispatchers.Main).launch {
  doSomething()
}

job.invokeOnCompletion {
    val error = it ?: return@invokeOnCompletion
    // Prints "invokeOnCompletion: java.util.concurrent.CancellationException".
    Log.d("demo", "invokeOnCompletion: $error")
  }
}

private suspend fun doSomething() {
  delay(1_000)
  throw CancellationException()
}

(3) Job

控制协程的生命周期。一个协程有如下状态:

job状态.png

查询job的当前状态很简单,用Job.isActive。

状态流图是:

job状态流图.png
  1. 协程工作时job是active态的
  2. job发生异常时将会变成cancelling. 一个job可以随时用cancel方法取消,这个强制使它立刻变为cancelling态
  3. 当job工作完成时,会变成cancelled态
  4. 父job会维持在completingcancelling态直到所有子job完成。注意completing是一种内部状态,对外部来说,completing态的job仍然是active的。
(3.1) Parent-child hierarchies(父-子层级)

弄明白状态后,我门还必须知道父-子层级是如何工作的。假设我们写了如下代码:

val parentJob = Job()
val childJob1 = CoroutineScope(parentJob).launch {
    val childJob2 = launch { ... }
    val childJob3 = launch { ... }
}

则其父子层级会长这样:

job父子层级.png

我们可以改变父job,像这样:

val parentJob1 = Job()
val parentJob2 = Job()
val childJob1 = CoroutineScope(parentJob1).launch {
    val childJob2 = launch { ... }
    val childJob3 = launch(parentJob2) { ... }
}

则父子层级会长这样:

job父子层级2.png

基于以上知识,我们需要知道如下一些重要概念:

  • 父job取消将立即导致所有子job取消

    val parentJob = Job()
    CoroutineScope(Dispatchers.Main + parentJob).launch {
        val childJob = launch {
            delay(5_000)
            
            // This function won't be executed because its parentJob is 
            // already cancelled after 1 sec. 
            canNOTBeExcecuted()
        }
        launch {
            delay(1_000)
            parentJob.cancel() // Cancels parent job after 1 sec.
        }
    }
    
  • 当某个子job因为除CancellationException外的异常而失败或取消时,会立刻导致所有父job和其他子job取消。但如果是CancellationException,则除该job的子job外的其他jobs不会受到影响。

例子1:如果抛出CancellationException,只有childJob1下的job被取消。

val parentJob = Job()
CoroutineScope(Dispatchers.Main + parentJob).launch {
  val childJob1 = launch {
    val childOfChildJob1 = launch {
      delay(2_000)
      // This function won't be executed since childJob1 is cancelled.
      canNOTBeExecuted()
    }
    delay(1_000)
    
    // Cancel childJob1.
    cancel()
  }

  val childJob2 = launch {
    delay(2_000)
    canDoSomethinghHere()
  }

  delay(3_000)
  canDoSomethinghHere()
}

例子2:如果某个子job抛出IOException,则所有关联job都会被取消

val parentJob = Job()
val handler = CoroutineExceptionHandler { _, throwable ->
  Log.d("demo", "handler: $throwable") // Prints "handler: java.io.IOException"
}

CoroutineScope(Dispatchers.Main + parentJob + handler).launch {
  val childJob1 = launch {
    delay(1_000)
    // Throws any exception "other than CancellationException" after 1 sec.
    throw IOException() 
  }

  val childJob2 = launch {
    delay(2_000)
    // The other child job: this function won't be executed.
    canNOTBExecuted()
  }

  delay(3_000)
  // Parent job: this function won't be executed.
  canNOTBExecuted()
}
  • cancelChildren(): 父job可以取消它的所有子job(递归到它们的子job)而不取消自己。注意:如果一个job已取消,则它不能再作为父job运行协程了。

如果我们用Job.cancel(),父job将会变成cancelled(当前是Cancelling),当其所有子job都cancelled后,父job会成为cancelled态。

val parentJob = Job()
val childJob = CoroutineScope(Dispatchers.Main + parentJob).launch {
  delay(1_000)
  
  // This function won't be executed because its parent is cancelled.
  canNOTBeExecuted()
}

parentJob.cancel()

// Prints "JobImpl{Cancelling}@199d143", parent job status becomes "cancelling".
// And will be "cancelled" after all the child job is cancelled.
Log.d("demo", "$parentJob")

而如果我们用Job.cancelChildren(),父job将会变为Active态,我们仍然可以用它来运行其他协程。

val parentJob = Job()
val childJob = CoroutineScope(Dispatchers.Main + parentJob).launch {
  delay(1_000)
  
  // This function won't be executed because its parent job is cancelled.
  canNOTBeExecuted()
}

// Only children are cancelled, the parent job won't be cancelled.
parentJob.cancelChildren()

// Prints "JobImpl{Active}@199d143", parent job is still active.
Log.d("demo", "$parentJob")

val childJob2 = CoroutineScope(Dispatchers.Main + parentJob).launch {
  delay(1_000)
  
  // Since the parent job is still active, we could use it to run child job 2.
  canDoSomethingHere()
}
(3.2) SupervisorJob v.s. Job

supervisor job的子job可以独立失败,而不影响其他子job。

正如前文提到的,如果我们用Job()作为父job,当某个子job失败时将会导致所有子job取消。

val parentJob = Job()
val handler = CoroutineExceptionHandler { _, _ -> }
val scope = CoroutineScope(Dispatchers.Default + parentJob + handler)
val childJob1 = scope.launch {
    delay(1_000)
    // ChildJob1 fails with the IOException().
    throw IOException()
}

val childJob2 = scope.launch {
    delay(2_000)
    // This line won't be executed due to childJob1 failure.
    canNOTBeExecuted()
}

如果我们使用SupervisorJob()作为父job,则其中一个子job取消时不会影响其他子jobs。

val parentJob = SupervisorJob()
val handler = CoroutineExceptionHandler { _, _ -> }
val scope = CoroutineScope(Dispatchers.Default + parentJob + handler)
val childJob1 = scope.launch {
    delay(1_000)
    // ChildJob1 fails with the IOException().
    throw IOException()
}

val childJob2 = scope.launch {
    delay(2_000)
    // Since we use SupervisorJob() as parent job, the failure of
    // childJob1 won't affect other child jobs. This function will be 
    // executed.
    canDoSomethinghHere()
}

4. 协程构建器(Coroutines Builder)

(1) launch

启动一个新协程,不会阻塞当前线程,返回一个指向当前协程的Job引用。

(2) async and await

async协程构建器是CoroutineScope的拓展方法。它创建一个协程,并以Deferred实现来返回它的未来结果,这是一个非阻塞的可取消future——一个带结果的Job

Async协程搭配await使用:不阻塞当前线程的前提下持续等待结果,并在可延迟的任务完成后恢复(resume),返回结果,或者如果deferred被取消了,抛出相应的异常。

下列代码展示了两个suspending functions的串行调用。在fetchDataFromServerOne()和fetchDataFromServerTwo()中,我们做了一些耗时任务,分别耗时1秒。在launch构建器里调用它们,会发现最终的耗时是它们的和:2秒。

override fun onCreate(savedInstanceState: Bundle?) {
  ...

  val scope = MainScope()
  scope.launch {
    val time = measureTimeMillis {
      val one = fetchDataFromServerOne()
      val two = fetchDataFromServerTwo()
      Log.d("demo", "The sum is ${one + two}")
    }
    Log.d("demo", "Completed in $time ms")
  }
}

private suspend fun fetchDataFromServerOne(): Int {
  Log.d("demo", "fetchDataFromServerOne()")
  delay(1_000)
  return 1
}
  
private suspend fun fetchDataFromServerTwo(): Int {
  Log.d("demo", "fetchDataFromServerTwo()")
  delay(1_000)
  return 2
}

日志是:

2019-12-09 00:00:34.547 D/demo: fetchDataFromServerOne()
2019-12-09 00:00:35.553 D/demo: fetchDataFromServerTwo()
2019-12-09 00:00:36.555 D/demo: The sum is 3
2019-12-09 00:00:36.555 D/demo: Completed in 2008 ms

耗时是两个suspending functions延时的和。该协程在fetchDataFromServerOne()结束前会中断(suspend),然后执行fetchDataFromServerTwo()。

如果我们想同时运行两个方法以减少耗时呢?Async闪亮登场!Async和launch很像。它启动一个可以和其他协程同时运行的新协程,返回Deferred引用——一个带返回值的Job。

public interface Deferred<out T> : Job {
  public suspend fun await(): T
  ...
}

在Deferred上调用await()获取结果,例如:

override fun onCreate(savedInstanceState: Bundle?) {
  ...
  
  val scope = MainScope()
  scope.launch {
    val time = measureTimeMillis {
      val one = async { fetchDataFromServerOne() }
      val two = async { fetchDataFromServerTwo() }
      Log.d("demo", "The sum is ${one.await() + two.await()}")
    }
    
    // Function one and two will run asynchrously,
    // so the time cost will be around 1 sec only. 
    Log.d("demo", "Completed in $time ms")
  }
}

private suspend fun fetchDataFromServerOne(): Int {
  Log.d("demo", "fetchDataFromServerOne()")
  delay(1_000)
  return 1
}

private suspend fun fetchDataFromServerTwo(): Int {
  Log.d("demo", "fetchDataFromServerTwo()")
  Thread.sleep(1_000)
  return 2
}

日志是:

2019-12-08 23:52:01.714 D/demo: fetchDataFromServerOne()
2019-12-08 23:52:01.718 D/demo: fetchDataFromServerTwo()
2019-12-08 23:52:02.722 D/demo: The sum is 3
2019-12-08 23:52:02.722 D/demo: Completed in 1133 ms

5. 协程体(Coroutine body)

在CoroutineScope中运行的代码,包括常规函数或挂起函数——挂起函数在结束前会中断协程,下篇博客将会详述。

今天就到这里啦。下篇博客将会深入介绍挂起函数及其用法。 Android中的Kotlin协程-挂起函数.

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

推荐阅读更多精彩内容