Kotlin协程和在Android中的使用总结(一 基础知识)

在这里插入图片描述
在学习kotlin的协程时,看了很多博客,在此做个汇总,其中会大量摘抄官网和其他博文内容,不喜勿喷,仅作为总结使用。
该总结系列包含多篇,从介绍协程开始,到如何使用,到与RxJava的对比,到如何将现有代码转向协程形式,以及现有的协程三方库(依赖注入、图片加载、权限请求等)。

0 协程是什么

官网https://www.kotlincn.net/docs/reference/coroutines/coroutines-guide.html
官网有关于协程的简单介绍,可以简单的理解成是对Java的线程做了一个封装(使用了CPS+状态机来完成协程的挂起和恢复),使开发者更加方便的控制线程的调度。

取自Bennyhuo大佬的破解 Kotlin 协程 番外篇(2) - 协程的几类常见的实现中的一些介绍:

因而协程的实现也按照是否开辟相应的调用栈存在以下两种类型:

  • 有栈协程 Stackful Coroutine:每一个协程都会有自己的调用栈,有点儿类似于线程的调用栈,这种情况下的协程实现其实很大程度上接近线程,主要不同体现在调度上。
  • 无栈协程 Stackless Coroutine:协程没有自己的调用栈,挂起点的状态通过状态机或者闭包等语法来实现。

Kotlin 的协程是一种无栈协程的实现,它的控制流转依靠对协程体本身编译生成的状态机的状态流转来实现,变量保存也是通过闭包语法来实现的

所以kotlin的协程也会被称为伪协程,因为它没有对应到操作系统中的特有调用栈。

也可以参考以下文章:
Kotlin Coroutines(协程) 完全解析(一),协程简介
Kotlin Coroutines(协程) 完全解析(二),深入理解协程的挂起、恢复与调度
破解 Kotlin 协程 番外篇(2) - 协程的几类常见的实现

使用场景

在官网的利用 Kotlin 协程提升应用性能一文中,介绍了使用协程的场景,

在 Android 平台上,协程有助于解决两个主要问题:

  • 管理长时间运行的任务,如果管理不当,这些任务可能会阻塞主线程并导致您的应用冻结。
  • 提供主线程安全性,或者从主线程安全地调用网络或磁盘操作。(也就是说方法的调用方无需知道该方法会对当前的主线程造成什么影响,不用考虑是否耗时,是否会产生异常以及如何处理异常,这一切都有方法的编写者负责完成。)

1 在Android中引入协程

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”

  • kotlinx-coroutines-core提供协程的基础API
  • kotlinx-coroutines-android提供一个Android主线程(类似于在使用RxJava时要使用一个io.reactivex.rxjava2:rxandroid
  • kotlinx-coroutines-rx2提供和RxJava一起使用的支持

2 一个协程示例

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

在上面的代码中,关键字CoroutineScope表示一个协程作用域,任何挂起函数都要在一个作用域内执行。
Dispatchers.Main + Job()表示一个协程上下文环境CoroutineContext,包含用于指定协程调度器(即在哪个线程上Resume恢复协程)和一个父协程Job,以及一个异常处理逻辑exception handler。
launch表示一个协程构建器,括号内的则是协程体(包含挂起函数和普通函数)。

3 CoroutineScope的种类

(1)CoroutineScope

如图上面的代码一样,这个协程作用域主要用于使用一个自定义的协程上下文CoroutineContext来构建Scope,在上文中有说明。

(2)MainScope

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

通过上面MainScope代码的定义,可以看出,它使用的是Dispatchers.Main,即协程会在主线程上运行,SupervisorJob()指定该协程体内的任意一个子Job(可以理解成一个协程任务)执行失败的时候,不会影响到其他的子Job。

(2)GlobalScope

通过该Scope创建的顶级协程不会跟任何其他Job有关,其生命周期跟随整个应用的生命周期,不会被过早的取消掉,所以通常这个Scope要谨慎使用,以免造成资源和内存占用。

4 协程上下文CoroutineContext

CoroutineContext主要包含三个主要的元素:调度器Dispatchers, 异常处理逻辑CoroutineExceptionHandler 和一个父协程任务 Job.

(1)调度器Dispatchers

Dispatchers用于指定当前的协程体 在哪个线程上执行,主要有以下几类:

  • Dispatchers.Default
    使用一个共享的线程池,最大线程数为CPU核数,最小为2,名字样式为Thread[DefaultDispatcher-worker-2,5,main],适用于CPU密集型的任务使用。
  • Dispatchers.IO
    与Dispatchers.Default共享线程,但是线程数受kotlinx.coroutines.io.parallelism的限制,它默认为64个线程或内核数(以较大者为准)的限制。适用于IO密集型的任务。
  • Dispatchers.Main
    即Android的主线程,线程名为Thread[main,5,main]
  • Dispatchers.Unconfined
    不局限于任何特定线程的协程调度程序。 协程首先在当前线程中执行,并让协程在相应的挂起函数使用的任何线程中恢复。
    一个使用Dispatchers.Unconfined的例子,注释说明其中各个协程体部分会在哪个线程执行。
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 {
  CoroutineScope(Dispatchers.Main).launch {
    doSomething()
  }
} catch (e: IOException) {
  // Cannot catch IOException() here.
  Log.d("demo", "try-catch: $e")
}

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

在上面的代码中,在整个协程体外面包上try catch是不起作用的,应用仍然会crash。

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

在上面的代码中,使用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()
}

在上面的代码中,如果协程体抛出了CancellationException,那么CoroutineExceptionHandler也无法捕获到,因为抛出CancellationException是正常的终止协程机制。


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

在上面的代码中,使用Job对象的invokeOnCompletion方法可以捕获所有异常,包括CancellationException

5 Job v.s. SupervisorJob

通过使用父子协程,可以实现结构化的流程控制。

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

上面代码中指定的父子协程关系如下图所示:
在这里插入图片描述
其中取消了父协程,则其所有的子协程会被立即取消:

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

⚠️注意:
如果一个子协程抛出了除CancellationException之外的异常,则其父协程和所有子协程都会被取消。
父协程可以使用cancelChildren()来取消其所有子协程,而不会取消自身协程体
如果一个Job被取消了,那么不可以再用来作为一个父Job使用

Job有多种状态,我们可以使用Job.isActive来判断当前协程是否仍处于Active状态。
在这里插入图片描述
如果父协程使用SupervisorJob来指定,那么任何一个子协程发生异常,都不会影响其他子协程的执行。如下代码所示:

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

6 协程构建器

launch和async是协程自带的两个构建器,分别用于同步和异步执行协程体。
下面代码中同步执行了两个挂起函数:

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
}

log输出如下:

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

下面代码中使用async异步执行了两个挂起函数:

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
}

log输出如下:

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

除上述两个构建器之外,还有一个liveData构建器,用于在ViewModel中使用,见Kotlin协程和在Android中的使用总结(二 和Jetpack架构组件一起使用)

参考:
Kotlin Coroutines in Android — Basics
官网协程文档:Coroutine Basics
官网协程Codelabs: Using Kotlin Coroutines in your Android App

发布了82 篇原创文章 · 获赞 86 · 访问量 11万+

猜你喜欢

转载自blog.csdn.net/unicorn97/article/details/105150935