1. Kotlin Coroutines 简介
在过去几年间,协程这个概念发展势头迅猛,到现在已经被诸多主流编程语言采用,例如:Go
、Python
等都可以在语言层面上实现协程,甚至是 Java
也可以通过使用扩展库来间接地支持协程。今日主角 Kotlin
也紧跟步伐,在 1.3 版本中添加了对协程的支持。
Kotlin Coroutines
是 Kotlin
提供的一套线程处理框架。开发者可以使用 Kotlin Coroutines
简化异步代码,使得不同线程的代码可以在同一代码块中执行,使代码显得更加线性,阅读起来更自如。
但是协程 Coroutines
并不是 Kotlin
提出来的新概念,其源自 Simula
和 Modula-2
语言,这个术语早在 1958 年就被 Melvin Edward Conway 发明并用于构建汇编程序,这说明了协程是一种编程思想,并不局限于特定的语言。
Kotlin Coroutines
为 Android
开发者解决了以下痛点
- 主线程安全问题
- 回调地狱「 Callback Hell 」
下面介绍一个简单的例子来看看协程能有多简洁。
fun test() {
thread {
// 子线程做网络请求
request1()
runOnUiThread {
// 切换到主线程更新UI
Log.d("tag", "request1")
thread {
// 子线程做网络请求
request2()
runOnUiThread {
// 切换到主线程更新UI
Log.d("tag", "request2")
}
}
}
}
}
private fun request1() = Unit
private fun request2() = Unit
可以看到,当要处理多个请求时,会出现多层嵌套(多层回调)的问题,代码的易读性会很差,反观以下使用协程的代码便会清晰很多。
fun test2() {
val coroutineScope = CoroutineScope(Dispatchers.Main)
coroutineScope.launch {
val request1 = withContext(Dispatchers.IO) {
// 子线程做网络请求
request1()
}
// 切换到主线程更新UI
Log.d("tag", request1)
val request2 = withContext(Dispatchers.IO) {
// 子线程做网络请求
request2()
}
// 切换到主线程更新UI
Log.d("tag", request2)
}
}
suspend fun request1(): String {
// 延迟 2s 模拟一次网络请求
delay(2000)
return "request1"
}
suspend fun request2(): String {
// 延迟 1s 模拟一次网络请求
delay(1000)
return "request2"
}
2. 小试牛刀
下面我们开始使用协程,首先使用 Android Studio
创建一个 Kotlin
项目,然后在 build.gradle
添加以下配置
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:7.0.3"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.20"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
然后在 app 模块的 build.gradle
添加以下依赖
dependencies {
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
// ...
// 协程核心库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0"
// 协程 Android 支持库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0"
}
这样一来我们就能使用协程了,接下来我们从如何创建一个协程说起。
2.1 创建一个协程
我们可以通过以下 3 种方法来启动一个协程
runBlocking
会阻断当前线程,直到闭包中语句执行完成,因此不会在业务开发场景使用,一般用于 main 函数以及单元测试中。CoroutineScope.launch
适用于执行一些不需要返回结果的任务。CoroutineScope.async
适用于执行需要返回结果的任务。会在下一小节讲到可以使用await
挂起函数来拿到返回结果。- 推荐使用
CoroutineContext
构建CoroutineScope
,来创建协程,因为这样能更好的控制和管理协程的生命周期。后面原理部分会专门讲这两部分。
/**
* 启动协程的三种方式
*/
fun startCoroutine() {
// 通过 runBlocking 启动一个协程
// 它会中断当前线程直到 runBlocking 闭包中的语句执行完成
runBlocking {
fetchDoc()
}
// 通过 CoroutineScope.launch 启动一个协程
val coroutineScope = CoroutineScope(Dispatchers.IO)
coroutineScope.launch {
fetchDoc()
}
// 通过 CoroutineScope.async 启动一个协程
val coroutineScope2 = CoroutineScope(Dispatchers.Default)
coroutineScope2.async {
fetchDoc()
}
}
2.2 线程切换操作
在 Kotlin Coroutines
中主要是使用 调度器 来控制线程的切换。在创建协程时可以传入指定的调度模式来决定协程体 block
在哪个线程中执行。
// 在后台线程中执行操作
someScope.launch(Dispatchers.Default) {
// ...
}
上面协程的代码块,会被分发到由协程所管理的线程池中执行。
上例中的线程池属于 Dispatchers.Default
。在未来的某一时间,该代码块会被线程池中的某个线程执行,具体执行时间取决于线程池的策略。
除了上面例子中的Dispatchers.Default
调度器,还有以下两种调度器
Dispatchers.IO
该调度器下的代码块会在 IO 线程中执行,主要处理网络请求,以及 IO 操作。Dispatchers.Main
该调度器下的代码块会在主线程执行,主要是做更新 UI 操作。
class MainActivity : AppCompatActivity() {
private val mainScope = MainScope()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
startLaunch()
}
private fun startLaunch() {
// 创建一个默认参数的协程,其默认的调度模式为 Main 也就是说该协程的线程环境是主线程
val job1 = mainScope.launch {
// delay 是一个挂起函数
Log.d("startLaunch", "Before Delay")
delay(1000)
Log.d("startLaunch", "After Delay")
}
val job2 = mainScope.launch(Dispatchers.IO) {
// 当前线程环境是 IO 线程
Log.d("startLaunch", "CurrentThread + ${Thread.currentThread()}")
withContext(Dispatchers.Main) {
// 当前线程环境是主线程
Log.d("startLaunch", "CurrentThread + ${Thread.currentThread()}")
}
}
mainScope.launch {
// 执行完成后可以有返回值
val userInfo = getUserInfo()
Log.d("startLaunch", "CurrentThread + ${Thread.currentThread()}")
}
}
// withContext 是一个挂起函数, 可以挂起当前协程(可以传入新的 Context )
private suspend fun getUserInfo() = withContext(Dispatchers.IO) {
delay(2000)
"Hello World"
}
}
2.3 处理并发操作
Kotlin Coroutines
通过 async
关键字来做并发操作,通常配合 await
方法或者 awaitAll
扩展方法来使用来使用。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
MainScope().launch {
startAsync()
}
}
private suspend fun startAsync() {
val coroutineScope = CoroutineScope(Dispatchers.IO)
// async 会返回 deferred 对象 可以通过 await() 返回值
val avatarJob = coroutineScope.async { fetchAvatar() }
val nameJob = coroutineScope.async { fetchName() }
// 也可以 Collections 的 awaitAll() 扩展方法返回 返回值的集合
val listOf = listOf(avatarJob, nameJob)
val startTime = System.currentTimeMillis()
val awaitAll = listOf.awaitAll()
val endTime = System.currentTimeMillis()
Log.d("startAsync", "用的时间${endTime - startTime}ms")
Log.d("startAsync", awaitAll.toString())
}
private suspend fun fetchAvatar(): String {
delay(2000)
return "头像"
}
private suspend fun fetchName(): String {
delay(2000)
return "昵称"
}
}
上面的代码就是一个简单的并发示例 fetchAvatar
延迟了2000ms,fetchName
延迟 1000 ms 完成,这里使用了 awaitAll()
方法返回结果的集合,它会等 fetchAvatar
完成后,也就是 2000 ms 后返回结果。
3. 理解 Kotlin Coroutines 挂起函数
在上述例子中出现了很多次 suspend
关键字,它是 Kotlin Coroutines
的一个关键字,我们将用 suspend
关键字修饰的函数称之为挂起函数。例如 withContext
、delay
这些都属于挂起函数。
3.1 什么是挂起
那么挂起究竟是什么意思,会阻塞当前线程吗?我们看一个日常的例子,我们向服务端请求用户信息,这个过程是耗时的因此要在 IO 线程获取用户信息,1 通过 withContext
挂起并切换到 IO 线程请求用户信息,2 回到 UI 线程更新UI。此时会阻塞主线程吗,答案是当然不会,不然页面早就卡死了。
class UserViewModel: ViewModel() {
// 请求用户信息后刷新 UI
fun fetchUserInfo() {
// 启动一个上下文是 UI线程的
viewModelScope.launch(Dispatchers.Main) {
// 1 挂起请求用户信息
val userInfo = withContext(Dispatchers.IO) {
UserResp.fetchUserInfo()
}
// 2 更新UI
Log.d("fetchUserInfo", userInfo)
}
}
我们从两个当前线程和挂起的协程两个角色来理解挂起。
线程
其实当线程执行到协程的 suspend 函数的时候,暂时不继续执行协程代码了。
那线程接下来会做什么呢?
如果它是一个后台线程:
- 要么无事可做,被系统回收
- 要么继续执行别的后台任务
我们上述例子中是在协程上下文是在主线程,因此主线程会继续去做工作,也就是刷新界面的工作。
协程
协程此时就从当前线程挂起了,如果其上下文是其他线程,那么协程就会在其他线程无限制的去运行。等到任务运行完成后在切换回主线程
我们上述例子是在 IO 线程,因此会切换到 IO 线程去请求用户的信息。
3.2 suspend 关键字有何作用
上述 suspend 修饰的函数都有挂起/切换线程的功能。
那是不是任何用 suspend
关键字都会有这样的特性?
答案是否定的,来看以下方法只做了打印一段文字,并没有切到某处,又切回来。所以 supend
关键字并不启到协程挂起/切换线程的作用
suspend fun test() {
println("我是挂起函数")
}
那么 suspend
关键字到底有什么用呢?
答案是提醒”函数调用方“,被 suspend
关键字修饰的是一个耗时的函数,需要在协程中才能使用。
4. 理解 Kotlin Coroutines 几个核心概念
4.1 CoroutineContext - 上下文
CoroutineContext
即协程的上下文,主要承载了资源获取,配置管理等工作,是执行环境相关的通用数据资源的统一提供者。使用协程中运行协程的上下文是极其重要的,这样才可以实现正确的线程行为、生命周期。
CoroutineContext
包含用户定义的一些数据集合,这些数据与协程密切相关。它是一个有索引的 Element
实例集合。这个有索引的集合类似于一个介于 Set
和 Map
之间的数据结构。每个 element
在这个集合有一个唯一的 Key
与之对应。对于相同 Key
的 Element
是不可以重复存在的。
Element
之间可以通过 + 号组合起来,Element
有几个子类,CoroutineContext
也主要由这几个子类组成:
Job
:协程的唯一标识,控制协程的生命周期。CoroutineDispatche
:指定协程运行的线程。CoroutineName
:协程的名称,默认为 coroutine,一般在调试的时候使用。CoroutineExceptionHandler
:指协程的异常处理器,用于处理未被捕捉的异常。
CoroutineContext
接口的定义如下:
public interface CoroutineContext {
// 操作符[]重载,可以通过CoroutineContext[Key]这种形式来获取与Key关联的Element
public operator fun <E : Element> get(key: Key<E>): E?
// 它是一个聚集函数,提供了从left到right遍历CoroutineContext中每一个Element的能力,并对每一个Element做operation操作
public fun <R> fold(initial: R, operation: (R, Element) -> R): R
// 操作符+重载,可以CoroutineContext + CoroutineContext这种形式把两个CoroutineContext合并成一个
public operator fun plus(context: CoroutineContext): CoroutineContext
// 返回一个新的CoroutineContext,这个CoroutineContext删除了Key对应的Element
public fun minusKey(key: Key<*>): CoroutineContext
// Key定义,空实现,仅仅做一个标识
public interface Key<E : Element>
// Element定义,每个Element都是一个CoroutineContext
public interface Element : CoroutineContext {
// 每个Element都有一个Key实例
public val key: Key<*>
//...
}
}
通过接口定义可以发现 CoroutineContext
几个特点
- 重写了 get 操作符,因此可以像访问 map 中的元素一样使用
CoroutineContext[key]
这种中括号的形式来访问。 - 重写了 plus 操作符,因此可以使用 + 号连接不同的
CoroutineContext
。
通过查看源码可以发现 CoroutineContext
主要被 CombinedContext
、Element
、EmptyCoroutineContext
所实现。
Element
可能会比较奇怪 "为什么元素本身也是集合"。原因是主要是设计 API 方便,表示Element
内部只存放 Element
。
EmptyCoroutineContext
是 CoroutineContext
的空实现,不持有任何元素。
public operator fun plus(context: CoroutineContext): CoroutineContext =
// 如果要相加的CoroutineContext为空,那么不做任何处理,直接返回
if (context === EmptyCoroutineContext) this else // fast path -- avoid lambda creation
// 如果要相加的CoroutineContext不为空,那么对它进行fold操作
context.fold(this) { acc, element -> // 我们可以把acc理解成+号左边的CoroutineContext,element理解成+号右边的CoroutineContext的某一个element
//首先从左边CoroutineContext中删除右边的这个element
val removed = acc.minusKey(element.key)
// 如果removed为空,说明左边CoroutineContext删除了和element相同的元素后为空,那么返回右边的element即可
if (removed === EmptyCoroutineContext) element else {
// 确保interceptor一直在集合的末尾
val interceptor = removed[ContinuationInterceptor]
if (interceptor == null) CombinedContext(removed, element) else {
val left = removed.minusKey(ContinuationInterceptor)
if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
CombinedContext(CombinedContext(left, element), interceptor)
}
}
}
由于 CoroutineContext
是由一组元素组成的,所以加号右侧的元素会覆盖加号左侧的元素,进而组成新创建的 CoroutineContext
。比如 (Dispatchers.Main, "name") + (Dispatchers.IO) = (Dispatchers.IO, "name")
。
4.2 Job & Deferred - 任务
4.2.1 Job
Job
用于处理协程。对于每一个所创建的协程(通过 launch
或者 async
),它会返回一个 Job
实例,该实例是协程的唯一标识,并且负责管理协程的生命周期。
除了使用 launch
或者 async
创建 Job
外,还可以通过下面 Job
构造方法创建 Job
。
public fun Job(parent: Job? = null): Job = JobImpl(parent)
这个很好理解,当传入 parent 时,此时的 Job
将会作为 parent 的子 Job
。
既然 Job
是来管理协程的,那么它提供了六种状态来表示协程的运行状态。见官方表格
State | [isActive] | [isComplete] | isCancelled |
---|---|---|---|
New (optional initial state) | false | false | false |
Active (default initial state) | true | false | false |
Completing (transient state) | true | false | false |
Cancelling (transient state) | false | false | true |
Cancelled (final state) | false | true | true |
Completed (final state) | false | true | false |
虽然我们获取不到协程具体的运行状态,但是可以通过 isActive
,isCompleted
、isCancelled
来获取当前协程是否处于三种状态。
我们可以通过下图可以大概了解下一个协程作业从创建到完成或者取消。
wait children
+-----+ start +--------+ complete +-------------+ finish +-----------+
| New | -----> | Active | ---------> | Completing | -------> | Completed |
+-----+ +--------+ +-------------+ +-----------+
| cancel / fail |
| +----------------+
| |
V V
+------------+ finish +-----------+
| Cancelling | --------------------------------> | Cancelled |
+------------+ +-----------+
4.2.2 Deferred
public interface Deferred<out T> : Job {
public val onAwait: SelectClause1<T>
public suspend fun await(): T
@ExperimentalCoroutinesApi
public fun getCompleted(): T
@ExperimentalCoroutinesApi
public fun getCompletionExceptionOrNull(): Throwable?
}
通过使用 async
创建协程可以得到一个有返回值 Deferred
,Deferred
接口继承自 Job
接口,额外提供了 await
方法来获取 Coroutine
的返回结果。由于 Deferred
继承自 Job
接口,所以 Job
相关的内容在 Deferred
上也是适用的。
4.3 CoroutineDispatcher - 调度器
调度器是什么呢?Kotlin 官方是这么给出解释的
调度器它确定了相关的协程在哪个线程或哪些线程上执行。协程调度器可以将协程限制在一个特定的线程执行,或将它分派到一个线程池,亦或是让它不受限地运行。
Dispatchers
是一个标准库中帮我们封装了切换线程的帮助类,可以简单理解为一个线程池。
public actual object Dispatchers {
@JvmStatic
public actual val Default: CoroutineDispatcher = createDefaultDispatcher()
@JvmStatic
public actual val Main: MainCoroutineDispatcher
get() = MainDispatcherLoader.dispatcher
@JvmStatic
public actual val Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined
@JvmStatic
public val IO: CoroutineDispatcher = DefaultScheduler.IO
}
Dispatchers.Default
默认的调度器,适合处理后台计算,是一个
CPU
密集型任务调度器。如果创建Coroutine
的时候没有指定dispatcher
,则一般默认使用这个作为默认值。Default dispatcher
使用一个共享的后台线程池来运行里面的任务。注意它和IO
共享线程池,只不过限制了最大并发数不同。Dispatchers.IO
顾名思义这是用来执行阻塞
IO
操作的,是和Default
共用一个共享的线程池来执行里面的任务。根据同时运行的任务数量,在需要的时候会创建额外的线程,当任务执行完毕后会释放不需要的线程。Dispatchers.Unconfined
由于
Dispatchers.Unconfined
未定义线程池,所以执行的时候默认在启动线程。遇到第一个挂起点,之后由调用resume
的线程决定恢复协程的线程。Dispatchers.Main:
指定执行的线程是主线程,在
Android
上就是UI
线程。
4.4 CoroutineStart - 启动器
CoroutineStart
协程启动模式,是启动协程时需要传入的第二个参数。协程启动模式有 4 种:
CoroutineStart.DEFAULT
默认启动模式,我们可以称之为饿汉启动模式,因为协程创建后立即开始调度,虽然是立即调度,但不是立即执行,也有可能在执行前被取消。
CoroutineStart.LAZY
懒汉启动模式,启动后并不会有任何调度行为,直到我们需要它执行的时候才会产生调度。也就是说只有我们主动的调用
Job
的start
、join
或者await
等函数时才会开始调度。CoroutineStart.ATOMIC
ATOMIC
一样也是在协程创建后立即开始调度,但是它和DEFAULT
模式有一点不一样,通过ATOMIC
模式启动的协程执行到第一个挂起点之前是不响应cancel
取消操作的,ATOMIC
一定要涉及到协程挂起后cancel
取消操作的时候才有意义。CoroutineStart.UNDISPATCHED:
协程在这种模式下会直接开始在当前线程下执行,直到运行到第一个挂起点。
4.5 CoroutineScope - 协程的作用域
启动一个协程必须指定其 CoroutineScope
。CoroutineScope
可以对协程进行追踪,即使协程被挂起也是如此。同调度程序 Dispatcher
不同,CoroutineScope
并不运行协程,它只是确保您不会失去对协程的追踪。
通过 CoroutineScope
可以取消协程中的任务,在 Android 中通常我们会在页面启动的时候做一下耗时操作,在页面关闭时这些耗时任务就没有意义了,此时 Activity
或 Fragment
就可以通过 lifecycleScope
来启动协程。
为了明确父子协程之间的关系以及协程异常传播情况,官方将协程的作用域分为以下三类
顶级作用域
没有父协程的协程所在的作用域为顶级作用域。
协同作用域
协程中启动新的协程,新协程为所在协程的子协程,这种情况下,子协程所在的作用域默认为协同作用域。此时子协程抛出的未捕获异常,都将传递给父协程处理,父协程同时也会被取消。
主从作用域
与协同作用域在协程的父子关系上一致,区别在于,处于该作用域下的协程出现未捕获的异常时,不会将异常向上传递给父协程。
除了三种作用域中提到的行为以外,父子协程之间还存在以下规则:
父协程被取消,则所有子协程均被取消。由于协同作用域和主从作用域中都存在父子协程关系,因此此条规则都适用。父协程需要等待子协程执行完毕之后才会最终进入完成状态,不管父协程自身的协程体是否已经执行完。子协程会继承父协程的协程上下文中的元素,如果自身有相同 key
的成员,则覆盖对应的 key
,覆盖的效果仅限自身范围内有效。
5. Android 中使用协程的几个🌰
注意: 以下实例代码都是在 ViewModel
中,因此可以使用 viewModelScope
5.1 网络请求
目前 Retrofit
官方已经对 Kotlin Coroutines
做了支持。
interface UserApi {
@GET("url")
suspend fun getUsers(): List<UserEntity>
}
定义完成接口后,用协程来实现一个最基本的请求。
private fun fetchUsers() {
viewModelScope.launch {
users.postValue(Resource.loading(null))
try {
val usersFromApi = apiHelper.getUsers()
users.postValue(Resource.success(usersFromApi))
} catch (e: Exception) {
users.postValue(Resource.error(e.toString(), null))
}
}
}
也可以通过 Kotlin Coroutines
实现多个接口同时请求,拿到两个接口数据后刷新 UI。
private fun fetchUsers() {
viewModelScope.launch {
users.postValue(Resource.loading(null))
try {
// coroutineScope is needed, else in case of any network error, it will crash
coroutineScope {
val usersFromApiDeferred = async { apiHelper.getUsers() }
val moreUsersFromApiDeferred = async { apiHelper.getMoreUsers() }
val usersFromApi = usersFromApiDeferred.await()
val moreUsersFromApi = moreUsersFromApiDeferred.await()
val allUsersFromApi = mutableListOf<ApiUser>()
allUsersFromApi.addAll(usersFromApi)
allUsersFromApi.addAll(moreUsersFromApi)
users.postValue(Resource.success(allUsersFromApi))
}
} catch (e: Exception) {
users.postValue(Resource.error("Something Went Wrong", null))
}
}
}
5.2 操作 Room 数据库
作为 JetPack
中的一员,Room
对 Kotlin Coroutines
做了较好的支持。代码如下
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUsers(vararg users: User)
@Update
suspend fun updateUsers(vararg users: User)
@Delete
suspend fun deleteUsers(vararg users: User)
@Query("SELECT * FROM user WHERE id = :id")
suspend fun loadUserById(id: Int): User
@Query("SELECT * from user WHERE region IN (:regions)")
suspend fun loadUsersByRegion(regions: List<String>): List<User>
}
5.3 做一些耗时操作
Kotlin Coroutines
也可以做一些耗时的操作,比如 IO 读写,列表的排序等等。这里用 delay
来模拟耗时任务。
fun startLongRunningTask() {
viewModelScope.launch {
status.postValue(Resource.loading(null))
try {
// do a long running task
doLongRunningTask()
status.postValue(Resource.success("Task Completed"))
} catch (e: Exception) {
status.postValue(Resource.error("Something Went Wrong", null))
}
}
}
private suspend fun doLongRunningTask() {
withContext(Dispatchers.Default) {
// your code for doing a long running task
// Added delay to simulate
delay(5000)
}
}
5.4 给耗时任务设置一个超时时间
可以通过 withTimeout
给一个协程增加超时时间,当任务超过这段时间就会抛出 TimeoutCancellationException
。
private fun fetchUsers() {
viewModelScope.launch {
users.postValue(Resource.loading(null))
try {
withTimeout(100) {
val usersFromApi = apiHelper.getUsers()
users.postValue(Resource.success(usersFromApi))
}
} catch (e: TimeoutCancellationException) {
users.postValue(Resource.error("TimeoutCancellationException", null))
} catch (e: Exception) {
users.postValue(Resource.error("Something Went Wrong", null))
}
}
}
5.5 全局异常的处理
可以自定义 CoroutineExceptionHandler
,来处理一些未被拦截的异常。
private val exceptionHandler = CoroutineExceptionHandler { _, exception ->
users.postValue(Resource.error("Something Went Wrong", null))
}
private fun fetchUsers() {
viewModelScope.launch(exceptionHandler) {
users.postValue(Resource.loading(null))
val usersFromApi = apiHelper.getUsers()
users.postValue(Resource.success(usersFromApi))
}
}
5.6 用 Kotlin Flow 来实现倒计时
在 Activity
或 Fragment
中通过 Flow
启动一个倒计时,每隔 1 s更新一次 UI 状态
lifecycleScope.launch {
(59 downTo 0).asFlow()
.onEach { delay(1000) }
.flowOn(Dispatchers.Default)
.onStart {
Logger.d("计时器开始")
}.collect { remain ->
Logger.d("计时器剩余$remain秒")
}
}
6. 小结
通过这篇文章带大家回顾了一下「协程是什么」、「如何使用协程」、「对挂起的理解」以及「协程在 Android
中的应用」。其实协程正如 Kotlin
这门年轻的语言一样,不断再优化,也不断增加新的功能,例如 Flow
,Channel
等等。如果大家有更好的意见欢迎留言评论。
更多精彩请关注我们的公众号 “百瓶技术 ”,有不定期福利呦!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。