Kotlin-Koroutinen – Tägliche fortgeschrittene Verwendung von Koroutinen

Kotlin-Koroutinen - allgemeine fortgeschrittene Verwendung von Koroutinen

Kotlin Coroutine-Serie:

Durch die vorherigen Artikel haben wir verstanden, wie man Coroutinen startet, wie man Threads wechselt, Funktionen anhält und den Unterschied zwischen Blockierung und Nicht-Blockierung.

Den Kontext der Coroutine verstehen, Threads planen, den Job der Coroutine verwalten, Ausnahmen verwalten usw.

Verstehen Sie den Umfang von Coroutinen, das Konzept von Parent-Child-Coroutinen, GlobalScop, MainScope und den Android-spezifischen lifecycleScope viewModelScope usw. Erfahren Sie mehr über ihre Ähnlichkeiten und Unterschiede und wie Sie sie verwenden.

Wie verwenden wir also in der tatsächlichen Entwicklung Coroutinen? Welche Punkte sind zu beachten? Beispielsweise, wie Netzwerkanforderungen verwendet und gekapselt werden, wie benutzerdefinierte Coroutinen verwendet werden, wie Coroutinen gleichzeitig ausgeführt und gesperrt werden, und so weiter.

Schauen wir gemeinsam nach unten.

1. Verwendung von Netzwerkanfragen und Kapselung in Coroutinen

Im Allgemeinen sollte unsere Netzwerkanfrage so aussehen

       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()
                }
            }
        }
    }

In Kombination mit dem, was wir zuvor gesagt haben, fordern wir ein Netzwerk wie dieses an, das Http-Fehler und benutzerdefinierte API-Fehler verarbeiten kann.

Muss ich jede Netzwerkanfrage so schreiben?Kann ich sie kapseln?Natürlich können wir eine Erweiterungsmethode als Toolklasse kapseln und dann die DSL-Callback-Methode verwenden, um das Ergebnis zurückzurufen (etwas bequemer als die Schnittstelle , wenn nicht Wenn ich DSL verstehe, versuche ich die Kommentare zu löschen Wenn ich es nicht verstehe, können Sie meinen vorherigen Artikel über die erweiterten Funktionen von Kotlin lesen ).

Wir definieren zunächst die Implementierung einer 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
    }
}

Kapselung synchroner Anfragen mit DSL und 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())
                }
            }
        }
    }
}

Beachten Sie, dass sich die Implementierung der DSL hier von der allgemeinen DSL unterscheidet. Im Allgemeinen bringt jeder der DSL bei, die Schnittstelle zu implementieren. Ich habe die Schnittstelle hier nicht implementiert, daher müssen Sie manuell zurückrufen, wenn Sie zurückrufen.

Dann bei Verwendung:

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

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

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

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

Ist es sehr OkHttp-Callback-Stil, aber dies ist tatsächlich ein Callback, ändern Sie einfach den Callback, der die Schnittstelle zuvor verwendet hat, in einen DSL-Callback, und es ist im Wesentlichen immer noch ein Callback.

Gibt es einen eleganteren Weg, bedeutet das nicht, dass die Coroutine den Callback eliminieren soll? Was ist der Unterschied zwischen der Verwendung einer Coroutine und der Nichtverwendung einer Coroutine auf diese Weise? . .

Zweitens die Kapselung der Suspendierungsmethode der Coroutine und des 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:

总结

Schnell ist diese Serie überall zu Ende, das Wissen um das Coroutinen-Vergleichssystem ist an dieser Stelle fast gleich, und es gibt kein Problem mit der grundlegenden Anwendung. Andere verstreute Wissenspunkte müssen nicht separat veröffentlicht werden und erscheinen im folgenden Code.

Um diese Serie zu machen, habe ich selbst die Wissenspunkte im Zusammenhang mit Koroutinen systematisch sortiert. Obwohl ich es in weniger als einem Monat vergessen werde, kann das Sortieren dieses Systems es für alle einfacher machen, es zu überprüfen.

Lassen Sie mich abschließend sagen, dass wir 挂起tatsächlich sehen können, dass es immer noch einen Unterschied zwischen einer Coroutine und einem Thread gibt , unabhängig davon, ob es sich um eine Coroutine oder eine Coroutine 并发oder eine Coroutine handelt. 阻塞Obwohl jeder sagt, dass Coroutinen die Kapselung von Threads sind, möchte ich dennoch, dass jeder von Coroutinen und Threads lernt, was einfacher zu verstehen ist. Nur meine bescheidene Meinung, sprühen Sie nicht, wenn Sie es nicht mögen.

Wenn Sie es nicht verstehen, empfehle ich Ihnen, mit dem ersten Artikel der Serie zu beginnen.Die interne Implementierung erfolgt Schritt für Schritt.

Das Konzept und der Rahmen von Coroutinen sind relativ umfangreich. Wenn meine Erklärung Fehler oder Irrtümer enthält, hoffe ich, dass die Schüler darauf hinweisen und sie mitteilen können.

Wenn Sie das Gefühl haben, dass dieser Artikel Sie ein wenig inspiriert hat, hoffe ich, dass Sie 点赞mich unterstützen können, denn Ihre Unterstützung ist meine größte Motivation.

Ok, das ist das Ende dieser Reihe.

Ich nehme an der Rekrutierung des Signierprogramms für Ersteller der Nuggets Technology Community teil. Klicken Sie auf den Link, um sich zu registrieren und einzureichen .

Ich denke du magst

Origin juejin.im/post/7121132393922035720
Empfohlen
Rangfolge