CoroutineLiveData 介绍与分析

1,303 阅读4分钟

之前有写过一篇文章简单讲解了一下 Flow。其中也提到了 LiveData 和 Flow 的区别。LiveData 使用起来足够简单,但缺点也很明显,那就是无法像 Flow 一样指定工作线程。尽管 LiveData 拥有 postValue() 方法允许开发者在子线程更新 LiveData。但是涉及到 LiveData 的 Transformation 时,所有的操作都是在主线程进行的。这里借用 5 Uses of KTX LiveData Coroutine Builder 一文中的图片:
这时有同学会说那我用 Flow 好了。然而,Flow 虽然可以使用 flowOn() 来指定工作线程,加上 repeatOnLifeCycle() 后也可以和 LiveData 一样拥有感知页面生命周期的能力。但由于 Flow 是建立在 Kotlin Coroutine 的基础之上。这就导致了在 Java 中无法使用 Flow。当然如果是纯 Kotlin 项目,那当我没说,这种场景下我们完完全全可以使用 Flow。 Google 的 JetPack 团队也考虑到了这个问题,于是在 lifecycle-livedata-ktx 库中提供了将 LiveData 与 Flow 相连接的方法。也就是今天的主角:CoroutineLiveData
使用入门 首先添加依赖 androidx.lifecycle:lifecycle-livedata-ktx:2.4.0 到项目中。 再然后我们就可以非常愉快的使用了:

val flowOne = flow {
    emit("Hello")
    emit("World")
}.flowOn(Dispatchers.IO)
val liveDataOne = flowOne.asLiveData()
// or
val liveDataTwo = liveData(Dispatchers.IO) {
    emit("Again")
    emitSource(liveDataOne)
}

我们一个一个来说。首先是 asLiveData() 方法。它可以将一个 Flow 转换成一个 LiveData 供我们使用。

// 注释很有用,篇幅原因这里就不贴出来了。
@JvmOverloads
public fun <T> Flow<T>.asLiveData(
    context: CoroutineContext = EmptyCoroutineContext,
    timeoutInMs: Long = DEFAULT_TIMEOUT
): LiveData<T> = liveData(context, timeoutInMs) {
    collect {
        emit(it)
    }
}

我们发现 asLiveData 方法其实是第二种写法的一层封装,所以我们直接看第二个方法:

public fun <T> liveData(
    context: CoroutineContext = EmptyCoroutineContext,
    timeoutInMs: Long = DEFAULT_TIMEOUT,
    @BuilderInference block: suspend LiveDataScope<T>.() -> Unit
): LiveData<T> = CoroutineLiveData(context, timeoutInMs, block)

这个方法接受三个参数,timeoutInMs 我们讲 Flow 时见过类似的参数,用于延迟一段时间后再取消 flow。DEFAULT_TIMEOUT 为 5000ms。context 也没什么好说的,需要注意一点就是可能需要设置一个 CoroutineExceptionHandler 来处理异常,否则的话 Flow 的异常会抛到主线程的 UncaughtExceptionHandler 去。也可以通过在原 Flow 上添加一个 catch 运算符来解决。

someFlow.asLiveData(CoroutineExceptionHandler { context, throwable ->
    TODO()
})
// 或
someFlow.catch{ TODO() }

至于 block 则比较关键,LiveDataScope:它是一个接口,定义了两个方法:

public interface LiveDataScope<T> {
    public suspend fun emit(value: T)
  
    public suspend fun emitSource(source: LiveData<T>): DisposableHandle

    public val latestValue: T?
}

首先,注意下这两个方法是有 suspend 关键字的,也就是说在这个 LiveData 里,我们可以使用协程。 另外可以看到,在这里我们不仅可以 emit 一个值,我们还可以 emitSource 一个 LiveData。这个方法和 MediatorLiveData 很像,但是存在一些差别。那就是 emitSource 后,如果再次调用 emit 或 emitSource 后,都会移除前面的 source。而 MediatorLiveData 的 addSource 是可以存在多个 source 的。 最后我们重点看下 CoroutineLiveData
CoroutineLiveData 继承自 MediatorLiveData:

