Android中kotlin协程
协程是一种并发设计模式,可以在 Android 平台上使用它来简化异步执行的代码
在 Android 上,协程有助于管理长时间运行的任务,如果管理不当,这些任务可能会阻塞主线程并导致应用无响应。
特点:
-
轻量
线程:
子线程与主线程都是独立的,可以创建多个子线程,创建和销毁线程都会消耗资源,
协程:可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作。
依赖项信息
若需要在Android项目中使用,需要在build.gradle
中添加以下依赖(版本可能会有变化)
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
}
协程域CoroutineScope
suspend
修饰的函数方法,即"挂起函数",这个函数耗时比较长,不希望阻塞主线程
挂起函数代码块必须在CoroutineScope
中执行
下面写了一个count()
方法,用于每200ms修改mTextView的文字
suspend fun count() {
for (i in 1..100) {
mTextView.text = "$i"
Thread.sleep(200)
}
}
协程的使用
创建协程域
通过CoroutineScope
方法创建协程域
协程上下文CoroutineContext
: 这个协程在哪个线程上执行
Didpatchers.IO
子线程
Dispatchers.Main
UI主线程
dispatchers.Default
默认
下面的代码就是在主线程中创建了一个协程域
val scope = CoroutineScope(Dispatchers.Main)
开启任务
launch同步任务
使用launch
开启一个同步任务: a -> b -> c 按顺序执行任务
launch
方法中可以传入CoroutineScope
参数, 声明任务在哪个线程执行,不传入则默认在scope的区域执行
scope.launch() { //任务默认就在scope所在区域执行
count()
}
查看当前线程:Thread.currentThread().name
运行上面的代码后会报错
注意报错:Only the original thread that created a view hierachy can touch its views
在主线程中操作时还是会阻塞线程, 而在子线程执行时,是不能操作UI的,那应该怎么解决呢?可以通过withContext()
切换线程 ,即切换代码执行时的协程域
将count
方法修改为:
suspend fun count() {
for (i in 1..100) {
withContext(Dispatchers.Main) { //切换上下文
mTextView.text = "$i" //在主线程
}
delay(1000) //Thread.sleep(1000) 子线程
}
}
这里只有改变mTextView.text
是在主线程中操作的,其他操作都是在子线程,既防止了阻塞主线程,又解决了不能在子线程对UI进行操作的问题,这里还用了delay()
方法来进行延时操作,注意这也是一个挂起函数,需要在协程域中执行
launch
同步执行,会有一个类型为Job
的返回值,
val job = scope.launch(Dispatchers.IO) {
count()
}
如果当有多个任务,需要一个任务做完再做下一个任务,该怎么办呢?
Job
的join
方法可以将协程挂起,直到任务结束
看下面的代码:
val job1 = scope.launch(Dispatchers.IO){ //任务默认就在scope所在的区域执行
count1()
}
val job2 = scope.launch(Dispatchers.IO){
job1.join()
count2()
}
suspend fun count1(){
for (i in 1..5){
withContext(Dispatchers.Main){//切换上下文 切换代码执行的scope 切换线程
mTextView.text = "$i"
}
delay(500)
}
}
suspend fun count2(){
for (i in 11..15){
withContext(Dispatchers.Main){//切换上下文 切换代码执行的scope 切换线程
mTextView.text = "$i"
}
delay(1000)
}
}
通过Job
的join
方法,将协程挂起,直到任务完成,这样job1执行后,job2才会执行
由于资源的抢夺,每次运行的结果可能会不同
async异步任务
使用async
开启一个异步任务:a b c 同时做
有返回值 Deferred
,当执行任务后需要返回值,使用async
val sum1 = scope.async { //开启一个异步任务
var sum = 0
for (i in 1..100){
sum += i
}
sum
}
async
中最后一行代码就是返回值如果是T,其返回值为Defferred<T>
,可通过.await
方法获取原来类型的值
看下面的代码,这里通过async
开启了一个异步任务,通过await
获取sum1
执行后的结果,再进行后面的操作
val sum2 = scope.async { //开启一个异步任务
var sum = sum1.await()
for (i in 1..100) {
sum += i
}
sum
}
注意:await
方法也是挂起函数,需要在协程域中执行
scope.launch {
Log.v("test", "sum2: ${sum2.await()}")
}
Flow数据流
当调用一个方法,在不同的时间/情况(异步),可能需要放回多个值,在之前,我们可以通过List等方法回调,但在挂起函数中,往往不能很快获得所有结果,这里就可以使用Flow回调数据
看下面的代码:
withContext
代码块,block
最后一句即是返回值
这里调用loadDataFromDB
方法,将返回值给TextView显示,如果想要异步返回多个值时,怎么办呢?
CoroutineScope(Dispatchers.Main).launch {
val value = loadDataFromDB()
mTextView.text = "$value"
}
//这个方法是在IO线程执行
suspend fun loadDataFromDB(): Int = withContext(Dispatchers.IO) {
//....
10
}
这里就需要Flow
数据流,FLow与枪类比,枪里面有很多子弹,但是统一时刻,只能打出来一颗,通过不断开枪将所有的子弹打出来,外部可以接收打出的子弹
suspend fun loadDatas(): Flow<Int> = withContext(Dispatchers.IO) {
//将子弹放到枪中 直接开枪
flowOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
}
CoroutineScope(Dispatchers.Main).launch {
val gunFlow = loadDatas()
//接收发射出来的子弹 (订阅)
gunFlow.collect {
mTextView.text = "$it"
delay(500)
}
}
在上面的代码中,通过conllect
订阅数据流,将每个数据流显示在textView中,由于flowOf开枪太快,这里使用delay
进行了延时操作,方便看到textView的变化
看一下flowOf
的源代码
public fun <T> flowOf(vararg elements: T): Flow<T> = flow {
for (element in elements) {
emit(element)
}
}
在flowOF
内部,已经通过 emit
将每个数据发出了,即"自动开枪"
emit()
发射新的数据collect
订阅/接收数据flow{}
创建flow对象
suspend fun loadDatas(): Flow<Int> = withContext(Dispatchers.IO) {
//将子弹放到枪中
flow {
for (i in 1..10) {
emit(i) //发射
delay(500)
}
}
}
CoroutineScope(Dispatchers.Main).launch {
val gunFlow = loadDatas()
//接收发射出来的子弹 (订阅)
gunFlow.collect {
mTextView.text = "$it"
}
}
Activity和Fragment的协程域
自己创建的协程域需要自己管理这个协程的生命周期(创建 -> 销毁)
需要自己释放,在Activity和Fragment中提供了协程域,不需要我们自己释放,用起来更加方便
导入依赖
implementation('androidx.lifecycle:lifecycle-runtime-ktx:2.6.1')
在activity和fragment等LifecycleOwner
中,可以直接通过lifecycleScope
获取提供的协程域
lifecycleScope.launch(Dispatchers.IO) {
withContext(Dispatchers.Main) {
}
}
ViewModel的协程域
和Activity类似,ViewModel也提供了协程域
导入依赖
implementation('androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1')
通过viewModelScope
获取协程域
viewModelScope.lauch {
}
协程的使