Kotlin进阶指南 - 协程入门
-
-
- 协程是什么?
- 为什么要使用协程?
- 如何使用协程?
- 如何避免协程泄露、内存泄露?
- Jetpack AAC 哪些组件支持协程?
-
协程是什么?
关于协程,其实在Lua语言、Python语言、Go语言、Java语言中都早已存在,Android中是在Kotlin 1.3版本
后引入了协程,只是因为当时Kotlin
都还没有普及,所以了解协程的人更少了,虽然2018协程已经有了初期稳定版本,但是依旧普及率不高…
协程(coroutines) 是由 JetBrains 开发的丰富的协程库,英语好的同学可以看官网学学基础使用
如果英文不好的话,看看官网中文版的Kotlin协程使用指导吧
在Google中有出过一篇:如何在 Android 应用中使用 Kotlin 协程
话说,android的协程主要体现在Kotlin
语言方面,众所周知Kotlin
也就是近两三年开始普及的,那么现在掌握协程也是必不可少的技能了
Kotlin
协程:我认为Kotlin协程,更多的时候代表的是一个轻量级的线程库或者说是线程框架
初步特征
- 协程是运行在单线程中的并发程序,意味着它的体量比线程更小
- 协程支持自动切换线程,子主线程可随意切换
关于协程环境主要涉及到了Dispatchers调度器
,常见有三种环境(最后一种不靠谱- - )
Dispatchers.Main
:调用程序在Android 中的主线程Dispatchers.IO
:适合主线程之外的操作,主要针对磁盘和网络 IO 进行了优化,适合 IO 密集型的任务,比如:读写文件,操作数据库以及网络请求Dispatchers.Default
:适合 CPU 密集型的任务,比如计算,json数据的解析,以及列表的排序,Dispatchers.Unconfined
:在调用的线程直接执行
协程 - 启动模式(枚举)
public enum class CoroutineStart {
DEFAULT,
LAZY,
@ExperimentalCoroutinesApi
ATOMIC,
@ExperimentalCoroutinesApi
UNDISPATCHED;
}
四种启动模式,含义如下
DEFAULT
:默认的模式,立即执行协程体LAZY
:只有在需要的情况下运行ATOMIC
:立即执行协程体,但在开始运行之前无法取消UNDISPATCHED
:立即在当前线程执行协程体,直到第一个 suspend 调用
为什么要使用协程?
关于协程的特点,Google早有说明
轻量
:您可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作。内存泄漏更少
:使用结构化并发机制在一个作用域内执行多项操作。内置取消支持
:取消操作会自动在运行中的整个协程层次结构内传播。Jetpack 集成
:许多 Jetpack 库都包含提供全面协程支持的扩展。某些库还提供自己的协程作用域,可供您用于结构化并发。
话说,协程在写法上允许在不同线程的代码,写在同一个代码块中,还是比较方便的 (这点比较符合内存泄露更少的描述
)~
可能很多人都会有一些和我一样的疑问 - 如果只是线程框架的话,为何不继续使用Thread?如果只是为了方便线程切换的话,为何继续使用RxJava?
- 协程是基于线程的,意味着
协程体量比线程要小(看下图秒懂)
,但是关于性能提升,并不明显; - 协程提供了专属的
Dispatchers
可满足不同场景的线程使用,可及时切换线程; - 协程隶属
Jetpack
组件库,首先Jetpack
组件库是Google首推,同时Jetpack
的组件被使用率很高 - 协程兼容了
Lifecycle
、ViewModel
、LiveData
等组件库,现在这些组件库已经都开始支持协程的使用了
这里借用一下网图,说明线程和协程运行的环境
线程运行环境
-
-
协程运行环境
如何使用协程?
如果你准备开始使用协程的话,最好是有一定的Kotlin
基础,同时对Jetpack相关组件
的了解,它会使你事半功倍
开启协程的方式
通常有俩种
,其一是launch函数
,其二是async函数
launch
更多是用来发起一个无需结果的耗时任务(如批量文件删除、创建),这个工作不需要返回结果。async
则是更进一步,用于异步执行耗时任务,并且需要返回值(如网络请求、数据库读写、文件读写),在执行完毕通过await()
函数获取返回值。
我们可以在协程中动态切换对应任务的执行环境,主要是通过withContext(Dispatchers.环境)方式
协程需要运行在协程上下文环境,在非协程环境中凭空启动协程
- 三种方式
runBlocking
建立新的协程,运行在当前线程上,因此会堵塞当前线程,直到协程体结束GlobalScope.launch
启动一个新的线程,在新线程上建立运行协程,不堵塞当前线程GlobalScope.asyn
启动一个新的线程,在新线程上建立运行协程,而且不堵塞当前线程,支持 经过await获取返回值
协程中的任务如何挂起和恢复?
协程中进行协程切换的场景,主要涉及到suspend
和resume
,意图是在挂起函数执行完毕之后,协程会自动的重新切回它原先的线程(注意:普通函数没有suspend
和resume
这两个特性)
关于suspend
挂起函数,更多是作为一个标记和提醒,提醒调用者我是需要耗时操作,需要用挂起的方式,在协程中使用放在后台执行
suspend
用于暂停执行的当前协程,并保存所有的局部变量resume
用于已暂停的协程中暂停出恢复
add 依赖
//新版本,慎用,不知有没有坑,介意的话可以使用1.1.1版本
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1'
先讲解launch的使用
- 在主线程中通过
GlobalScope.launch(Dispatchers.Main) {}
启动协程,这里要注意我们一般将上下文环境设为Dispatchers.Main
- 关于子线程(IO线程)执行函数,我们首先需要用
suspend
进行修饰为挂起函数,同时在内部通过withContext
进行线程切换
主要使用GlobalScope.launch函数
开启全局范围的协程,而其参数我们一般使用的是Dispatchers.Main
,意味着主线程协程
GlobalScope.launch(Dispatchers.Main) {
//子线程函数
ioThread()
//主线程函数
mainThread()
}
一般子线程方法,我们使用suspend
挂起函数,结合withContext
切换子主线程
//IO线程执行的挂起函数,内部通过withContext声明内部逻辑在子线程执行,执行完毕后会自动切回主线程
suspend fun ioThread() {
withContext(Dispatchers.IO) {
print("IOThread: ${Thread.currentThread().name}")
}
}
像上方的写法你可能感觉不到协程的快感,那么你在看看下方同等代码
GlobalScope.launch {
//子线程任务
withContext(Dispatchers.IO) {
print("IOThread: ${Thread.currentThread().name}")
}
//自动切回主线程
print("MainThread:+${Thread.currentThread().name}")
//子线程任务
withContext(Dispatchers.IO) {
print("IOThread: ${Thread.currentThread().name}")
}
}
在讲解async的使用
首先async
执行的协程是支持通过await()
返回数据的,同时async也常用于并行任务
,我们可以同步执行多个协程任务,最后一起同步返回
协程的suspend挂起函数,除了本身提醒的作用外,一般涵盖着线程切换
//并发请求
val asyncLaunch = GlobalScope.launch {
val async = async { add1() }
val async1 = async { add2() }
System.out.println(async.await() + async1.await())
}
suspend fun add1(): Int {
delay(1000L)
return 10 + 10;
}
suspend fun add2(): Int {
delay(2000L)
return 5 + 8;
}
在文档中有一种通过awaitAll
批量获取async
数据的方式,有兴趣可以学学,简单方便
suspend fun fetchTwoDocs() = // called on any Dispatcher (any thread, possibly Main)
coroutineScope {
val deferreds = listOf( // fetch two docs at the same time
async { fetchDoc(1) }, // async returns a result for the first doc
async { fetchDoc(2) } // async returns a result for the second doc
)
deferreds.awaitAll() // use awaitAll to wait for both network requests
}
如何避免协程泄露、内存泄露?
首先想一下我们常规是如何避免内存泄漏的?嗯… 有点墨迹了,其实大多是在onDestroy中将组件cancle或将数据设置为null
等~
通过launch函数
查看内部源码可以发现它会返回一个Job对象
,那么我们在往内部看一看
查看Job对象内部可以看出Job是拥有cancel方法的
,那么我们完全可以在组件的onDestroy中取消协程,避免协程内部持续引用外部对象而造成泄露
-
故此,我们可以直接获取协程的对象,然后调用cancel的方法,从而防止内存泄露;不过现在使用Lifecycle更便捷一些
//开启协程
val job = GlobalScope.launch {
//子线程任务
withContext(Dispatchers.IO) {
print("IOThread: ${Thread.currentThread().name}")
}
//自动切回主线程
print("MainThread:+${Thread.currentThread().name}")
//子线程任务
withContext(Dispatchers.IO) {
print("IOThread: ${Thread.currentThread().name}")
}
}
//取消协程
job.cancel()
兴趣扩展
Job
:协程构建函数的返回值,可以把 Job 看成协程对象本身,协程的操作方法都在 Job 身上了
job.start()
- 启动协程,除了 lazy 模式,协程都不需要手动启动job.join()
- 等待协程执行完毕job.cancel()
- 取消一个协程job.cancelAndJoin()
- 等待协程执行完毕然后再取消
Jetpack AAC 哪些组件支持协程?
谈支持协程的组件前,还是想说一下GlobeScope
不受欢迎的原因 - - ~
GlobeScope
:生命周期与app同步,随着kotlin的更新,已经慢慢不推荐使用这个了
-
不推荐的原因:主要是很难避免因自己失误操作,出现的内存泄漏问题
-
推荐原因:个人认为作为新手入门使用的
Scope
还是可以的
话说回来,目前来看AAC组件库中的Lifecycle、ViewModel、LiveData
都已经开始支持协程的使用了,也都提供了对应的协程调用方式
使用 lifecycleScope
或 viewModelScope
,最好有 Lifecycle 、ViewModel 基础,记得加入以下依赖
//lifecycleScope
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0'
//viewModelScope
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
lifecycleScope
:在activity
或者fragment
里面使用协程的时候,用lifecycleScope
,它在Lifecycle
执行,onDestory
的时候取消
半夜了,有点无聊,我们看看lifecycleScope
提供的方法,内部封装了启动协程的生命周期,又一次可以偷懒了…
LifecycleCoroutineScope
内涵盖方法
无聊,写个样例,我们可以通过lifecycleScope
动态设置启动协程的时间
val launchWhenCreated = lifecycleScope.launchWhenCreated {
print("半夜咯")
suspend {
withContext(Dispatchers.IO){
print("睡觉吧")
}
}
print("晚安")
}
override fun onDestroy() {
super.onDestroy()
launchWhenCreated.cancel()
}
viewModelScope
:viewModelScope
只能在ViewModel
里面使用协程,它会在ViewModel
调用clear
方法的时候取消;这方便近期忙着学习,还没有用到,等以后我回头补全