内容简介:这是由于 Kotlin 语言创立之初就考虑了要和 Java 语言可以互相调用(现在也支持 JavaScript 了),所以 Kotlin 语言中的异步 API Coroutine 的设计就比较简单并且可以比较简便的配合其他语言中的异步 API 一起使用。 本文主要介绍 Coroutine 的一些基本概念以及在 Android 应用中的使用。Kotlin Coroutine 有很多模块,不同的模块应用在不同的环境下,而要在 Android 应用中使用 Coroutine 则需要在项目中添加两个基础的依赖模块:
这是 掌握Kotlin Coroutine 系列文章第一节内容。之所以取名为 掌握Kotlin Coroutine 而不是 精通 是由于 Coroutine 是一个新的特性,也刚刚正式发布没多久,我也并没有经常使用过,所以有些概念或者背后的原理我自己也不是很了解,或者理解的不是很正确。本系列文章只是总结了自己学习过程中对 Coroutine 的理解,整理成文方便自己梳理一下对 Coroutine 的了解,所以文中不可避免会出现一些理解不准确的地方,欢迎大家在公众号(ID: yunzaiqianfeng )留言交流。
由于 Kotlin 语言创立之初就考虑了要和 Java 语言可以互相调用(现在也支持 JavaScript 了),所以 Kotlin 语言中的异步 API Coroutine 的设计就比较简单并且可以比较简便的配合其他语言中的异步 API 一起使用。 本文主要介绍 Coroutine 的一些基本概念以及在 Android 应用中的使用。
在项目中引用 Coroutine 库
Kotlin Coroutine 有很多模块,不同的模块应用在不同的环境下,而要在 Android 应用中使用 Coroutine 则需要在项目中添加两个基础的依赖模块:
<br />dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1' }
小提示:代码可以左右拖动查看,下同!
kotlinx-coroutines-core 为 Coroutine 的核心 API, 而kotlinx-coroutines-android 为安卓平台的一些提供了一些支持,特别是提供了 Dispatchers.Main 这个 UI Dispatcher。
如果项目使用了 ProGuard 而不是 R8 来混淆代码的话, 还需要添加下面的 ProGuard 配置:
# ServiceLoader support -keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {} -keepnames class kotlinx.coroutines.CoroutineExceptionHandler {} -keepnames class kotlinx.coroutines.android.AndroidExceptionPreHandler {} -keepnames class kotlinx.coroutines.android.AndroidDispatcherFactory {} # Most of volatile fields are updated with AFU and should not be mangled -keepclassmembernames class kotlinx.** { volatile <fields>; }
需要注意的是, Coroutine 在 18年10月底正式发布 1.0.0 版本,该版本需要和 Kotlin 1.3 版本一起使用。而最新的 Coroutine 版本为 1.1.1, 对应的 Kotlin 版本为 1.3.20。 所以需要记得在项目的 Gradle 配置文件中设置 Kotlin 语言的版本号:ext.kotlin_version = ‘1.3.20’。
初窥 Coroutine
在介绍 Coroutine 概念之前先来看一个简单的示例,通过代码体验一下 Coroutine 的基本用法以及和 Java(Android) 异步处理的不同之处。
通过 Android Studio 创建一个新的项目,选择 Kotlin 语言和 AndroidX 库,然后在项目中按照上面的方式来添加 Coroutine 依赖库即可动手开始体验 Coroutine 了。
@ObsoleteCoroutinesApi val BG = newSingleThreadContext("Background") @ObsoleteCoroutinesApi class MainActivity : AppCompatActivity() { lateinit var job: Job; override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) setSupportActionBar(toolbar) GlobalScope.launch { Log.d(TAG, "no dispatcher coroutine ${Thread.currentThread().name}") } Log.d(TAG, "onCreate: ${Thread.currentThread().name}") job = GlobalScope.launch(Dispatchers.Main) { Log.d(TAG, "in coroutine a: ${Thread.currentThread().name}") delay(10000) background() delay(10000) Log.d(TAG, "in coroutine b: ${Thread.currentThread().name}") textView.text = Date().toLocaleString() } textView.text = "Main" Log.d(TAG, "onCreate after launch ${Thread.currentThread().name}") fab.setOnClickListener { view -> job.cancel() Snackbar.make(view, "cancel job ${job.isCancelled}", Snackbar.LENGTH_LONG).show() } } suspend fun background() { withContext(BG) { Log.d(TAG, "background() called ${Thread.currentThread().name}") } } }
上面是在 Activity 的 onCreate 函数中通过 launch
这个函数来创建了两个 Coroutine,其中第二个 Coroutine 指定了 Dispatchers.Main
这个 Context ,所以在第二个 Coroutine 中可以直接访问 textView 并设置其文本内容为当前日期。
运行上面的代码,在打开的界面上会先显示 “Main” 文字,然后20秒后显示当前日期和时间。 查看 LogCat 显示如下的 Log 信息:
03-02 07:40:13.672 D/MainActivity: onCreate: main 03-02 07:40:13.672 D/MainActivity: onCreate after launch main 03-02 07:40:13.673 D/MainActivity: no dispatcher coroutine DefaultDispatcher-worker-1 03-02 07:40:13.684 D/MainActivity: in coroutine a: main 03-02 07:40:23.686 D/MainActivity: background() called Background 03-02 07:40:33.688 D/MainActivity: in coroutine b: main
第一、二行 Log 显示在 main
线程执行;第三行 Log 显示的是第一个 Coroutine 在 DefaultDispatcher-worker-1
线程中执行,这个线程为 Coroutine 默认的线程;第三、五行 Log 显示是在 main
线程执行,这是因为这个 Coroutine 使用了 Dispatchers.Main
这个 context, 而第四行 log 显示 background 这个函数是在 Background
这个线程执行, Background
线程是通过 val BG = newSingleThreadContext("Background")
来创建的,在定义 background()
函数的时候,使用 withContext(BG)
来限定里面的代码执行的 context 为 BG
。
另外上面的代码使用了 delay
函数来延迟了 Coroutine 的执行,注意看最后三行 Log 的时间,分别为 40:13、40:23、40:33. 说明 launch
里面的代码被阻塞了 10秒,并且这个 launch
里面的代码是在 main 线程执行的,为啥没有导致 ANR 呢? 我们先记住这些问题,下面来介绍 Coroutine 的一些概念。
Coroutine 基本概念
先来看一组 Coroutine 中的一些术语。
coroutine: coroutine 是一个 suspendable computation
实例。 suspendable computation
翻译过来就是可以被暂停的计算逻辑,类似于 Java 中线程的概念,需要执行一个代码块并且具有和线程类似的生命周期(创建状态、开始状态),和线程不同的是 Coroutine 并没有和具体的某一个线程绑定到一起,Coroutine 要比线程轻量级多了,消耗的资源也少多了。多个 Coroutine 可以运行在同一个线程中。Coroutine 可以在一个线程上被暂停执行(Suspend),然后在另外一个线程恢复执行(Resume)。并且还可以具有 Future
或者 Promise
的特性 — 在执行完成的时候返回一个结果。
coroutine builder: coroutine builder
是一个用来创建一个 coroutine 实例的函数。Kotlin 定义了一些基础的建构 Coroutine 的函数,比如 launch{}
、 future{}
、 sequence{}
等。使用这些基础的建构函数可以构造其他 Coroutine 实例。
suspending function: suspending function
是一个带有 suspend
修饰符的函数,比如上面例子中的 background()
函数。 这种函数可以在不阻塞当前线程的情况下调用其他耗时较长的 suspending function
。 suspending function
只能在 Coroutine 里面或者其他 suspending function
中被调用, 在普通的 Kotlin 代码中无法调用。标准库中提供了一些基础的 suspending function
,比如上面用到的 delay()
函数。在 Android Studio 中 suspending function
旁边会有一个特殊的标记符号,告诉开发者这是一个 suspending function
。如下图:
suspending lambda: 普通的函数有 lambda
表达式形式,所以 suspending function
也有 lambda
表达式形式,和普通的表达式相比, suspending lambda
只是多了一个 suspend
标识符。比如系统 launch
函数的最后一个参数就是 suspending lambda
:
`` public fun CoroutineScope.launch( context: CoroutineContext = EmptyCoroutineContext, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> Unit ): Job { //... } ```
注意看上面 launch 函数第三个参数 block 的类型为 suspend CoroutineScope.() -> Unit,这个类型被称之为 suspending function
类型。
suspending function type: suspending function 类型
就是一个标识 suspending 函数和 lambda 的函数类型,如上所示,使用 suspend 标识符。再比如 suspend () -> Int
是一个 suspending
函数类型 这个函数没有参数返回 Int 类型的值。 而 suspend fun foo(): Int
函数就是一个符合这个类型定义的函数。
由于在 Kotlin 中函数也是一个类型定义,所以有普通的函数类型和 suspending 函数类型。
suspension point: suspension point
是 Coroutine 执行过程中遇到的可以被暂停执行的地方。通常而言, suspension point
是调用 suspending function
的地方,但是只有当 suspending function
调用标准库提供的用来暂停 Coroutine 执行的函数时,Coroutine 才会真正的停止执行。 比如 delay()
就是标准库提供的一个暂停当前 Coroutine 一段时间的函数。
continuation: continuation
代表 Coroutine 在 suspension point
被暂停时的状态。
下面根据上面的示例代码来和上面的概念做一下对应:
job = GlobalScope.launch(Dispatchers.Main) { Log.d(TAG, "in coroutine a: ${Thread.currentThread().name}") delay(10000) background() delay(10000) Log.d(TAG, "in coroutine b: ${Thread.currentThread().name}") textView.text = Date().toLocaleString() }
上面的代码中, GlobalScope.launch
为 coroutine builder
,该代码创建了一个 Coroutine 实例 job
,通过 job
可以查看当前 Coroutine 执行的状态并且可以控制 Coroutine,比如可以通过 job.cancel()
来取消该 Coroutine 的执行,和取消线程的执行概念类似。 launch
函数有三个参数,其中第三个参数为 suspending function type
函数类型,所以上面示例中 launch 的第三个参数的代码块为 suspending lambda
, delay()
函数为 Coroutine 标准库中提供的一个可以用来暂停当前线程执行的函数,所以当执行到 delay()
的时候,这个 Coroutine 就被暂停了,这个时候就是 suspension point
,当 Coroutine 从 delay()
返回的时候, 继续恢复执行 background()
这个自定义的 suspending function
,在 background()
这个函数里面使用 withContext()
函数来创建一个新的 子 Coroutine ,并且用其提供的 context 来执行所定义的代码块( suspending lambda
)。
所以综合来看,上面的代码在 UI 线程创建了一个在 Dispatchers.Main
线程(也是UI线程)执行的 Coroutine,Coroutine 并不会阻塞当前线程,所以当 Coroutine 里面的代码在被暂停执行的时候, UI 线程也不会被阻塞,而当 Coroutine 恢复执行的时候, 代码继续执行。 看起来是不是和 Java 里面的线程、Android 里面的 AsyncTask 等概念很类似呢? 只不过 Coroutine 的代码看起来更加简洁(当然了,一部分归功于 lambda 语法),并且当从其他线程获取返回值的时候可以避免回调函数嵌套的问题。
本节使用一个示例代码来介绍了 Coroutine 的各种基本概念和基本用法,后续章节将继续深入介绍 Coroutine 的方方面面。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- [译] 重温一下 JS 进阶需要掌握的 13 个概念
- 视频 | 冰箱学习法:教你 30 分钟掌握 K8s 核心概念
- 聊聊你可能并没有完全掌握的 Flex 布局:从概念入手,丝丝入扣
- 掌握面向对象编程本质,彻底掌握OOP
- 如何入门掌握Nginx?
- 掌握JMH 原 荐
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。