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

相关推荐
好的佩奇4 小时前
蓝牙之广播与扫描
android·flutter·蓝牙
传奇开心果编程6 小时前
【Jetpack Compose娓娓道来】第36课:焦点管理与键盘输入——让遥控器、键盘和输入法都“找得到北”
android·kotlin·移动开发·交互·android jetpack·ui框架·声明式ui
hai_android7 小时前
AudioTrack 深度解析:从原理到实战
android·java·kotlin
红尘伴蝶舞7 小时前
从秒开优化到连环崩溃:深入剖析 ViewPager2 在常驻 DialogFragment 中的复用陷阱与自愈方案
android·kotlin
巫山老妖12 小时前
Android 应用架构演进:MVC / MVP / MVVM / MVI (2026)
android·android jetpack
YB137513 小时前
kotlin中闭包特性
kotlin
木头左16 小时前
【架构设计】教育类 App 服务端化改造:AI 上移、题库分层与个人题库归属
android·架构设计·系统设计·后端开发·数据库设计
墨狂之逸才16 小时前
USB 的 HOST 和 DEVICE 到底有什么区别?ADB 调试应该选哪个?
android
奶油话梅糖17 小时前
苹果的内核跑上了安卓手机:2,631 Star 的 HakiOS 让 XNU 在 Adreno GPU 上画出 118 帧
android·macos·智能手机
恋猫de小郭17 小时前
Flutter THLC 的 WebView 滚动终于从 70 FPS 提升到 120 FPS
android·前端·flutter