Kotlin协程

        要使用协程,需要额外引入指定的依赖,具体的版本可以查看google文档:

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'

参考:Android 上的 Kotlin 协程  |  Android 开发者  |  Android Developers

一、协程和线程的区别

        协程是跑在线程上的,一个线程可以同时跑多个协程,每一个协程则代表一个耗时任务,我们手动控制多个协程之间的运行、切换,决定谁什么时候挂起,什么时候运行,什么时候唤,协程在线程中是顺序执行的。Thread中我们有阻塞、唤醒的概念,协程里同样也有,区别是Thread的阻塞是会阻塞线程的,而协程的挂起不会阻塞线程不影响后面的协程的执行。

二、协程的启动

协程的启动需要依赖于上下文的环境,凭空运行一个协程有以下三中方式

1、协程的启动方式

(1)runBlocking:

        启动一个新协程,该协程是阻塞的,直到其内部所有逻辑及子协程逻辑全部执行完成,才会执行后面的,使用在主线程会阻塞主线程,所以开发中通常不会使用。

(2)GlobalScope.launch:

        启动了一个运行在子线程的顶层协程,协程的生命周期与应用程序一致。由于这样启动的协程存在启动协程的组件已被销毁但协程还存在的情况,使用不当会导致内存泄露。

(3)实现CoroutineScope + launch:

        在应用范围内启动一个新协程,不会阻塞主线程,这是在应用中最推荐使用的协程使用方式,自己的组件实现CoroutieScope接口,在需要的地方使用launch{}方法启动协程。使得协程和该组件生命周期绑定,组件销毁时,协程一并销毁。从而实现安全可靠地协程调用。

2、子协程的启动方式

(1)aunch

        异步启动一个子协程

(2)async{}

        异步启动一个子协程,并返回Deffer对象,可通过调用Deffer.await()方法等待该子协程执行完成并获取结果,常用于并发执行-同步等待的情况。

class CoroutinesTest1 {

    fun testRunBlocking() {
        runBlocking {
            Log.d(MainActivity.TAG, "方法1:runBlocking{}")
            launch {
                Log.d(MainActivity.TAG, "启动了第1个子协程")
            }
            launch {
                Log.d(MainActivity.TAG, "启动了第2个子协程")
            }
            val async = async {
                Log.d(MainActivity.TAG, "启动了第3个子协程,有返回值")
                "return--------我是第3个协程的返回值"
            }
            launch {
                Log.d(MainActivity.TAG, "启动了第4个子协程")
            }
            Log.d(MainActivity.TAG,  async.await())
            launch {
                Log.d(MainActivity.TAG, "启动了第5个子协程")
            }
        }
    }
}

打印结果:

Kotlin协程_第1张图片

3、启动时协程指定所在的线程 

        协程可以指定在某个线程中执行,可以在launch参数中指定,默认是子线程,不是主线程或者当前线程。

GlobalScope.launch(Dispatchers.Unconfined) {...}

        设置 CoroutineDispatcher 协程运行的线程调度器,有4种线程模式:

  • Dispatchers.Default
  • Dispatchers.IO -
  • Dispatchers.Main - 主线程
  • Dispatchers.Unconfined - 没指定,就是在当前线程

        不写的话就是 Dispatchers.Default 模式的,或者我们可以自己创建协程上下文,也就是线程池,newSingleThreadContext 单线程,newFixedThreadPoolContext 线程池等等。

launch(newSingleThreadContext("")){
    Log.d(MainActivity.TAG, "启动了第8个子协程---指定线程池newSingleThreadContext")
}

launch(Dispatchers.Main){
    Log.d(MainActivity.TAG, "启动了第9个子协程---指定线程Main线程")
}

4、启动时候指定其启动模式

        不指定默认是立即启动,可以在launch中指定启动方式,提供了如下:

  • DEAFAULT - 模式模式,不写就是默认
  • ATOMIC -
  • UNDISPATCHED
  • LAZY - 懒加载模式,你需要它的时候,再调用启动,入下:
launch(Dispatchers.IO, start = CoroutineStart.LAZY) {
    Log.d(MainActivity.TAG, "启动了第9个子协程---指定线程IO线程")
}.start()

三、协程的取消

        launch{}返回Job,async{}返回Deffer,Job和Deffer都有以下方法:

  • job.start() - 启动协程,除了 lazy 模式,协程都不需要手动启动
  • job.join() - 等待协程执行完毕
  • job.cancel() - 取消一个协程
  • job.cancelAndJoin() - 等待协程执行完毕然后再取消
val launch6 = async {
    Log.d(MainActivity.TAG, "启动了第6个子协程")
    delay(3000)
}
Log.d(MainActivity.TAG, "启动了第7个子协程,取消launch6")
launch6.cancel()

