Kotlin coroutines - daily advanced use of coroutines

Kotlin coroutines - common advanced use of coroutines

Kotlin coroutine series:

Through the previous articles, we understand how to start coroutines, how to switch threads, suspend functions, and the difference between blocking and non-blocking.

Understand the context of the coroutine, schedule threads, manage the Job of the coroutine, exception management, etc.

Understand the scope of coroutines, the concept of parent-child coroutines, GlobalScop, MainScope and Android-specific lifecycleScope viewModelScope, etc. Learn about their similarities and differences and how to use them.

So in actual development, how do we use coroutines? What are the points to pay attention to? For example, how to use and encapsulate network requests, how to use custom coroutines, how to concurrency and lock coroutines, and so on.

Let's look down together.

1. Using network requests and encapsulation in coroutines

Generally speaking, our network request should be like this

       private fun doNetWork() {
        val api = DemoRetrofit.apiService.getNews("1", "2")

        val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
            YYLogUtils.e(throwable.message ?: "网络错误")
        }

        MainScope().launch(exceptionHandler) {

            val work = async(Dispatchers.IO) {
                api.execute()  //同步请求
            }

            //获取到异步的网络请求结果
            val response = work.await()
            response?.let {

                if (response.isSuccessful) {
                    val bean = response.body()
                    YYLogUtils.w(bean.toString())

                } else {
                    when (response.code()) {
                        402 -> {
                            toast("token过期了")
                        }
                        500 -> {
                            toast("服务器错误")
                        }
                        
                    }
                }
            }

            work.invokeOnCompletion {
                // 协程关闭时,取消任务
                if (work.isCancelled) {
                    api.cancel()
                }
            }
        }
    }

Combined with what we said before, we request a network like this, which can handle Http errors and custom Api errors.

Do I have to write every network request like this? Can I encapsulate it? Of course, we can encapsulate an extension method as a tool class, and then use the DSL callback method to call back the result (a bit more convenient than the interface, if not If I understand DSL, I try to clear the comments. If I don't understand, you can read my previous article on Kotlin's advanced functions ).

We first define the implementation of a DSL

class RetrofitResultCallbackDsl<ResultType> {

    var api: (Call<ResultType>)? = null

    internal var onSuccess: ((ResultType?) -> Unit)? = null

    internal var onComplete: (() -> Unit)? = null

    internal var onFailed: ((error: String?, code: Int) -> Unit)? = null


    fun onSuccess(block: (ResultType?) -> Unit) {
        this.onSuccess = block
    }

    fun onComplete(block: () -> Unit) {
        this.onComplete = block
    }

    fun onFailed(block: (error: String?, code: Int) -> Unit) {
        this.onFailed = block
    }
    
    fun clean() {
        onSuccess = null
        onComplete = null
        onFailed = null
    }
}

Encapsulation of synchronous requests using DSL and Retrofit

fun <ResultType> CoroutineScope.retrofit(
    init: RetrofitResultCallbackDsl<ResultType>.() -> Unit
) {
    //先初始化DSL类
    val retrofitCoroutine = RetrofitResultCallbackDsl<ResultType>()
    init(retrofitCoroutine)
    //如果DSL是接口实现类,这里需要绑定接口,我们这里没有实现接口,就无需绑定了

    //DSL初始化完成之后启动协程
    launch(Dispatchers.Main) {

        retrofitCoroutine.api?.let { it ->
            val work = async(Dispatchers.IO) {
                try {
                    it.execute()
                } catch (e: Exception) {
                    //这里DSL如果是绑定接口的无需我们自己手动调用,一般都是接口调用,我们这里没有绑定接口就自己手动调用了,
                    retrofitCoroutine.onFailed?.invoke("网络错误", -1)
                    null
                }
            }

            work.invokeOnCompletion {
                if (work.isCancelled) {
                    it.cancel()
                    retrofitCoroutine.clean()
                }
            }

            val response = work.await()
            retrofitCoroutine.onComplete?.invoke()
            response?.let {
                if (response.isSuccessful) {
                    retrofitCoroutine.onSuccess?.invoke(response.body())
                } else {
                    when (response.code()) {
                        402 -> {
                            toast("token过期了")
                        }
                        500 -> {
                            toast("服务器错误")
                        }
                    }
                    retrofitCoroutine.onFailed?.invoke(response.errorBody()?.toString(), response.code())
                }
            }
        }
    }
}

