Kotlin 协程源代码泛读:Continuation 思想实验-2

接着上回 Continuation 思想实验,我们来思考另一个问题,如何实现 download

kotlin 复制代码
fun download(url: String, completion: Continuation) {
    // 下载中...
    completion.resumeWith(...)
}

聪明的你一定能想到各种各样的方法,最简单粗暴的做法,可以这样

kotlin 复制代码
fun download(url: String, completion: Continuation) {
    val thread = new Thread {
        // 下载中...
        completion.resumeWith(...)
    }
}

这种史前时代的写法,Code Review 的时候会被提 N 个问题,简单改进一下

kotlin 复制代码
val dispatchers = Executors.newCachedThreadPool()
fun download(url: String, completion: Continuation) {
    dispatchers.execute {
        // 下载中...
        completion.resumeWith(...)
    }
}

稍微好点,但是还是有一堆问题,我们点到为止

其它类似的耗时函数,比如 decode 也需要进行类似的改造。代码书写信条:DO NOT REPEAT YOURSELF!告诉你这些函数有相同的样板代码(Executor.execute),而且它和具体的业务(下载,解码)无关,所以更应出现在框架层。那么有没有其它解法呢?

Continuation 思想实验提到带 Continue 参数的方法就像状态机,Continuation.resumeWith 可以在 方法内不同的 label(挂起点)「恢复」执行,我们来看看 download 的调用者 display:

kotlin 复制代码
fun display(url: String, completion: Continuation? = null) {
    var $continuation: Continuation? = null
    if (continuation is ContinuationImpl) {
        // 自己人!(递归调用) 
    } else {
        // 异乡人!,注意这里保存了 completion
        $continuation = ContinuationImpl(completion)
    }

    // 定义一个内部类
    inner class ContinuationImpl(
        completion: Continuation
    ): Continuation {
        var label: Int = 0
        var result: Any? = null
        // resumeWith 让 display 从某个 label 恢复执行
        fun resumeWith(result: Any?) {
            completion.ressult = result
            display(url, completion)
        }
    }

    when (continuation.label) {
        0 -> {
            // 下次 resume 该执行 1 了
            continuation.label = 1
            download(url, continuation) 
            return
        }

        1 -> {
            // 下次 reusme 执行 2,when 代码块之后剩下的代码块
            continuation.label = 2
            decode(continuation.result as ByteArray, continuation) 
            return
        }
    }
    show(...)
    continuation?.resumeWith(...)
}

这个 resumeWith 就是突破口! resume 有恢复、唤醒的意思,想象你坐卧铺车从一个乡下到城市,上车后你睡下(suspend)了,然后睡一觉在城里的火车站醒来(resume)。我们如何实现这种在一个线程挂起,然后其它线程恢复的效果呢?答案是:

bash 复制代码
调用者(caller)进行线程切换

使用代理模式,偷梁换柱,拦截(intercepted)resumeWith 调用

kotlin 复制代码
class DispatchedContinuation(
    val executor: Executor, 
    val delegate: Continuation
): Continuation, Runnable {
    fun resumeWith(result: Any) {
        executor.submit {
            delegate.resumeWith(result)
        }
    }
}

为了方便使用,定义一个扩展方法

kotlin 复制代码
fun Continuation.intercepted(executor: Executor): Continuation {
    return DispatchedContinuation(this)
}

修改 display 方法:

kotlin 复制代码
fun display(url: String, executor: Executor, completion: Continuation? = null) {
   var $continuation: Continuation? = null
    if (continuation is ContinuationImpl) {
        // 自己人!(递归调用) 
    } else {
        // 异乡人!,注意这里保存了 completion
        // 对 ContinuationImpl 进行 wrap,这样就可以在 Executor 中 resume 啦
        $continuation = ContinuationImpl(completion).intercepted(executor)
    }
    ...
}

这个 executor 参数,所有的异步方法都需要,进一步抽象&封装:

kotlin 复制代码
interface Scope {
    val context: Context
}

interface Context {
    val executor: Executor
}

fun Scope.display(url: String, completion: Continuation? = null) {
    ...
}

这样可以让调用者指定 executor,是不是有那么一丢丢协程的味道了! 在我们上述思想的过程中,已经触及到协程中一个重要的概念 Dispatcher(调度器),以及它是如何和 Continuation 协作的

最后再考虑一个问题,目前的封装可以让 display 在指定的 Executor 中 resume,但是 diaplay 第一个 resume 之前(when 语句块中的 label 0)呢?我们可以指定 display 在哪个线程启动吗? 我们把 display 也变成状态机:

kotlin 复制代码
class DisplayContinuation(
    val scope: Scope,
): Continuation {
    fun resumeWith(result: Any?) {
        invokeSuspend(result as url)
    }

    fun invokeSuspend() {
        // 这里有点问题
        scope.display(url, this)
    }
}

ok,现在对 display 的调用,变成这样

scss 复制代码
DisplayContinuation(scope).resumeWith(...)

如果想在工作线程中调用(调度)display,就可以这样写

scss 复制代码
DisplayContinuation(scope).intercepted().resumeWith(...)

如果又来了个其它的任务,比如下载和更新应用,你也可以写一个类似的 UpdateContinuation,它们两有重复(相似)的代码,所以可以提取一个基类

kotlin 复制代码
class BaseContinuationImpl: Continuation {
    fun resumeWith(result: Any?) {
    invokeSuspend(result)
}
    abstract fun invokeSuspend(param: Any?)
}

DisplayContinuation: BaseContinuationImpl {...}

如果这么抽象和封装下去,你会搞出个类似 launch 的协程启动器...

这些近乎玩具的代码,只是为了展示Kotlin协程基本概念,具体实现的时候,需要考虑很多问题!

相关推荐
阿巴斯甜9 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker9 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952710 小时前
Andorid Google 登录接入文档
android
黄林晴12 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_1 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android