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协程基本概念,具体实现的时候,需要考虑很多问题!

相关推荐
dora27 分钟前
Flutter的屏幕适配
android·flutter
SoulKuyan1 小时前
android 发送onkey广播,Android 添加键值并上报从驱动到上层
android
Yang-Never1 小时前
ADB->查看某个应用的版本信息
android·adb·android studio
tangweiguo030519871 小时前
Kotlin集合全解析:List和Map高频操作手册
kotlin
居然是阿宋3 小时前
Android RecyclerView 多布局场景下的设计思考:SRP 与 OCP 的权衡与优化
android·开闭原则·单一职责原则
前行的小黑炭3 小时前
Retrofit框架分析(二):注解、反射以及动态代理,Retrofit框架动态代理的源码分析
android·kotlin·retrofit
zach3 小时前
android项目如何修改第三方类库,导入到自己的项目中
android·前端·架构
三思而后行,慎承诺3 小时前
安卓的Launcher 在哪个环节进行启动
android
WenGyyyL4 小时前
《Android 应用开发基础教程》——第四章:Intent 与 Activity 跳转、页面传值
android·手机·android studio
Wgllss4 小时前
Android图片处理:多合一,多张生成视频,裁剪,放大缩小,滤镜色调,饱和度,亮度调整
android·架构·android jetpack