前言
介绍Kotlin协程之前,我们先来看看广义上的协程是啥。
- 用于在程序中处理并发任务的一种方案
- 比线程更加轻量级的存在,线程的上下文切换都需要内核(操作系统的核心)参与,而协程的上下文切换,完全由用户去控制,避免了大量的中断参与,减少了线程上下文切换与调度消耗的资源。线程上下文切换具体如下所示:
挂起一个线程,将这个线程在 CPU 中的状态(上下文)存储于内存中的某处;
恢复一个线程,在内存中检索下一个线程的上下文并将其在 CPU 的寄存器中恢复;
跳转到程序计数器所指向的位置(即跳转到线程被中断时的代码行),以恢复该线程。线程上下文:线程Id + 线程状态 + 堆栈 + 寄存器状态等
1. Kotlin协程是啥?
- Kotlin 的协程和⼴义的协程不是⼀种东⻄,Kotlin 的协程是⼀个线程框架,本质上只是一套基于原生Java Thread API 的封装,类似于Android 的 Handler 系列 API。
- Android官方文档中提到,协程是一种并发设计模式,可以在 Android 平台上使用Kotlin协程来简化异步执行的代码。
2. Kotlin协程具体介绍
2.0 Kotlin协程的优点
- 协程最大的优点是可以帮我们自动切线程,用看起来同步的方式写出异步的代码,不用再写回调了。
- 利用线程发起两个并行请求很困难,一般可能就会选择先后请求,而如果是协程的话利用两个async()函数开启两个协程同步请求就很容易了。
- 使用协程作用域统一管理协程可以减少内存泄漏。
- 许多 Jetpack 库都包含提供全面协程支持的扩展。
2.1 添加依赖
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
2.2 启动协程
- launch()
需要在CoroutineScope中启动,launch返回一个「Job」,用于协程监督与取消,用于无返回值的场景。
GlobalScope.launch {
}
- async()
需要在CoroutineScope中启动,async返回一个Job的子类「Deferred」,可通过await()获取完成时返回值。
val res = GlobalScope.async {
}
- runBlocking()
runBlocking函数会建立一个 堵塞当前线程的协程。
runBlocking {
}
2.3 协程作用域(CoroutineScope)
用于管理协程生命周期。
- GlobalScope
定义成了一个 单例对象, 在整个JVM虚拟中只有一份对象实例,生命周期贯穿整个JVM,故使用时需要警惕 内存泄漏!!!
- CoroutineScope
自定义协程作用域,可以指定协程作用域的上下文参数。
val scope = CoroutineScope(Dispatchers.IO + CoroutineName("MyCoroutineScope"))
scope.launch {
}
scope.cancel()
- MainScope()
开启一个创建基于主线程的协程作用域
open class BaseActivity: AppCompatActivity(), CoroutineScope by MainScope(){ //可以构建一个实现了CoroutineScope接口的BaseActivity用于方便的调用launch开协程
override fun onDestroy() {
super.onDestroy()
cancel()
}
}
- coroutineScope()
会继承外部的协程作用域并且创建一个子作用域。在作用域内所有代码和子协程运行完之前一直阻塞当前协程,但不影响其他协程和线程。可以用于在suspend函数中开协程。
2.4 Job(作业)
- 协程的 Job 是上下文的一部分,并且可以使用 coroutineContext [Job] 表达式在上下文中检索它。
- 调用launch函数会返回一个Job对象,代表一个 协程的工作任务,可以用于查看协程状态以及控制协程。
//查看协程状态
isActive: Boolean //是否存活
isCancelled: Boolean //是否取消
isCompleted: Boolean //是否完成
children: Sequence<Job> // 所有子作业
//控制协程
cancel() // 取消协程
join() // 堵塞当前线程直到协程执行完毕
cancelAndJoin() // 两者结合,取消并等待协程完成
cancelChildren() // 取消所有子协程,可传入CancellationException作为取消原因
attachChild(child: ChildJob) // 附加一个子协程到当前协程上
2.5 CoroutineDispatcher(调度器)
协程调度器是协程上下文的一部分,它确定了相关的协程在哪个线程或哪些线程上执行。协程调度器可以将协程限制在一个特定的线程执行,或将它分派到一个线程池,亦或是让它不受限地运行。
- Kotlin协程有四种调度器
- withContext
和launch、async及runBlocking不同,withContext不会创建新的协程,常用于 切换代码执行所运行的线程。
它也是一个挂起方法,直到结束返回结果。多个withContext是串行执行的,所以很适合那种一个任务依赖上一个任务返回结果的情况。
2.6 suspend关键字
- suspend 也就是被挂起,而所谓的被挂起,就是切个线程;当运行到挂起函数的时候,协程会处于挂起状态(不会阻塞当前线程,脱离当前线程,开一个新的线程),等返回结果后,自动切回当前线程(协程会帮我 post 一个 Runnable,让我剩下的代码继续回到之前的线程去执行。)在协程挂起(等待)时,当前线程会回到线程池,当等待结束,协程会从线程池中一个空闲的线程上恢复。
fun main() {
println("start ${Thread.currentThread().name}")
GlobalScope.launch(Dispatchers.Main) {
delay(1000)// 挂起函数,另开一个线程执行,不会阻塞当前线程
println("launch ${Thread.currentThread().name}")
}
println("end ${Thread.currentThread().name}")
}
- suspend函数运行在协程里面或者另一个suspend函数中,协程里才有上下文信息,让挂起函数切线程之后,能跳转到原来的线程。Kotlin协程中的上下文环境 → CoroutineContext,以 键值对 的方式存储各种不同元素:
CoroutineContext
Job(协程唯一标识) + CoroutineDispatcher(调度器) + ContinuationInterceptor(拦截器) + CoroutineName(协程名称,一般调试时设置)
- suspend用于提醒和标记,代表是耗时函数,需要在后台运行,调用者不需要自行判断代码是否耗时。
- 创建一个 suspend 函数,为了让它包含真正挂起的逻辑,要在它内部直接或间接调用 Kotlin 自带的 suspend 函数,这个 suspend 才是有意义的。
3. Jetpack库对协程的支持
- ViewModelScope
对于 ViewModelScope,请使用 androidx.lifecycle:lifecycle-viewmodel-ktx:2.1.0-beta01 或更高版本。
为应用中的每个 ViewModel 定义了 ViewModelScope。如果 ViewModel 已清除,则在此范围内启动的协程都会自动取消。如果您具有仅在 ViewModel 处于活动状态时才需要完成的工作,此时协程非常有用。例如,如果要为布局计算某些数据,则应将工作范围限定至 ViewModel,以便在 ViewModel 清除后,系统会自动取消工作以避免消耗资源。
可以通过 ViewModel 的 viewModelScope 属性访问 ViewModel 的 CoroutineScope,如以下示例所示:
class MyViewModel: ViewModel() {
init {
viewModelScope.launch {
// ViewModel被清除时协程将会自动取消
}
}
}
- LifecycleScope
对于 LifecycleScope,请使用 androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha01 或更高版本。
为每个 Lifecycle 对象定义了 LifecycleScope。在此范围内启动的协程会在 Lifecycle 被销毁时取消。您可以通过 lifecycle.coroutineScope 或 lifecycleOwner.lifecycleScope 属性访问 Lifecycle 的 CoroutineScope。
以下示例演示了如何使用 lifecycleOwner.lifecycleScope 异步创建预计算文本:
class MyFragment: Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
val params = TextViewCompat.getTextMetricsParams(textView)
val precomputedText = withContext(Dispatchers.Default) {
PrecomputedTextCompat.create(longTextContent, params)
}
TextViewCompat.setPrecomputedText(textView, precomputedText)
}
}
}
- 将协程与 LiveData 一起使用
对于 liveData,请使用 androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha01 或更高版本。
使用 LiveData 时,您可能需要异步计算值。例如,您可能需要检索用户的偏好设置并将其传送给界面。在这些情况下,您可以使用 liveData 构建器函数调用 suspend 函数,并将结果作为 LiveData 对象传送,可以用于Repository中生成livedata数据返回给ViewModel。
在以下示例中,loadUser() 是在其他位置声明的暂停函数。使用 liveData 构建器函数异步调用 loadUser(),然后使用 emit() 发出结果:
val user: LiveData<User> = liveData {
val data = database.loadUser() // loadUser 需要声明为suspend函数,否则liveData将创建一个在主线程执行
emit(data)
}
当 LiveData 变为活动状态时,代码块开始执行;当 LiveData 变为非活动状态时,代码块会在可配置的超时过后自动取消。如果代码块在完成前取消,则会在 LiveData 再次变为活动状态后重启;如果在上次运行中成功完成,则不会重启。请注意,代码块只有在自动取消的情况下才会重启。如果代码块由于任何其他原因(例如,抛出 CancelationException)而取消,则不会重启。
还可以从代码块中发出多个值。每次 emit() 调用都会暂停执行代码块,直到在主线程上设置 LiveData 值。
val user: LiveData<Result> = liveData {
emit(Result.loading())
try {
emit(Result.success(fetchUser()))
} catch(ioException: Exception) {
emit(Result.error(ioException))
}
}
也可以将 liveData 与 Transformations 结合使用,如以下示例所示:
class MyViewModel: ViewModel() {
private val userId: LiveData<String> = MutableLiveData()
val user = userId.switchMap { id ->
liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) {
emit(database.loadUserById(id))
}
}
}
4. Retrofit对协程的支持
Retrofit 2.6.0 版本,内置了对 Kotlin Coroutines 的支持,进一步简化了使用 Retrofit 和协程来进行网络请求的过程。
举例如下:
在ApiService中,利用suspend关键字声明该函数需要在后台执行,同时函数可以直接返回我们需要的对象,而不是返回Deferred<T> 对象。
interface ApiService {
@GET("article/list/{page}/json")
suspend fun getHotArticle(@Path("page")page: Int): ArticleResponse
}
object ServiceCreator {
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl("https://www.wanandroid.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
inline fun <reified T> create() = retrofit.create(T::class.java)
}
object ApiNetwork {
private val apiService = ServiceCreator.create<ApiService>() // 创建动态代理对象
suspend fun getArticle(page: Int) = apiService.getHotArticle(page) // 调用suspend函数,该函数也需要声明为suspend函数
}
在Repository中获取数据
object HotRepository {
fun getArticle(page: Int) = liveData(Dispatchers.IO){ // livedata() 函数最后一个参数是一个lambda表达式,该lambda表达式中可以调用suspend函数
val res = try {
val articleResponse = ApiNetwork.getArticle(page)
if (articleResponse.errorCode == 0) {
Result.success(articleResponse) // Result 类可以对结果进行封装,返回更加直观的信息
} else {
Result.failure(RuntimeException("errorCode is ${articleResponse.errorCode}"))
}
} catch (e: Exception) {
Result.failure<ArticleResponse>(e)
}
emit(res)
}
}