Kotlin协程运用实践

2020/6/20 23:26:38

本文主要是介绍Kotlin协程运用实践,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

一、协程的定义:

1、官方文档的描述

      协程通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、订阅相关事件、在不同线程(甚至不同机器)上调度执行,而代码则保持如同顺序执行一样简单。

协程的开发人员 Roman Elizarov 是这样描述协程的:协程就像非常轻量级的线程。线程是由系统调度的,线程切换或线程阻塞的开销都比较大。而协程依赖于线程,但是协程挂起时不需要阻塞线程,几乎是无代价的,协程是由开发者控制的。所以协程也像用户态的线程,非常轻量级,一个线程中可以创建任意个协程。

总而言之:协程可以简化异步编程,可以顺序地表达程序,协程也提供了一种避免阻塞线程并用更廉价、更可控的操作替代线程阻塞的方法 -- 协程挂起。

2、协程的基本概念

(1) 挂起函数

挂起函数能够以与普通函数相同的方式获取参数和返回值,但是调用函数可能挂起协程(如果相关调用的结果已经可用,库可以决定继续进行而不挂起),挂起函数挂起协程时,不会阻塞协程所在的线程。挂起函数执行完成后会恢复协程,后面的代码才会继续执行。但是挂起函数只能在协程中或其他挂起函数中调用。事实上,要启动协程,至少要有一个挂起函数,它通常是一个挂起 lambda 表达式。所以suspend修饰符可以标记普通函数、扩展函数和 lambda 表达式。

(2)launch函数

 先从新建一个协程开始分析协程的创建,最常见的协程创建方式为CoroutineScope.launch {},关键源码如下:

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    ...
    coroutine.start(start, coroutine, block)
    return coroutine
}复制代码

从上面函数定义中可以看到协程的一些重要的概念:

CoroutineScope,可以理解为协程本身,包含了 CoroutineContext。

CoroutineContext,协程上下文,是一些元素的集合,主要包括 Job 和 CoroutineDispatcher 元素,可以代表一个协程的场景。

EmptyCoroutineContext 表示一个空的协程上下文。

CoroutineDispatcher,协程调度器,决定协程所在的线程或线程池。它可以指定协程运行于特定的一个线程、一个线程池或者不指定任何线程(这样协程就会运行于当前线程)。coroutines-core中 CoroutineDispatcher 有三种标准实现Dispatchers.DefaultDispatchers.IODispatchers.MainDispatchers.Unconfined,Unconfined 就是不指定线程。

launch函数定义如果不指定CoroutineDispatcher或者没有其他的ContinuationInterceptor,默认的协程调度器就是Dispatchers.DefaultDefault是一个协程调度器,其指定的线程为共有的线程池,线程数量至少为 2 最大与 CPU 数相同。

Job,任务,封装了协程中需要执行的代码逻辑。Job 可以取消并且有简单生命周期,它有isActive、isCompleted、isCancelled三种状态。Job 完成时是没有返回值的,如果需要返回值的话,应该使用 Deferred,它是 Job 的子类public interface Deferred<out T> : Job

CoroutineScope.launch函数属于协程构建器 Coroutine builders,不阻塞当前线程,在后台创建一个新协程,也可以指定协程调度器,例如在 Android 中常用的GlobalScope.launch(Dispatchers.Main) {}

(3)async 函数

CoroutineScope.async {}可以实现与 launch builder 一样的效果,在后台创建一个新协程,唯一的区别是它有返回值,因为CoroutineScope.async {}返回的是 Deferred 类型。

获取CoroutineScope.async {}的返回值需要通过await()函数,它也是是个挂起函数,调用时会挂起当前协程直到 async 中代码执行完并返回某个值。

(4)runBlocking 函数

runBlocking {}是创建一个新的协程同时阻塞当前线程,直到协程结束。这个不应该在协程中使用,主要是为main函数和测试设计的。

(5)withContext 函数

withContext {}不会创建新的协程,在指定协程上运行挂起代码块,并挂起该协程直至代码块运行完成。