internal class CoroutineLiveData<T>(
    context: CoroutineContext = EmptyCoroutineContext,
    timeoutInMs: Long = DEFAULT_TIMEOUT,
    block: Block<T>
) : MediatorLiveData<T>() {
    private var blockRunner: BlockRunner<T>?
    private var emittedSource: EmittedSource? = null

    init {
        // use an intermediate supervisor job so that if we cancel individual block runs due to losing
        // observers, it won't cancel the given context as we only cancel w/ the intention of possibly
        // relaunching using the same parent context.
        val supervisorJob = SupervisorJob(context[Job])

        // The scope for this LiveData where we launch every block Job.
        // We default to Main dispatcher but developer can override it.
        // The supervisor job is added last to isolate block runs.
        val scope = CoroutineScope(Dispatchers.Main.immediate + context + supervisorJob)
        blockRunner = BlockRunner(
            liveData = this,
            block = block,
            timeoutInMs = timeoutInMs,
            scope = scope
        ) {
            blockRunner = null
        }
    }

    internal suspend fun emitSource(source: LiveData<T>): DisposableHandle {
        clearSource()
        val newSource = addDisposableSource(source)
        emittedSource = newSource
        return newSource
    }

    internal suspend fun clearSource() {
        emittedSource?.disposeNow()
        emittedSource = null
    }

    override fun onActive() {
        super.onActive()
        blockRunner?.maybeRun()
    }

    override fun onInactive() {
        super.onInactive()
        blockRunner?.cancel()
    }
}

可以看到主要逻辑其实在 blockRunner 和 emittedSource 里。onActive()onInactive() 方法可以获取到 LiveData 的运行状态。CoroutineLiveData 在这里调用 blockRunner 的 maybeRun()cancel() 来控制启动和取消。 那么我们就需要看一看 BlockRunner 了:

internal class BlockRunner<T>(
    private val liveData: CoroutineLiveData<T>,
    private val block: Block<T>,
    private val timeoutInMs: Long,
    private val scope: CoroutineScope,
    private val onDone: () -> Unit
) {
    // currently running block job.
    private var runningJob: Job? = null

    // cancelation job created in cancel.
    private var cancellationJob: Job? = null

    @MainThread
    fun maybeRun() {
        cancellationJob?.cancel()
        cancellationJob = null
        if (runningJob != null) {
            return
        }
        runningJob = scope.launch {
            val liveDataScope = LiveDataScopeImpl(liveData, coroutineContext)
            block(liveDataScope)
            onDone()
        }
    }

    @MainThread
    fun cancel() {
        if (cancellationJob != null) {
            error("Cancel call cannot happen without a maybeRun")
        }
        cancellationJob = scope.launch(Dispatchers.Main.immediate) {
            delay(timeoutInMs)
            if (!liveData.hasActiveObservers()) {
                // one last check on active observers to avoid any race condition between starting
                // a running coroutine and cancelation
                runningJob?.cancel()
                runningJob = null
            }
        }
    }
}

逻辑不复杂,所以想要研究明白我们还得继续往下走,看一看 LiveDataScopeImpl:

internal class LiveDataScopeImpl<T>(
    internal var target: CoroutineLiveData<T>,
    context: CoroutineContext
) : LiveDataScope<T> {

    override val latestValue: T?
        get() = target.value

    // use `liveData` provided context + main dispatcher to communicate with the target
    // LiveData. This gives us main thread safety as well as cancellation cooperation
    private val coroutineContext = context + Dispatchers.Main.immediate

    override suspend fun emitSource(source: LiveData<T>): DisposableHandle =
        withContext(coroutineContext) {
            return@withContext target.emitSource(source)
        }

    override suspend fun emit(value: T) = withContext(coroutineContext) {
        target.clearSource()
        target.value = value
    }
}

emit() 方法首先会清掉之前添加进来的 source,然后给 LiveData setValue emitSource() 方法实际调用了 CoroutineLiveData.emitSource() 方法,在内部同样清掉了之前的 source,并利用拓展函数 addDisposableSource 调用父类的 addSource 方法,并返回当前 source 的一层封装 emittedSource,以便在取消前一个 source 时使用。 至此 CoroutineLiveData 也就分析完了。其实这里我们忽略了一个重要角色:MediatorLiveData。CoroutineLiveData、LiveData 的各种 Transformations 其实都是基于 MediatorLiveData 实现的。它的实现也不复杂,但在我们的开发中使用频率很低。感兴趣的同学可以自行学习一下。