四、挂起函数suspend

        协程里使用suspend关键字修饰方法,该方法可以被协程挂起,挂起函数挂起协程,并不会阻塞协程所在的线程,例如协程的delay()挂起函数会暂停协程一定时间,并不会阻塞协程所在线程,但是Thread.sleep()函数会阻塞线程。没用suspend修饰的方法不能参与协程任务,suspend修饰的方法只能在协程中只能与另一个suspend修饰的方法交流,需要注意的是suspend方法只能在协程里面调用, 不能在协程外面调用。

fun test() {
    Log.d(MainActivity.TAG, "方法3:实现CoroutineScope + launch{}")
    launch{
        Log.d(MainActivity.TAG, "启动第1个协程")
    }
    launch {
        Log.d(MainActivity.TAG, "启动第2个协程")
        Log.d(MainActivity.TAG, "suspend挂起函数")
        val name = requestHost()
        Log.d(MainActivity.TAG, "suspend挂起函数之后的数据:$name")
    }
    launch{
        Log.d(MainActivity.TAG, "启动第3个协程")
    }
}

private suspend fun requestHost(): String {
    return async {
        "6666"
    }.await()
}

打印结果:

Kotlin协程_第2张图片

五、协程内部线程切换withContext

        withContext这个函数主要可以切换到指定的线程,并在闭包内的逻辑执行结束之后,自动把线程切回去继续执行。必须在协程或者suspend函数中调用,它必须显示指定代码块所运行的线程,它会阻塞当前上下文协程,会返回代码块的最后一行的值,如果最后一行没有返回值则返回Unit。

var withCxt  = withContext(Dispatchers.IO) {
    delay(1000)
    Log.d(MainActivity.TAG, "withContext---阻塞1秒")
    "返回最后一一行"
}
Log.d(MainActivity.TAG, "withContext---阻塞1秒----之后执行$withCxt")

打印结果:

Kotlin协程_第3张图片

 区别:

  • launch、async:启动一个新协程
  • withContext:不启动新协程,在原来的协程中切换线程,需要传入一个CoroutineContext对象
  • withContext、async:都可以返回耗时任务的执行结果。 一般来说,多个withContext任务是串行的,且withContext可直接返回耗时任务的结果。 多个async任务是并行的,async返回的是一个Deferred,需要调用其await()方法获取结果。

六、Activity、Fragment中协程的正确使用

       引入组件库:

implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0"
implementation "androidx.activity:activity-ktx:1.4.0"

        协程使用不当也会引起内存泄露的问题,在 Android 开发中,请使用 LifecycleOwner.addRepeatingJob、suspend Lifecycle.repeatOnLifecycle 或Flow.flowWithLifecycle 从 UI 层安全地收集数据流。

lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.flow.collect { 

        }
    }
}
lifecycleScope.launchWhenStarted {
    flow.collect {
    
    }
}
lifecycleScope.launch {
    flow.flowWithLifecycle(lifecycle,Lifecycle.State.STARTED)
        .collect {
     }
}

 这种方式非常推荐,原因如下:

  • 自动取消,不会造成内存泄漏。
  • 可以替代MainScope。
  • 可以基于指定的生命周期执行。

参考:List of KTX extensions  |  Android Developers

        除此之外,lifecycleScope还提供了更加精确的,带生命周期的创建函数、例如:

        表示onCreated()方法被调用之后这里的协程才会被创建并启动的launchWhenCreated

lifecycleScope.launchWhenCreated {  }

        表示onStarted()方法被调用之后这里的协程才会被创建并启动的launchWhenStarted        

lifecycleScope.launchWhenStarted {  }:

        表示onResumed()方法被调用之后这里的协程才会被创建并启动的launchWhenResumed

lifecycleScope.launchWhenResumed {  }

        lifecycleScope可以直接使用,也可以针对特定的生命周期控制,默认主线程,可以通过withContext来指定线程,通常有以下用法:

//在withContext中切换线程
lifecycleScope.launch {
    withContext(Dispatchers.IO) {
 
    }
}

//在launch时候指定线程
lifecycleScope.launch(Dispatchers.IO){
  
}

//不指定线程,默认主线程
lifecycleScope.launch {
    whenResumed {
        
    }
}

不指定线程,默认主线程
lifecycleScope.launchWhenResumed {
    
}

whenResumed和launchWhenResumed执行时机一样,区别在于:

  • whenResumed 可以有返回结果。
  • launchWhenResumed 返回的是Job对象。

七、其他独立类中的使用方法

1、方法一:通过job.cancel()来取消

        每当我们通过创建一个协程,就可以得到一个返回值job,然后我们在不需要协程的地方取消即可:job.cancel()

var job: Job = CoroutineScope(Dispatchers.Main).launch {
	......
}

fun onDestroy() {
    job.cancel()
}

2、方法二:通过MainScope()来处理

        创建一个在主线程上面运行的、在主线程上面启动所有协程的CoroutineScope对象,然后在其子类里面使用。然后他同样可以在onDestroy()方法里面调用cancel()方法取消,避免协程泄露。