二、协程之间的关系

官方文档中有提到协程之间可能存在父子关系,取消父协程时也会取消所有子协程,所以协程间父子关系有三种影响:

  • 父协程手动调用cancel()或者异常结束,会立即取消它的所有子协程。

  • 父协程必须等待所有子协程完成(处于完成或者取消状态)才能完成。

  • 子协程抛出未捕获的异常时,默认情况下会取消其父协程。

launchasync新建协程时,首先都是newCoroutineContext(context)新建协程的 CoroutineContext 上下文,新的协程的 CoroutineContext 都继承了原来 CoroutineScope 的 coroutineContext,GlobalScope和普通协程的CoroutineScope的区别,GlobalScope的 Job 是为空的,GlobalScope.launch{}GlobalScope.async{}新建的协程是没有父协程的。
对于协程的取消,cancel()只是将协程的状态修改为已取消状态,并不能取消协程的运算逻辑,协程库中很多挂起函数都会检测协程状态,如果想及时取消协程的运算,最好使用isActive判断协程状态。

三、协程中的异常处理

首先协程运算过程中所有未捕获异常其实都会在第二层包装中被捕获,所以出现未捕获异常时,首先会取消所有子协程,然后可能会取消父协程。而有些情况下并不会取消父协程,一是当异常属于 CancellationException 时,二是使用SupervisorJobsupervisorScope时,子协程出现未捕获异常时也不会影响父协程,它们的原理是重写 childCancelled() 为

override fun childCancelled(cause: Throwable): Boolean = false

launch式协程和async式协程都会自动向上传播异常,取消父协程。

除非是 SupervisorJob 和 supervisorScope,一般协程出现未捕获异常时,不仅会取消父协程,一步步取消到最根部的协程,而且最后还由最根部的协程(Root Coroutine)处理协程。
默认的handleJobException的实现为空,所以如果 Root Coroutine 为async式协程,不会有任何异常打印操作,也不会 crash,但是为launch式协程或者actor式协程的话,会调用handleExceptionViaHandler()处理异常。

默认情况下,launch式协程对未捕获的异常只是打印异常堆栈信息,如果在 Android 中还会调用uncaughtExceptionPreHandler处理异常。但是如果使用了 CoroutineExceptionHandler 的话,只会使用自定义的 CoroutineExceptionHandler 处理异常。

async式协程只有通过await()将异常重新抛出,不过可以可以通过try { deffered.await() } catch () { ... }来捕获异常

处理异常时,用coroutineScope或者使用SupervisorJob包装异步调用,通过用coroutineScope包裹async{},当异常发生在async{}内部时,它将会取消这个域内创建的所有其他协程,而没有影响外面的域。

四、协程的并发处理

线程中锁都是阻塞式,在没有获取锁时无法执行其他逻辑,而协程可以通过挂起函数解决这个,没有获取锁就挂起协程,获取后再恢复协程,协程挂起时线程并没有阻塞可以执行其他逻辑。这种互斥锁就是Mutex,它与synchronized关键字有些类似,还提供了withLock扩展函数,替代常用的mutex.lock; try {...} finally { mutex.unlock() }:

fun main(args: Array<String>) = runBlocking<Unit> {
    val mutex = Mutex()
    var counter = 0
    repeat(10000) {
        GlobalScope.launch {
            mutex.withLock {
                counter ++
            }
        }
    }
    println("The final count is $counter")
}复制代码

五、通过协程封装Retrofit网络请求框架

 引入JakeWharton 的开源库让Retrofit 直接返回 Deferred<T>:

implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
implementation 'com.squareup.retrofit2:retrofit:2.6.1'
implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2'
复制代码

/** 
* 用户登录接口 
* 
* @param data 
* @return 
*/
@POST("/api/login/")
fun loginAsync(@Body data: LoginBean): Deferred<Response<LoginResponse>>复制代码

