1 协程(Coroutines)是什么
kotlin
官方文档说:本质上,协程是轻量级的线程。
从 Android 开发者的角度去理解它们的关系:
- 我们所有的代码都是跑在线程中的,而线程是跑在进程中的。
- 协程没有直接和操作系统关联,但它不是空中楼阁,它也是跑在线程中的,可以是单线程,也可以是多线程。
- 单线程中的协程总的执行时间并不会比不用协程少。
- Android 系统上,如果在主线程进行网络请求,会抛出
NetworkOnMainThreadException
,对于在主线程上的协程也不例外,这种场景使用协程还是要切线程的。我们学习
Kotlin
中的协程,一开始确实可以从线程控制的角度来切入。因为在Kotlin
中,协程的一个典型的使用场景就是线程控制。就像 Java 中的Executor
和 Android 中的AsyncTask
,Kotlin
中的协程也有对 Thread API 的封装,让我们可以在写代码时,不用关注多线程就能够很方便地写出并发操作。
小结:
- 协程最常用的功能是并发,而并发的典型场景就是多线程。
- 协程设计的初衷是为了解决并发问题,让 协作式多任务实现起来更加方便。
- 简单理解
Kotlin
协程的话,就是封装好的线程池,也可以理解成一个线程框架。 - 那么Kotlin中的协程是通过什么来实现异步操作的呢?它使用的是一种叫做 挂起 的机制。
2 你需要用协程吗?
RxJava
可以解决回调问题,同样我们可以用协程解决回调问题。
3 使用协程优点
- 轻量级,占用更少的系统资源;
- 更高的执行效率;
- 挂起函数较于实现Runnable或Callable接口更加方便可控;
- kotlin.coroutine 核心库的支持,让编写异步代码更加简单。
4 kotlin协程的演进
- Job: 任务,封装了协程中需要执行的代码逻辑。Job 可以取消并且有简单生命周期
- Coroutine context:协程上下文,协程上下文里是各种元素的集合
- Coroutine dispatchers :协程调度,可以指定协程运行在 Android 的哪个线程里
- suspend:挂起函数。挂起,就是一个稍后会被自动切回来的线程调度操作。
5 实现方式
5.1 环境准备
-
Kotlin
版本: 1.3.+ - 依赖的框架:在
app/build.gradle
里添加Kotlin
协程库的依赖如下所示。
//kotlin 标准库
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
//依赖协程核心库 ,提供Android UI调度器
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1"
//依赖当前平台所对应的平台库 (必须)
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'
5.2 创建协程的几种方式
方式 | 作用 |
---|---|
launch:job |
创建一个不会阻塞当前线程、没有返回结果的 Coroutine,但会返回一个 Job 对象,可以用于控制这个 Coroutine 的执行和取消,返回值为Job。 |
runBlocking:T |
创建一个会阻塞当前线程的Coroutine,常用于单元测试的场景,开发中一般不会用到 |
async/await:Deferred |
async 返回的 Coroutine 多实现了 Deferred 接口,简单理解为带返回值的launch函数 |
实现方式一: GlobalScope.launch
,使用 GlobalScope 单例对象, 可以直接调用 launch 开启协程。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_thread)
loadData()
}
private fun loadData() {
GlobalScope.launch(Dispatchers.IO) { //在IO线程开始
//IO 线程里拉取数据
val result = fetchData()
//主线程里更新 UI
withContext(Dispatchers.Main) { //执行结束后,自动切换到UI线程
tvShowContent.text = result
}
}
}
//关键词 suspend
private suspend fun fetchData(): String {
delay(2000) // delaying for 2 seconds to keep JVM alive
return "content"
}
我们最常用的用于启动协程的方式,它最终返回一个Job类型的对象,这个Job类型的对象实际上是一个接口,它包涵了许多我们常用的方法。 该方式启动的协程任务是不会阻塞线程的*
实现方式二:使用 runBlocking
顶层函数
runBlocking {}
是创建一个新的协程同时阻塞当前线程,直到协程结束。这个不应该在协程中使用,主要是为main
函数和测试设计的 。
fun main(args: Array) = runBlocking { // start main coroutine
launch { // launch new coroutine in background and continue
delay(1000L)
println("World!")
}
println("Hello,") // main coroutine continues here immediately
delay(2000L) // delaying for 2 seconds to keep JVM alive
}
实现方式三:async
+await
private fun testAysnc() = GlobalScope.launch {
val deferred = async(Dispatchers.IO) {
delay(3000L)
"Show Time"
}
// 此处获取耗时任务的结果,我们挂起当前协程,并等待结果
val result = deferred.await()
//挂起协程切换至UI线程 展示结果
withContext(Dispatchers.Main) {
tvShowContent.text = result
}
}
- async和await是两个函数,这两个函数在我们使用过程中一般都是成对出现的。
- async用于启动一个异步的协程任务,await用于去得到协程任务结束时返回的结果,结果是通过一个Deferred对象返回的。
那我们平日里常用到的调度器有哪些?
Dispatchers 种类 |
作用 |
---|---|
Dispatchers.Default | 共享后台线程池里的线程(适合 CPU 密集型的任务,比如计算) |
Dispatchers.Main | Android中的主线程 |
Dispatchers.IO | 共享后台线程池里的线程(针对磁盘和网络 IO 进行了优化,适合 IO 密集型的任务,比如:读写文件,操作数据库以及网络请求) |
Dispatchers.Unconfined | 不限制,使用父Coroutine的现场 |
回到我们的协程,它从 suspend
函数开始脱离启动它的线程,继续执行在 Dispatchers
所指定的 IO 线程。
紧接着在 suspend
函数执行完成之后,协程为我们做的最爽的事就来了:会自动帮我们把线程再切回来。
这个"切回来"是什么意思?
我们的协程原本是运行在主线程的,当代码遇到 suspend 函数的时候,发生线程切换,根据 Dispatchers
切换到了 IO 线程;
当这个函数执行完毕后,线程又切了回来,"切回来"也就是协程会帮我再 post
一个 Runnable
,让我剩下的代码继续回到主线程去执行。
6 协程的应用场景
6.1 从相册中读取图片并显示
从相册中直接读取图片,这是一个典型的IO操作使用场景,操作不当,可能会出现ANR。
版本1.0实现方式
val mImageUri = MediaStore.Images.Media.INTERNAL_CONTENT_URI
val bitmap = MediaStore.Images.Media.getBitmap(contentResolver, mImageUri)
imageView.setImageBitmap(bitmap)
版本2.0 我们可能会引入Handler
或 AysnTask
来通过异步的方式实现
版本3.0 我们可以这样用doAsync
实现 这种方式也不错
doAsync{
//后台执行
val mImageUri = MediaStore.Images.Media.INTERNAL_CONTENT_URI
val bitmap = MediaStore.Images.Media.getBitmap(contentResolver,mImageUri)
//回到主线程
uiThread{
imageView.setImageBitmap(bitmap)
}
}
版本4.0 时我们就可以用协程来实现。
val job = launch(Background) {
val mImageUri = MediaStore.Images.Media.INTERNAL_CONTENT_URI
val bitmap = MediaStore.Images.Media.getBitmap(contentResolver,mImageUri)
launch(UI) {
imageView.setImageBitmap(bitmap)
}
这里的参数Background是一个CoroutineContext对象,确保这个协程运行在一个后台线程,确保你的应用程序不会因耗时操作而阻塞和崩溃。你可以像下边这样定义一个CoroutineContext:
internal val Background = newFixedThreadPoolContext(2, "bg")
人个感觉 最后两种方式都可取。
6.2 Android Jetpack 中使用 kotlin 协程
后面介绍的三种使用方式在实现前需要分别添加以下的依赖包
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-rc02'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-rc02'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-rc02'
6.2.1在ViewModel中使用ViewModelScope
为应用程序中的每个ViewModel
定义ViewModelScope
。如果清除ViewModel
,则在此作用域中启动的任何协同程序都将自动取消。
当只有在ViewModel处于活动状态时才需要完成工作时,协程在这里非常有用。
例如,如果要为布局计算某些数据,则应将工作范围设置为ViewModel,以便在清除ViewModel时,自动取消工作以避免消耗资源。
可以通过ViewModel的viewModelScope属性访问ViewModel的协同作用域,如下例所示:
class MyViewModel :ViewModel() {
init {
viewModelScope.launch {
// Coroutine that will be canceled when the ViewModel is cleared.
}
}
}
6.2.2 在Activity或Fragment中使用LifecycleScope
为每个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)
}
}
}
6.2.3 使用LiveData
使用LiveData时,可能需要异步计算值。例如,您可能希望检索用户的首选项并将其提供给您的UI。在这些情况下,可以使用liveData builder函数调用suspend函数,将结果作为liveData对象提供。
在下面的示例中,loadUser()是在别处声明的挂起函数。使用liveData 构建函数异步调用loadUser(),然后使用emit()发出结果。
val user: LiveData = liveData {
val data = database.loadUser() // loadUser is a suspend function.
emit(data)
}
LiveData构建块充当协同路由和liveData之间的结构化并发原语。代码块在LiveData变为活动时开始执行,并且在LiveData变为非活动时经过可配置的超时后自动取消。如果在完成之前取消,则在LiveData再次激活时重新启动。如果在上一次运行中成功完成,则不会重新启动。请注意,只有在自动取消时才会重新启动。如果由于任何其他原因(例如抛出异常CancelationException)而取消块,则不会重新启动它。
也可以从块中发射多个值。每次emit()调用都会暂停块的执行,直到在主线程上设置LiveData值。
val user: LiveData = liveData {
emit(Result.loading())
try {
emit(Result.success(fetchUser()))
} catch(ioException: Exception) {
emit(Result.error(ioException))
}
}
我们也可以和 LifeCycle
中的Transformations
结合使用,如下例所示:
class MyViewModel: ViewModel() {
private val userId: LiveData = MutableLiveData()
val user = userId.switchMap { id ->
liveData(context = viewModelScope.coroutineContext + Dispatchers.IO) {
emit(database.loadUserById(id))
}
}
}
6.3 在Retofit 使用kotlin协程
retrofit 2.6.0(2019-06-05)中的更新日志如下:
Support
suspend
modifier on functions for Kotlin! This allows you to express the asynchrony of HTTP requests in an idiomatic fashion for the language.
@GET("users/{id}")
suspend fun user(@Path("id") id: Long): User
Behind the scenes this behaves as if defined asfun user(...): Call
and then invoked withCall.enqueue
. You can also returnResponse
for access to the response metadata.
在函数前加上 suspend
函数直接返回你需要对象类型不需要返回Call
对象
总结
本文总结了kotlin中的协程的相关知识点,协程是值得深入研究的。 未来的项目中运用是趋势所在,现将学习的心得总结于此,方便未来迭代中做为技术的储备。如有不足之处,欢迎留言讨论。
参考资料:
3.Kotlin 的协程用力瞥一眼 - 学不会协程?很可能因为你看过的教程都是错的
5.【码上开学】Kotlin 协程的挂起好神奇好难懂?今天我把它的皮给扒了