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 分钟前
受 go-zero 启发,我给 Flutter 整了套 MVI-BLoC 脚手架
android·flutter·架构
delta_hell4 小时前
【阅读源码-Android】动画之AnimatorSet--2
android·源码·animatorset
叶辞树7 小时前
Android CLI
android
我命由我1234510 小时前
Android 开发问题:unexpected element <service> found in <manifest>
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
程思扬10 小时前
Android ANR实战排查:主线程SystemClock.sleep导致页面启动偶现无响应
android·笔记
码农coding10 小时前
android12 InputManagerService分析之输入事件如何回到应用层
android
zhangguojia710 小时前
从一个窗口覆盖问题出发,理解 Android Window 的层级与权限
android
古法安卓13 小时前
Android-SELinux 策略调试实战:从 AVC 日志到策略修复
android·java·android studio
Dovis(誓平步青云)14 小时前
DevEco Studio 6.1.1 Windows 安装实录:从下载校验到首次启动
android·开发语言·数据库·人工智能·windows·harmonyos
Zender Han14 小时前
Flutter 自适应(Adaptive)与响应式(Responsive)设计实践:官方推荐方案详解
android·flutter·ios