Note that the implementation of the DSL here is different from the general DSL. Generally, everyone is teaching the DSL to implement the interface. I have not implemented the interface here, so you need to manually call back when you call back.

Then when using:

        MainScope().retrofit<String> {
            api = DemoRetrofit.apiService.getNews("1", "2")

            onComplete {
                YYLogUtils.w("网络请求执行完毕")
            }

            onSuccess { result ->
                YYLogUtils.w("成功的结果:" + result)
            }

            onFailed { error, code ->
                YYLogUtils.e("网络请求出错了")
            }
        }

Is it very OkHttp callback style, but this is indeed a callback, just change the callback that used the interface before to a DSL callback, and it is still a callback in essence.

Is there a more elegant way, doesn't it mean that the coroutine is to eliminate the callback? What is the difference between using a coroutine and not using a coroutine in this way. . .

Second, the encapsulation of the suspension method of the coroutine and Retrofit

确实,上面都是使用协程+同步的方式,其实协程在2.6之后就支持了挂起 suspend 的方式。那么我们使用 协程 + 挂起函数岂不是绝配。

例如我们定义Api的时候,我们就能直接标记方法为 suspend

    @GET("/index.php/api/employee/industry")
    suspend fun getIndustry(
        @Header("Content-Type") contentType: String,
        @Header("Accept") accept: String
    ): BaseBean<List<Industry>>

那么我们封装这样的网络请求带错误处理的时候就要这么来

suspend fun <T : Any> Any.extRequestHttp(call: suspend () -> BaseBean<T>): OkResult<T> {

    return try {

        val response = call()

        if (response.code == 200) {
            OkResult.Success(response.data)
        } else {
            OkResult.Error(ApiException(response.code, response.message))
        }

    } catch (e: Exception) {

        e.printStackTrace()
        OkResult.Error(handleExceptionMessage(e))
    }

}

fun handleExceptionMessage(e: Exception): IOException {
    return when (e) {
        is UnknownHostException -> IOException("Unable to access domain name, unknown domain name.")
        is JsonParseException -> IOException("Data parsing exception.")
        is HttpException -> IOException("The server is on business. Please try again later.")
        is ConnectException -> IOException("Network connection exception, please check the network.")
        is SocketException -> IOException("Network connection exception, please check the network.")
        is SocketTimeoutException -> IOException("Network connection timeout.")
        is RuntimeException -> IOException("Error running, please try again.")
        else -> IOException("unknown error.")
    }

}

直接定义一个扩展方法,请求网络,需要注意的是我们的参数是 suspend 我们内部的实现并没有开启协程,所以我们自己的方法必须也要加上 suspend 前缀,确保它最后实在协程内执行的。

当然了,如果不想用try catch的话,使用 runCatching 也是一样的。

    runCatching {
        call.invoke()
    }.onSuccess { response: BaseBean<T> ->
        if (response.code == 200) {
            OkResult.Success(response.data)
        } else {
            OkResult.Error(ApiException(response.code, response.message))
        }
    }.onFailure { e ->
        e.printStackTrace()
        OkResult.Error(handleExceptionMessage(Exception(e.message, e)))
    }

其实 runCatching 的实现和try-catch是一样的,语法糖而已,内部还是try-catch的实现。

使用的时候,一般都是现在Repository中定义

    suspend fun getIndustry(): OkResult<List<Industry>> {

        return extRequestHttp {
            DemoRetrofit.apiService.getIndustry(
                Constants.NETWORK_CONTENT_TYPE,
                Constants.NETWORK_ACCEPT_V1
            )
        }

    }

这里一样的内部的实现并没有开启协程,所以我们自己的方法必须也要加上 suspend 前缀,确保它最后实在协程内执行。

真正的调用在ViewModel中

 fun requestIndustry() {

        viewModelScope.launch {
            //开始Loading
            loadStartLoading()

            val result = mRepository.getIndustry()

            result.checkResult({
                //处理成功的信息
                toast("list:$it")
                liveData.value = it
            }, {
                //失败
                liveData.value = null
            })

            loadHideProgress()
        }
    }