fun buildRetrofit(host: String): Retrofit {
    return Retrofit.Builder()
        .baseUrl(host)
        .addConverterFactory(GsonConverterFactory.create())
        .addCallAdapterFactory(CoroutineCallAdapterFactory())
        .client(okHttpClient!!)
        .build()
}复制代码

设计BaseViewModel公共基类并继承LifecycleObserver:

open class BaseViewModel : ViewModel(), LifecycleObserver {
    private val viewModelJob = SupervisorJob()
    protected val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
    protected val ioScope = CoroutineScope(Dispatchers.IO + viewModelJob)

    var httpStatus: MutableLiveData<HttpStatus> = MutableLiveData()

    suspend fun <T>invokeHttpRequest(job: Deferred<Response<T>>): T?{
        var body: T? = null
        try {
            job.await().apply {
                if (isSuccessful){
                    if(body() == null){
                        uiScope.launch {
                            Toast.makeText(App.getInstance().applicationContext,
                                "服务器无返回值!",Toast.LENGTH_SHORT).show()
                        }
                    }else{
                        body = body()
                    }
                }else{
                    val status = HttpStatus(code(),message())
                    httpStatus.postValue(status)
                    uiScope.launch {
                        Toast.makeText(App.getInstance(),"HTTP状态码${status.code}:${status.message}",
                            Toast.LENGTH_SHORT).show()
                    }
                }
            }
        }catch (e: Exception){
            println(e.message)
            uiScope.launch {
                Toast.makeText(App.getInstance(),"网络连接失败!", Toast.LENGTH_SHORT).show()
            }
        }
        return body
    }


    override fun onCleared() {
        super.onCleared()
        viewModelJob.cancel()
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    open fun onStop() {
        viewModelJob.cancelChildren()
    }
}复制代码

在BaseViewModel的子类中实现相关的业务逻辑

class LoginViewModel: BaseViewModel() {

    var status: MutableLiveData<Boolean> = MutableLiveData()

    fun login(account: String?,  password: String?){
        uiScope.launch {
            App.getInstance().run {
                if (account == null || account.trim() == ""){
                    Toast.makeText(this, getString(R.string.empty_account), Toast.LENGTH_SHORT).show()
                    status.value = false
                    return@launch
                }
                if (password == null || password.trim() == ""){
                    Toast.makeText(this, getString(R.string.empty_password), Toast.LENGTH_SHORT).show()
                    status.value = false
                    return@launch
                }
            }
            ioScope.launch {
                val result = invokeHttpRequest(BaseApiService.instance
                    .getApiImp(EduApi::class.java)
                    .loginAsync(LoginBean(account?.trim(),password?.trim())))
                result?.run {
                    status.postValue(status_code == 200)
                    if(status.value == true){
                        saveJwtToken(this)
                    }
                }
            }
        }
    }
}复制代码

在视图层运用ViewModel的观察者模式,实现网络请求调用和对返回数据接收,并将ViewModel绑定LifecycleOwner的生命周期,在页面销毁时取消网络请求。

private fun initViewModel(){
    viewModel = ViewModelProvider(this).get(LoginViewModel::class.java)
    lifecycle.addObserver(viewModel)
    viewModel.status.observe(this, Observer{
        if (it){
            startActivity(Intent(this,MainActivity::class.java))
            finish()
        }else{
            Toast.makeText(this, getString(R.string.login_fail), Toast.LENGTH_SHORT).show()
        }
        ivLoading.visibility = View.INVISIBLE
    })
}复制代码

六、进一步理解CoroutineDispatcher

CoroutineDispatcher 定义了 Coroutine 执行的线程。CoroutineDispatcher 可以限定 Coroutine 在某一个线程执行、也可以分配到一个线程池来执行、也可以不限制其执行的线程。

CoroutineDispatcher 是一个抽象类,所有 dispatcher 都应该继承这个类来实现对应的功能。标准库中提供了下面几个常用的实现:

