Kotlinコルーチンの基本的な使用法
Kotlinコルーチンシリーズ:
- コルーチンの基本的な使用法(この記事)
- コルーチンのコンテキスト理解
- コルーチンスコープ管理
- コルーチンの一般的な高度な使用
実際、インターネット上にはすでにたくさんKotlin协程
のます。ここでは、自分の要約を記録するためにチュートリアルを公開します。それは私たち自身の理解と成果でもあります。
コルーチンの概念は比較的大きなものなので、ここではいくつかの異なるモジュールに分けて説明します。この記事はシリーズの創刊号です。このシリーズは、コルーチンの各モジュールの実際の使用法についてです。原理とソースコードが含まれています。この記事では、コルーチンの基本的な使用法について説明します。
ここでコルーチンの接頭辞を付けたことがわかります。 Kotlin协程
、、およびシリーズの後半で参照されるコルーチンはすべてKotlin协程
です。Kotlinコルーチンが他の言語のコルーチンとは異なる方法で実装されていることはすでによく知られています。ここではあまり紹介しませんが、わからない場合はこちらをご覧ください。
1.コルーチンを使用する理由
Android開発ではJava言語を使用し、Javaのスレッド管理を使用しThread
ます。私たちはAndroid開発で多かれ少なかれそれを使用しましたThread
。
ただし、メインスレッドでUIを更新する必要があります。サブスレッドThread
で ApirunOnUiThread
を呼び出してメインスレッドに切り替え、Kotlin構文を含むUIを更新する必要があります。これは使用するルーチンと同じです。スレッドの拡張方式はありますが、内部処理フローはJavaと同じです。
擬似コードは次のとおりです。
thread {
dosth()
runOnUiThread {
updateUI()
}
}
ロジックが多すぎる場合、またはスレッドの切り替えが頻繁に行われる場合は、Kotlinのシンタックスシュガーでさえ何度もネストさRxJava
れるます。
而得益于Kotlin语言的设计,在1.3版本加入了协程的概念,后期又出了一些Jetpack的组件,天然支持协程,使得协程的概念越来越为人熟知,更多的人开始使用协程了。而我们也就无需使用 RxJava
等第三方框架来管理线程了。
协程的特点:
- 轻量:您可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作。
- 更少的内存泄漏:使用结构化并发机制在一个作用域内执行多项操作
- 支持取消:取消操作会自动在运行中的整个协程层次结构内传播。
- Jetpack 集成:许多 Jetpack 库都包含提供全面协程支持的扩展。某些库还提供自己的协程作用域,可供您用于结构化并发
二、怎么使用协程
Kotlin语法是默认不带协程的,如果我们想使用协程还是需要引入协程框架库。注意需要Kotlin版本1.3以上。这里我使用的Kotlin版本为1.4.21。
协程库的引入:
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
我们就能使用一个简单的协程:
GlobalScope.launch {
YYLogUtils.w("执行在协程中...")
delay(1000L)
YYLogUtils.w("执行完毕...")
}
Kotlin中,有几种方式能够启动协程,如下所示:
-
launch{} CoroutineScope的扩展方法,启动一个协程,不阻塞当前协程,并返回新协程的Job。
-
async{} CoroutineScope的扩展方法,启动一个协程,不阻塞当前协程,返回一个Deffer,除包装了未来的结果外,其余特性与launch{}一致
-
runBlocking{} 是一个裸方法,创建一个协程,并阻塞当前线程,直到协程执行完毕。前面说过,这里不再赘述。
-
withContext(){} 一个suspend方法,在给定的上下文执行给定挂起块并返回结果,它并不启动协程,只会(可能会)导致线程的切换。用它执行的挂起块中的上下文是当前协程的上下文和由它执行的上下文的合并结果。 withContext的目的不在于启动子协程,它最初用于将长耗时操作从UI线程切走,完事再切回来。
-
coroutineScope{} 一个suspend方法,创建一个新的作用域,并在该作用域内执行指定代码块,它并不启动协程。其存在的目的是进行符合结构化并发的并行分解(即,将长耗时任务拆分为并发的多个短耗时任务,并等待所有并发任务完成后再返回)。
可以看到只有前三种方法是创建或启动一个协程的,后面那种方式都是切换线程,或者创建作用域的一个方法。
我们举例说明一下
coroutineScope { } //报错 ,不能直接用,只能在协程里面使用
runBlocking {
coroutineScope { } //正常使用,因为runBlocking创建了协程
YYLogUtils.w("执行在协程中...")
delay(1000L)
YYLogUtils.w("执行完毕...")
}
runBlocking {
coroutineScope { //这里包一层也没什么,只是多了一层代码块而已, 不影响逻辑
YYLogUtils.w("执行在协程中...")
delay(1000L)
YYLogUtils.w("执行完毕...")
}
}
源码查看
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
launch返回的是Job对象,用于控制协程的生命周期
异步的启动
public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T>
async返回的是Deferred用于等待未来结果的返回。一般使用 await 来调用获取结果。
public suspend fun await(): T
切换线程withContext
public suspend fun <T> withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T
内部需要传入一个协程上下文,我们一般使用调度器Dispatchers来切换线程,它是协程上下文CoroutineContext的实现类之一。(后面会单独出一期)
- Dispatchers.Main Android主线程
- Dispatchers.Unconfined 当前CoroutineScope的线程策略
- Dispatchers.Default 默认值,为JVM共享线程池
- Dispatchers.IO IO线程池,默认为64个线程
通过lauch 和 async 和 withContext 的使用示例如下:
GlobalScope.launch{
YYLogUtils.w("执行在协程中...")
delay(1000L)
val deferred = async {
YYLogUtils.w("切换到另一个协程")
Thread.sleep(2000)
return@async "response data"
}
val response = deferred.await()
YYLogUtils.w("response:$response")
val result = withContext(Dispatchers.IO) {
//异步执行
delay(1000L)
YYLogUtils.w("切换到另一个协程")
return@withContext "1234"
}
YYLogUtils.w("result:$result")
delay(1000L)
YYLogUtils.w("执行完毕...")
}
这也是我们常用的两种方式:async是异步执行,withContext是同步执行。
当它们的代码块执行完毕,就会回到主协程的线程中,换句话说就是通过调度器实现切换线程,执行完就回到当前线程。如果想知道底层逻辑可以看这里。
到这里就会简单的协程使用了,但是注意上面的代码有时候用的 sleep 有时候用的 delay ,看着意思都是延迟的意思,有什么区别?
三、协程的阻塞与挂起
阻塞的意思就是会阻断当前线程后面的代码不会执行。挂起的全名应该叫非阻塞式挂起,其意思是为不会阻塞其他协程,只是当前自己所在协程会挂起等待不执行,但是其他协程还是能继续执行的。
3.1 suspend非阻塞挂起函数
我们使用AS来编程,就很清晰的可以看到,左侧有箭头的就是挂起,而sleep方法是没有箭头的就不是挂起而是阻塞。
而挂起的方法调用都是需要 suspend 标记的,如
public suspend fun delay(timeMillis: Long) {
...
}
我们举一个简单的例子说明:
GlobalScope.launch{
YYLogUtils.w("执行在协程中...")
val result1 = withContext(Dispatchers.IO) {
//异步执行
YYLogUtils.w("异步执行result1")
delay(1000L)
return@withContext "1234"
}
YYLogUtils.w("result1:$result1")
val result2 = withContext(Dispatchers.IO) {
//异步执行
YYLogUtils.w("异步执行result2")
delay(1000L)
return@withContext "123456"
}
YYLogUtils.w("result2:$result2")
YYLogUtils.w("执行完毕...")
}
打印Log如下:
这是正常的,顺序执行的。为什么‘阻塞’了?不是说 delay 函数是挂起函数,是非阻塞的吗?OK,再次强调一点,此阻塞的概念并非是说阻塞这个线程,阻塞这段代码不让执行,此阻塞是针对其他 协程
的。上面的 withContext 它创建/启动了了协程吗?没有,它只是切换了线程,它本身其实也是 suspend 的函数而已。所以上面的代码是顺序执行的。
下面我们修改一下代码为启动协程:
GlobalScope.launch{
YYLogUtils.w("执行在协程中...")
GlobalScope.launch(Dispatchers.IO) {
//异步执行
YYLogUtils.w("异步执行result1")
delay(1000L)
YYLogUtils.w("result1:1234")
}
GlobalScope.launch(Dispatchers.IO) {
//异步执行
YYLogUtils.w("异步执行result2")
delay(1000L)
YYLogUtils.w("result2:123456")
}
YYLogUtils.w("执行完毕...")
}
挂起函数是不会阻塞协程的,打印Log如下:
而我们自定义的函数方法,也可以通过标记 suspend 而在协程中使用
GlobalScope.launch{
YYLogUtils.w("执行在协程中...")
saveSth2Local()
val result1 = withContext(Dispatchers.IO) {
//异步执行
YYLogUtils.w("异步执行result1")
delay(1000L)
saveSth2Local()
return@withContext "1234"
}
YYLogUtils.w("result1:$result1")
YYLogUtils.w("执行完毕...")
}
suspend fun saveSth2Local() {
DBHelper.get().saveUser()
}
使用我们定义的 saveSth2Local 挂起方法的时候,在哪个作用域使用就是在哪个线程执行,如上面的 saveSth2Local 方法是在主线程执行,withContext 中的 saveSth2Local 方法则是在子线程中使用。
3.2 runBlocking阻塞协程
上面我们讲到的是 suspend 挂起函数的阻塞与非阻塞的概念,而我们启动函数launch 和 runBlocking 也是区分阻塞与非阻塞的。概念都是一样的,就是是否阻塞其他协程。
launch的是非阻塞的,runBlocking就是阻塞的,它会阻止其他协程的运行。
同じコードで、GlobalScope.launchをrunBlockingに変更して、次のことを試してみましょう。
GlobalScope.launch{
YYLogUtils.w("执行在协程中...")
runBlocking(Dispatchers.IO) {
//异步执行
YYLogUtils.w("异步执行result1")
delay(1000L)
YYLogUtils.w("result1:1234")
}
runBlocking(Dispatchers.IO) {
//异步执行
YYLogUtils.w("异步执行result2")
delay(1000L)
YYLogUtils.w("result2:123456")
}
YYLogUtils.w("执行完毕...")
}
実行結果:
runBlockingは、他のコルーチンの実行を実際に防止し、他のコルーチンを実行し続ける前に、自分で実行する必要があることがわかります。これはブロッキングです。
スコープであるかどうか、同じレベルの兄弟コルーチンであるか、親子コルーチンであるかにかかわらず、ブロックされます。
CoroutineScope(Dispatchers.Main).launch {
YYLogUtils.w("执行在协程中...")
withContext(Dispatchers.IO) {
//异步执行
YYLogUtils.w("异步执行result1")
delay(1000L)
YYLogUtils.w("result1:1234")
}
withContext(Dispatchers.IO) {
//异步执行
YYLogUtils.w("异步执行result2")
delay(1000L)
YYLogUtils.w("result2:123456")
}
delay(1000L)
YYLogUtils.w("执行完毕...")
}
GlobalScope.launch(Dispatchers.Main) {
YYLogUtils.w("执行在另一个协程中...")
delay(1000L)
YYLogUtils.w("另一个协程执行完毕...")
}
上記のコードのように、2つの親スコープのコルーチンが同時に実行されます。最初のコルーチンがブロックされていない場合、次のコルーチンを実行できます。このとき、2つのコルーチンは同時に実行され、ログは次のようになります。次のとおりです。
ただし、上記のコルーチンをブロックするようにコードを変更すると、次のコルーチンは、ブロックされたコードが実行されるのを待ってから実行する必要があります。このとき、2つのコルーチンは非並行のシリアル順序で実行されます。
CoroutineScope(Dispatchers.Main).launch {
YYLogUtils.w("执行在协程中...")
runBlocking(Dispatchers.IO) {
//异步执行
YYLogUtils.w("异步执行result1")
delay(1000L)
YYLogUtils.w("result1:1234")
}
runBlocking(Dispatchers.IO) {
//异步执行
YYLogUtils.w("异步执行result2")
delay(1000L)
YYLogUtils.w("result2:123456")
}
delay(1000L)
YYLogUtils.w("执行完毕...")
}
GlobalScope.launch(Dispatchers.Main) {
YYLogUtils.w("执行在另一个协程中...")
delay(1000L)
YYLogUtils.w("另一个协程执行完毕...")
}
非同時印刷の結果は次のとおりです。
この説明は、誰もがブロッキングの概念を理解できるはずだと信じています。
要約する
この記事は、コルーチンの使用に関するシリーズの最初の記事です。基本的な使用法。この記事には、基本的な概念と基本的な使用法のみが含まれます。コルーチンにはライフサイクル、サポートキャンセル、およびその他の機能があります。これについては、次で説明します。いくつかの問題。
この問題で習得する必要があるのは、コルーチンを開始するいくつかの方法、スレッドを切り替えるいくつかの方法、非同期実行と同期実行の類似点と相違点、関数の一時停止、およびブロックと非ブロックの概念です。
コルーチンの概念や枠組みは比較的大きいので、説明に間違いや間違いがあれば、生徒たちに指摘して伝えてもらいたいと思います。
この記事が少し刺激を受けたと感じたら、私をサポートしていただければ幸いです点赞
。あなたのサポートが私の最大のモチベーションです。
さて、これでこの号は終わりです。
ナゲッツテクノロジーコミュニティのクリエイター署名プログラムの募集に参加しています。リンクをクリックして登録し、送信してください。