这样就完成一次封装到使用的全部过程,可以看到确实是比协程+同步的要简单一些了。如果想看更完整的协程+挂起可以看看我之前的文章协程的使用与封装

三、自定义协程的几种方法

之前讲到协程作用域的时候,我们有些特殊情况需要自定义协程作用域。一起看看自定义协程有哪几种方式

3.1 自己管理Job

其实最简单也是最直接的做法是像RxJava那样,自己管理Dispose对象

open class BaseVM : ViewModel(){
    val jobs = mutableListOf<Job>()
    override fun onCleared() {
        super.onCleared()
        jobs.forEach { it.cancel() }
    }
}
class UserVM : BaseVM() {
    val userData = StateLiveData<UserBean>() 
    fun login() {
        jobs.add(GlobalScope.launch {
            YYLogUtils.w("切换到一个协程3")
            delay(3000)
            YYLogUtils.w("协程3执行完毕")
        })
    }

自己关联和管理job,destroy的时候关闭job

3.2 委托 MainScope 管理

我们可以使用 MainScope 管理当前类的协程作用域,这里不止使用MainScope,但是用它最方便。

比如我们可以在一个自定义View中都可以使用协程了:

class AsyncView @JvmOverloads constructor(context: Context?, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
    View(context, attrs, defStyleAttr), CoroutineScope by MainScope() {

    fun doSth(block: (View) -> Unit) {

        launch {
            YYLogUtils.w("切换到一个协程")
            delay(3000)
            YYLogUtils.w("协程执行完毕")
            block(view)
        }
    }

    override fun onDetachedFromWindow() {
        cancel()
        super.onDetachedFromWindow()
    }
}

3.3 原始的 CoroutineScope 实现

我们可以直接让我们的类实现 CoroutineScope 接口,但是我们需要指定协程的上下文,我们可以这样。

abstract class CoroutineDialog : Dialog, CoroutineScope {
  // 默认上下文使用context.dispatcher()
  override val coroutineContext: CoroutineContext by lazy { context.dispatcher() }
  ...
}

比如我想封装一个带协程的PopupWindow,我这样封装一个基类

/**
 * 自定义带协程作用域的弹窗
 */
abstract class CoroutineScopeCenterPopup(activity: FragmentActivity) : CenterPopupView(activity), CoroutineScope {

    private lateinit var job: Job

    private val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
        YYLogUtils.e(throwable.message ?: "Unkown Error")
    }

    //此协程作用域的自定义 CoroutineContext
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job + CoroutineName("CenterPopupScope") + exceptionHandler


    override fun onCreate() {
        job = Job()
        super.onCreate()
    }


    override fun onDismiss() {
        job.cancel()  // 关闭弹窗后,结束所有协程任务
        YYLogUtils.w("关闭弹窗后,结束所有协程任务")
        super.onDismiss()
    }
}

那么我就可以直接使用这个基类的实现了:

class InterviewAcceptPopup(private val mActivity: FragmentActivity) : CoroutineScopeCenterPopup(mActivity) {

    override fun getImplLayoutId(): Int {
        return R.layout.dialog_interview_accept
    }

    override fun onCreate() {
        super.onCreate()

        val btnYes = findViewById<TextView>(R.id.btn_y)

        btnYes.click {
            doSth()
        }
    }

    private fun doSth() {
        launch {
            YYLogUtils.w("执行在协程中...")

            delay(1000L)

            YYLogUtils.w("执行完毕...")

            dismiss()
        }
    }

}

实际开发中如果是涉及到 Android 页面的一些生命周期的,我们可以使用viewModelScope、lifecycleScope 。如果是其他的页面比如 View 或者 Dialog 或者干脆不涉及到页面的一些地方,我们就可以使用以上的几种方法来实现自定义的协程作用域。

当然还有更多的自定义协程作用域方法,我没有穷举出来,如果大家有更多更好的方法也可以评论区讨论一下。

四、协程的并发与锁

之前我们就讲到过 并发使用 async ,切换线程使用 withContext 。本质原因是 async 函数是创建一个协程,而 withContext 函数只是一个挂起函数。