val baseScope = MainScope()

使用的时候
baseScope.launch {
	......
}

然后统一是在onDestroy()方法里面取消CoroutineScope对象

override fun onDestroy() {
    super.onDestroy()
    baseScope.cancel()
}

        因为baseScope是创建在主线程的,在主线程启动协程的对象,因此这里我们可以不用像GlobalScope.launch(Dispatchers.Main){}和CoroutineScope(Dispatchers.Main).launch {}那样指定线程,省略了Dispatchers.Main。

3、方法三:通过CoroutineScope接口实现

        相比于上面的第二种方法,可以将协程提出到指定的类中,适合于更加解耦的情况,在需要启动的时候直接调用launch启动,也是在主线程。

class CoroutinesTest3() : CoroutineScope by MainScope(), LifecycleObserver {

    //手动销毁资源
    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    fun onDestroy() {
        cancel()
    }
    ………………省略………………
 }

4、方法四:通过viewModelScope对象

        viewModelScope是依赖于ViewModel的,而且最新的AndroidSDK的ViewModel.kt已经帮我们创建好了,因此可以直接在ViewModel里面调用或者通过ViewModel对象来调用。

class HomeViewModel :ViewModel() {
	suspend fun getName(): List {
        viewModelScope.launch{
			......
        }
    }
}

        ViewModel的生命周期跟Activity/Fragment的生命周期是同步的,因此,viewModelScope对象创建的协程系统会自动帮我们管理好,不用我们去关心内存泄露问题。

5、方法五:通过lifecycleScope对象

        lifecycleScope是Lifecycle的拓展函数,是Lifecycle对协程的支持,因此我们可以直接在Activity/Fragment调用lifecycleScope来创建、启动并管理协程,我们可以在任何生命周期中调用,优先推荐使用该方式。该功能需要额外引入ktx包:

implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.0"

lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
         updateUi()
    }
}

七、协程的异常处理

1、捕获的方式

        如果协程内部没有通过try-catch处理异常,那么异常并不会被重新抛出或者被外部的try-catch捕获。异常将会在job层级结构中向上传递,将会被安装的CoroutineExceptionHandler处理,如果没有安装过,异常将会被线程的未捕获的异常处理器处理。

//通过这种方式是无法捕获协程的异常的
val topLevelScope = CoroutineScope(Job())
topLevelScope.launch {
    try {
        throw RuntimeException("RuntimeException in coroutine")
    } catch (exception: Exception) {
        println("Handle $exception")
    }
}

/**
 * 协程的异常捕获
 */
fun testGlobalScope2(){
    //协程的异常处理
    val topLevelScope = CoroutineScope(Job())
    topLevelScope.launch(coroutineExceptionHandler) {
        launch(coroutineExceptionHandler) {
            throw RuntimeException("协程发生异常")
        }
    }
}
private val coroutineExceptionHandler = CoroutineExceptionHandler() { _, exception ->
    Log.d(MainActivity.TAG, "Handle $exception in CoroutineExceptionHandler")
}

2、try-cach VS CoroutineExceptionHandler

        如果你想要重试某些操作,或者在协程完成之前执行某些操作,那么可以考虑使用try-cach。需要注意的是,在协程内部使用了try-cach捕获该异常之后,那么这个异常将不会再向上传递,也不能使用利用结构性并发的取消函数。在i协程因异常结束需要打印异常信息的时候可以考虑使用CoroutineExceptionHandler。

3、coroutineScope{}的异常处理特性

        coroutineScope{}会重新抛出失败子协程内的异常而不是将其继续向上传递,这样我就可以自己处理失败子协程的异常了。在try-cach内启动的协程内的异常不能被捕获,但如果在失败的协程外部套上coroutineScope{}函数,那就会不太一样了:

topLevelScope.launch {
    try {
        coroutineScope {
            launch {
                throw RuntimeException("RuntimeException in nested coroutine")
            }
        }
    } catch (exception: Exception) {
        println("Handle $exception in try/catch")
    }
}

        catch成功捕获了异常,这是因为coroutineScope{}将失败的子协程内部的异常抛出,而没有继续向上传递。

4、supervisorScope{}异常处理特性

        作用域函数supervisorScope{}会在job层级中安装一个独立的新的子作用域,并使用SupervisorJob作为该作用域的job,这个新的作用域并不会将异常继续向上传递,异常将由它自己处理。在supervisorScope内直接启动的协程将作为顶级协程。顶级协程在由launch或者async启动的时候,它的表现和作为子协程时的表现将有所不同。

参考文章:

  • https://blog.csdn.net/haoyuegongzi/article/details/108834032
  • https://www.jianshu.com/p/d661a56031f4
  • https://blog.csdn.net/zou8944/article/details/106447727
     

你可能感兴趣的:(kotlin,kotlin,android,java)