什么是“异步数据流”?它在什么业务场景下有用武之地?它背后的原理是什么?读一读 Flow 的源码,尝试回答这些问题。
同步 & 异步 & 连续异步
同步和异步是用来形容“调用”的:
- 同步调用:当调用发起者触发了同步调用后,它会等待调用执行完毕并返回结果后才继续执行后续代码。显然只有当调用者和被调用者的代码执行在同一个线程中才会发生这样的串行执行效果。
- 异步调用:当调用发起者触发了异步调用后,它并不会等待异步调用中的代码执行完毕,因为异步调用会立马返回,但并不包含执行结果,执行结果会用异步的方式另行通知调用者。当调用者和被调用者的代码执行在不同线程时就会发生这种并行执行效果。
异步调用在 App 开发中随处可见,通常把耗时操作放到另一个线程执行,比如写文件:
suspend fun writeFile(content: String) {
// 写文件
}
// 启动协程写文件
val content = "xxx"
coroutineScope.launch { wirteFile(content) }
kotlin 中的suspend
方法用于表达一个异步过程,“多个连续产生的异步过程”如何表达?
for 循环是首先想到的方案:
val contents = listOf<String>(...) // 将要写入文件的多个字串
contents.forEach { string ->
coroutineScope.launch { writeFile(string) }
}
用 for 循环的前提条件是得先拿到所有需要进行异步操作的数据。但“多个连续产生的数据”这个场景下,数据是一点一点生成的,没法一下子全部拿到。比如“倒计时 1 分钟,每 2 秒做一次耗时运算,计时结束后将所有运算结果累加并在主线程打印”。这个时候就要用“异步数据流”重新认识问题。
异步数据流用“生产者/消费”模型来解释这个场景:倒计时器是这个场景中的生产者,它每隔两秒产生一个新数据。累加器是这个场景中的消费者,他将所有异步数据累加。生产者和消费者之间就好像有一条管道,生产者从管道的一头插入数据,消费者从另一头取数据。因为管道的存在,数据是有序的,遵循先进先出的原则。
传统方案
在给出 Flow 的解决方案之前,先看下传统解决方案。
首先得实现一个定时器,它可以在异步线程中以一定时间间隔执行异步操作。用线程池就再合适不过了:
// 倒计时器
class Countdown<T>(
private var duration: Long, // 倒计时长
private var interval: Long, // 倒计时间隔
private val action: (Long) -> T // 倒计时后台任务
) {
// 任务结果累加值
var acc: Any? = null
// 倒计时剩余时间
private var remainTime = duration
// 任务开始回调
var onStart: (() -> Unit)? = null
// 任务结束回调
var onEnd: ((T?) -> Unit)? = null
// 任务结果累加器
var accumulator: ((T, T) -> T)? = null
// 倒计时任务包装类
private val countdownRunnable by lazy { CountDownRunnable() }
// 用于主线程回调的 Handler
private val handler by lazy { Handler(Looper.getMainLooper()) }
// 线程池
private val executor by lazy { Executors.newSingleThreadScheduledExecutor() }
// 启动倒计时
fun start(delay: Long = 0) {
if (executor.isShutdown) return
// 向主线程回调倒计时开始
handler.post(onStart)
executor.scheduleAtFixedRate(countdownRunnable, delay, interval, TimeUnit.MILLISECONDS)
}
// 将倒计时任务包装成 Runnable
private inner class CountDownRunnable : Runnable {
override fun run() {
remainTime -= interval
// 执行后台任务并获取返回值
val value = action(remainTime)
// 累加任务返回值
acc = if (acc == null) value else accumulator?.invoke(acc as T, value)
if (remainTime <= 0) {
// 关闭倒计时
executor?.shutdown()
// 向主线程回调倒计时结束
handler.post { onEnd?.invoke(acc as? T) }
}
}
}
}
抽象出Countdown
用于执行后台倒计时任务,它使用scheduleAtFixedRate()
构造线程池,并按一定间隔执行倒计时任务。
对外倒计时任务被表达成(Long) -> T
,即输入倒计时时间输出异步任务结果的 lambda。在内部它又被包装成一个 Runnable,以便在 run() 方法中实现倒计时及累加逻辑。
然后就可以像这样使用:
Countdown(60_000, 2_000) { remianTime -> calculate(remianTime) }.apply {
onStart = { Log.v("test", "countdown start") }
onEnd = { ret -> Log.v("test", "countdown end, ret=$ret") }
accumulator = { acc, value -> acc + value }
}.start()
虽然不得不引入一些复杂度,比如线程池、Handler、累加器。但得益于类的封装和 Kotlin 语法糖,最终调用形式还是简洁达意的。
Flow 方案
若用 Flow 就可以省去这些复杂度:
fun <T> co