 launch {

            YYLogUtils.w("查看运行的线程1:" + Thread.currentThread().name)

            val start = System.currentTimeMillis()
            val res1 = async {
                YYLogUtils.w("查看运行的线程2:" + Thread.currentThread().name)
                delay(1000)
                "123"
            }

            val res2 = async(Dispatchers.IO) {
                YYLogUtils.w("查看运行的线程3:" + Thread.currentThread().name)
                delay(2000)
                "456"
            }

            val res3 = res1.await() + res2.await()
            YYLogUtils.w("并发耗时:" + (System.currentTimeMillis() - start))

            withContext(Dispatchers.IO) {
                YYLogUtils.w("查看运行的线程4:" + Thread.currentThread().name)
                delay(1000)
                YYLogUtils.w("res3:$res3")
            }

            YYLogUtils.w("查看运行的线程4:" + Thread.currentThread().name)

        }

这样的代码大家就能看懂了,打印结果如下:

为什么要这么写,是因为有人认为 async 就是异步的,异步的才是并发的。No No No ,async 翻译过来是异步的意思,但是这里它并不是异步的,async 只是创建一个新的协程而已,并发是协程的并发,而不是异步而并发,可以看到我们创建一个 async 函数,我们打印它的线程默认是主线程的,除非你指定线程运行,如第二个 async 函数,我们指定了线程才是异步的。

这么写就是为了不要混淆协程的并发与线程的异步并发的区别,并发协程与并发异步线程不同,再次强调,因为它是创建了协程所以并发,而不是异步才并发的。

再举例,创建协程的又不止 async 函数一家,我们的 launch 一样的能创建协程,我们试试看能不能并发:

        launch {

            YYLogUtils.w("查看运行的线程1:" + Thread.currentThread().name)

            val start = System.currentTimeMillis()
            val res1 = async {
                YYLogUtils.w("查看运行的线程2:" + Thread.currentThread().name)
                delay(1000)
                "123"
            }

            val res2 = async(Dispatchers.IO) {
                YYLogUtils.w("查看运行的线程3:" + Thread.currentThread().name)
                delay(2000)
                "456"
            }

            launch {
                YYLogUtils.w("查看运行的线程4:" + Thread.currentThread().name)
                delay(1000)
            }

            val res3 = res1.await() + res2.await()
            YYLogUtils.w("并发耗时:" + (System.currentTimeMillis() - start))

            withContext(Dispatchers.IO) {
                YYLogUtils.w("查看运行的线程5:" + Thread.currentThread().name)
                delay(1000)
                YYLogUtils.w("res3:$res3")
            }

            YYLogUtils.w("查看运行的线程6:" + Thread.currentThread().name)

        }
 

打印结果证明是并发的:

那为什么我们一般并发都使用 async 而不用 launch 。那是因为他们虽然都是创建了协程,但是 launch 返回的是 Job 对象 ,而 async 返回的是一个 Deferred 可以通过它拿到处理之后的值。

比如这样:

        launch {

            YYLogUtils.w("查看运行的线程1:" + Thread.currentThread().name)

            val start = System.currentTimeMillis()
            val res1 = async {
                YYLogUtils.w("查看运行的线程2:" + Thread.currentThread().name)
                delay(1000)
                "123"
            }

            val res2 = async(Dispatchers.IO) {
                YYLogUtils.w("查看运行的线程3:" + Thread.currentThread().name)
                delay(2000)
                "456"
            }

            val res4 = launch {
                YYLogUtils.w("查看运行的线程4:" + Thread.currentThread().name)
                delay(1000)
                "789"
            }

            val res3 = res1.await() + res2.await()
            YYLogUtils.w("并发耗时:" + (System.currentTimeMillis() - start))
      }

我们能拿到 res4 的值吗? 它返回的是 789 吗?不是它返回的是Job对象 。所以我们才在需要接收返回值的并发中使用 async ,而如果不需要返回值 那么我们使用 launch 也是可以并发的。

协程的并发跟线程是否在主线程和子线程没关系,我们再举例:

