淺嚐 Jetpack Compose — MutatorMutex

Andy Liu
6 min readOct 29, 2023

--

緣由

最近在看 Scrollable 系列的 Compose 程式碼時讀到了一個 class 叫 MutatorMutex,它也出現在很多滑動相關的源碼中,甚至在 Draggable 中也有它的身影(精確來說是 foundation 庫中的副本),抱著向下挖掘的心態繼續看下去……。

先看看 MutatePriority

enum class MutatePriority {
/**
* The default priority for mutations. Can be interrupted by other [Default], [UserInput] or
* [PreventUserInput] priority operations.
* [Default] priority should be used for programmatic animations or changes that should not
* interrupt user input.
*/
Default,

/**
* An elevated priority for mutations meant for implementing direct user interactions.
* Can be interrupted by other [UserInput] or [PreventUserInput] priority operations.
*/
UserInput,

/**
* A high-priority mutation that can only be interrupted by other [PreventUserInput] priority
* operations. [PreventUserInput] priority should be used for operations that user input should
* not be able to interrupt.
*/
PreventUserInput
}

MutatePriority 是個 enum class ,將優先級分成三個等級,要中斷前一個操做必須要是自己或是自己以上的優先級才行。

再來瞧瞧 MutatorMutex 做了些什麼事

    private class Mutator(val priority: MutatePriority, val job: Job) {
fun canInterrupt(other: Mutator) = priority >= other.priority

fun cancel() = job.cancel(MutationInterruptedException())
}

首先我們能看到 MutatorMutex 內有個 private class Mutator ,挺單純的,內含剛剛提到的 MutatePriority 和一個Coroutine Job 。

也定義了cancel 以及 canInterrupt 兩個方法,前者是 cancel 掉coroutine job,後者用來比較兩個 priority ,若優先級大於等於則返回 true 表示可以 interrupt 。

private val currentMutator = AtomicReference<Mutator?>(null)

接著先定義了 currentMutator 來表示目前的 Mutator ,AtomicReference 用來保證操作該變數時的原子性。

private val mutex = Mutex()

而 Mutex 能夠在 coroutine 中上鎖,作用類似於 Java 中的 synchronized。

    private fun tryMutateOrCancel(mutator: Mutator) {
while (true) {
val oldMutator = currentMutator.get()
if (oldMutator == null || mutator.canInterrupt(oldMutator)) {
if (currentMutator.compareAndSet(oldMutator, mutator)) {
oldMutator?.cancel()
break
}
} else throw CancellationException("Current mutation had a higher priority")
}
}

來看一下 tryMutateOrCancel ,若新 mutator 可以中斷原先的 currentMutator(兩者比較priority) ,就將 currentMutator 換成新的 mutator,並且將舊 Mutator 的 job cancel 掉(也就是停止該 coroutine),若無法 interrupt 則自己直接 throw CancellationException。

補充說明:因為使用了 AtomicReference ,可以保證同一時間只有一個執行緒能夠成功設定 currentMutator 。

suspend fun <R> mutate(
priority: MutatePriority = MutatePriority.Default,
block: suspend () -> R
) = coroutineScope {
val mutator = Mutator(priority, coroutineContext[Job]!!)

tryMutateOrCancel(mutator)

mutex.withLock {
try {
block()
} finally {
currentMutator.compareAndSet(mutator, null)
}
}
}

於是 mutate 也很好理解了,當有新操作呼叫 mutate 時,會先經由上一段介紹的 tryMutateOrCancel 嘗試 cancel 舊 Mutator 綁定的 coroutine 將自己取而代之,或是將自己 cancel 掉。

若成功 currentMutator 則上鎖並執行 block ,並在結束後清空 currentMutator 。

結論

第一眼看到它著實困惑了一下,但其實是融合 Priority 概念並將 Mutex 包裝起來,實現一些需要優先級操作又想保證執行緒安全的場景,在 Jetpack Compose 中它被廣泛運用在滾動和拖動的功能,有機會再寫個文章聊聊。

本文章撰寫於 Jetpack Compose 1.6.0-alpha08

--

--