  • Dispatchers.Default — 如果创建 Coroutine 的时候没有指定 dispatcher,则一般默认使用这个作为默认值。Default dispatcher 使用一个共享的后台线程池来运行里面的任务。
  • Dispatchers.IO — 顾名思义这是用来执行阻塞 IO 操作的,也是用一个共享的线程池来执行里面的任务。根据同时运行的任务数量,在需要的时候会创建额外的线程,当任务执行完毕后会释放不需要的线程。通过系统 property kotlinx.coroutines.io.parallelism 可以配置最多可以创建多少线程,在 Android 环境中我们一般不需要做任何额外配置。
  • Dispatchers.Unconfined — 立刻在启动 Coroutine 的线程开始执行该 Coroutine直到遇到第一个 suspension point。也就是说,coroutine builder 函数在遇到第一个 suspension point 的时候才会返回。而 Coroutine 恢复的线程取决于 suspension function 所在的线程。 一般而言我们不使用 Unconfined
  • 通过 newSingleThreadContextnewFixedThreadPoolContext 函数可以创建在私有的线程池中运行的 Dispatcher。由于创建线程比较消耗系统资源,所以对于临时创建的线程池在使用完毕后需要通过 close 函数来关闭线程池并释放资源。
  • 通过 asCoroutineDispatcher 扩展函数可以把 Java 的 Executor 对象转换为一个 Dispatcher 使用。
  • Dispatchers.Main — 是在 Android 的 UI 线程执行。

由于子Coroutine 会继承父Coroutine 的 context,所以为了方便使用,我们一般会在 父Coroutine 上设定一个 Dispatcher,然后所有 子Coroutine 自动使用这个 Dispatcher。

七、CoroutineContext的含义

在前面介绍的 coroutine builder 函数中,都需要一个 CoroutineContext 参数。CoroutineContext 是很重要的一部分内容。

CoroutineContext 包含了一些用户定义的数据集合,这些数据和当前的 Coroutine 关联。CoroutineContext 和线程的 Thread-local 变量概念类似,区别在于 Thread-local 是可以被修改的而 CoroutineContext 是不可变(immutable)的。由于 CoroutineContext 是非常轻量级的实现,如果遇到 CoroutineContext 需要变化的时候, 只需要使用新的 context 重新创建一个 Coroutine 就可以了。

CoroutineContext 是一个被索引的 Element set 集合,里面的每个元素(Element)都有一个唯一的 Key。定义为一个 set 和 map 的混合,这样里面的每个元素都和 map 一样有个对应的 key,而每个 key 又像 set 一样直接和这个元素关联。

CoroutineContext 有两个非常重要的元素 — Job 和 Dispatcher,Job 是当前的 Coroutine 实例而 Dispatcher 决定了当前 Coroutine 执行的线程。

CoroutineContext 定义了四个核心的操作:

  • 操作符(Operator)get 可以通过 key 来获取这个 Element。由于这是一个 get 操作符,所以可以像访问 map 中的元素一样使用 context[key] 这种中括号的形式来访问。
  • 函数 foldCollection.fold 扩展函数类似,提供便利当前 context 中所有 Element 的能力。
  • 操作符 plusSet.plus 扩展函数类似,返回一个新的 context 对象,新的对象里面包含了两个里面的所有 Element,如果遇到重复的(Key 一样的),那么用+号右边的 Element 替代左边的。
  • 函数 minusKey 返回删除一个 Element 的 context。

通过上面这些函数,context 可以很方便的组合使用,比如一个库定义了一个用来保存已经登录用户 id 的 auth Element,而另外一个库定义了一个包含一些执行信息的 threadPool Element, 可以通过 + 号来把这两个 context 组合一起使用:launch(auth + threadPool) {...},这样代码看起来更加直观。

标准库中包含了一个空的啥功能都没有的实现 EmptyCoroutineContext。一般继承 AbstractCoroutineContextElement 这个类来实现自定义的 context。

控制 Coroutine 的执行线程是非常重要的一个功能,而这个功能是通过 CoroutineDispatcher 这个 context 接口实现的。

如果需要在 Coroutine 中创建一个不同 context 的子Coroutine,则可以使用 withContext() 这个函数来实现。




这篇关于Kotlin协程运用实践的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程