       launch {

            YYLogUtils.w("查看运行的线程1:" + Thread.currentThread().name)

            val start = System.currentTimeMillis()
            val res1 = async {
                YYLogUtils.w("查看运行的线程2:" + Thread.currentThread().name)
                delay(1000)
                "123"
            }

            val res2 = async {
                YYLogUtils.w("查看运行的线程3:" + Thread.currentThread().name)
                delay(2000)
                "456"
            }

            val res3 = res1.await() + res2.await()
            YYLogUtils.w("并发耗时:" + (System.currentTimeMillis() - start))

            withContext(Dispatchers.IO) {
                YYLogUtils.w("查看运行的线程5:" + Thread.currentThread().name)
                delay(1000)
                YYLogUtils.w("res3:$res3")
            }

            YYLogUtils.w("查看运行的线程6:" + Thread.currentThread().name)

        }

打印结果:

并发的线程如果都在主线程,一样的并发的。

那有同学要问了,newki newki,你这个不对啊,我们使用 Retrofit 请求网络,我们标记 suspend 之后再协程里面直接使用,它就是异步的 , 你看我这样,这样,再这样。并发 + 异步 so easy , async 我不也没有指定异步吗,它可不就是异步的吗!

   viewModelScope.launch {

       val industryResult = async {
            mRepository.getIndustry()
        }

        val schoolResult = async {
            mRepository.getSchool()
        }

        val industry = industryResult.await()
        val school = schoolResult.await()
    }   

好吧,其实 Retrofit 是比较特殊的情况,他的 ApiService 虽然标记了 suspend ,看起来我们是直接使用了,但是其实内部 Retrofit 的动态代理的时候会找到你是否标记了 suspend ,然后它会对 suspend 做单独的处理 。我不太会讲源码,大家如果有兴趣可以全局搜索一下 SuspendForResponse 类 和 awaitResponse 类,看看 Retrofit 怎么把 suspend 转换为协程处理的,这里我就不贴 Retrofit 的源码了。

并发是没有问题了,协程与线程一样,对于数据的操作无法保持原子性,所以在协程中,需要使用原子性的数据结构,例如使用 mutex.withLock 来处理。

线程中锁都是阻塞式,在没有获取锁时无法执行其他逻辑,而协程可以通过挂起函数解决这个,没有获取锁就挂起协程,获取后再恢复协程,协程挂起时线程并没有阻塞可以执行其他逻辑。这种互斥锁就是Mutex。这也是为什么在协程中不推荐使用 synchronized 关键字的原因,因为会导致阻塞。

使用Mutex的方式

    suspend fun CoroutineScope.massiveRun(action: suspend () -> Unit) {
        val n = 100  // 启动的协程数量
        val k = 1000 // 每个协程重复执行同一动作的次数
        val time = measureTimeMillis {
            List(n) {
                launch {
                    repeat(k) { action() }
                }
            }

        }
        YYLogUtils.w("Completed ${n * k} actions in $time ms")
    }


      //使用
        var counter = 0
        val mutex = Mutex()
        runBlocking {
            massiveRun {
                mutex.withLock {
                    counter++
                }
            }
        }
        YYLogUtils.w("Counter = $counter")

打印Log:

总结

In a hurry, this series is over everywhere. At this point, the knowledge of the coroutine comparison system is almost learned, and there is no problem with the basic use. Other scattered knowledge points do not need to be published separately, and will appear in the code that follows.

To do this series, in fact, I have systematically sorted out the knowledge points related to coroutines. Although I will forget it in less than a month, sorting out this system can make it easier for everyone to review.

Finally, let me say that, in fact, whether it is a coroutine 挂起or a coroutine 并发, or a coroutine 阻塞, we can see that there is still a difference between a coroutine and a thread. Although everyone says that coroutines are the encapsulation of threads, I still want everyone to distinguish between coroutines and threads to learn, so that it is easier to understand. Just my humble opinion, don't spray if you don't like it.

If you don't understand, I recommend that you start from the first article of the series. The internal implementation is progressive step by step.

The concept and framework of coroutines are relatively large. If there are any mistakes or mistakes in my explanation, I hope students can point out and communicate.

If you feel that this article has inspired you a little, I hope you can 点赞support me. Your support is my biggest motivation.

Ok, that's the end of this series.

I am participating in the recruitment of the creator signing program of the Nuggets Technology Community, click the link to register and submit .

Guess you like

Origin juejin.im/post/7121132393922035720