5 分钟学废 Compose MutatorMutex

结论

  • 用于 cancel 之前的协程 Job,并且执行新的协程体的工具类。

背景(说垃圾话环节)

看 Compose 源码的时候发现跟动画有关、滚动有关的操作基本上都会出现一个叫做 MutatorMutex 的类,一开始还以为是 Kotlin 标准库 Mutex 的什么黑科技。仔细看包名后才发现原来是 Compose 全家桶的黑魔法。

androidx.compose.foundation.MutatorMutex

当机立断的我如往常一样点进去看源码注释

Mutual exclusion for UI state mutation over time.
mutate permits interruptible state mutation over time using a standard MutatePriority. A MutatorMutex enforces that only a single writer can be active at a time for a particular state resource. Instead of queueing callers that would acquire the lock like a traditional Mutex, new attempts to mutate the guarded state will either cancel the current mutator or if the current mutator has a higher priority, the new caller will throw CancellationException.
MutatorMutex should be used for implementing hoisted state objects that many mutators may want to manipulate over time such that those mutators can coordinate with one another. The MutatorMutex instance should be hidden as an implementation detail.

image.png

翻译过来字面意思大概是

在一段时间内让 UI 状态变化互相排斥。
一段时间内 mutate 允许使用标准的 MutatePriority 来中断状态变化。 MutatorMutex 强制对于特定状态资源,一次只能有一个写入器处于活动状态。 不像传统的 Mutex 那样将获取锁的调用者排队,新的改变受保护状态的尝试将取消当前的 mutator,或者如果当前的 mutator 具有更高的优先级,则新的调用者将抛出 CancellationException 。
MutatorMutex 应该用于实现许多 mutator 可能希望随着时间的推移操纵的提升状态对象,以便这些 mutator 可以相互协调。 MutatorMutex实例应作为实现细节隐藏。

看了注释之后还是一脸懵逼,翻开注释那个 例子

import androidx.compose.foundation.MutatorMutex
import androidx.compose.foundation.layout.Row
import androidx.compose.material.Text
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope

@Stable
class ScrollState(position: Int = 0) {
    private var _position by mutableStateOf(position)
    var position: Int
        get() = _position.coerceAtMost(range)
        set(value) {
            _position = value.coerceIn(0, range)
        }

    private var _range by mutableStateOf(0)
    var range: Int
        get() = _range
        set(value) {
            _range = value.coerceAtLeast(0)
        }

    var isScrolling by mutableStateOf(false)
        private set

    private val mutatorMutex = MutatorMutex()

    /**
     * Only one caller to [scroll] can be in progress at a time.
     */
    suspend fun  scroll(
        block: suspend () -> R
    ): R = mutatorMutex.mutate {
        isScrolling = true
        try {
            block()
        } finally {
            // MutatorMutex.mutate ensures mutual exclusion between blocks.
            // By setting back to false in the finally block inside mutate, we ensure that we
            // reset the state upon cancellation before the next block starts to run (if any).
            isScrolling = false
        }
    }
}

/**
 * Arbitrary animations can be defined as extensions using only public API
 */
suspend fun ScrollState.animateTo(target: Int) {
    scroll {
        animate(from = position, to = target) { newPosition ->
            position = newPosition
        }
    }
}

/**
 * Presents two buttons for animating a scroll to the beginning or end of content.
 * Pressing one will cancel any current animation in progress.
 */
@Composable
fun ScrollControls(scrollState: ScrollState) {
    Row {
        val scope = rememberCoroutineScope()
        Button(onClick = { scope.launch { scrollState.animateTo(0) } }) {
            Text("Scroll to beginning")
        }
        Button(onClick = { scope.launch { scrollState.animateTo(scrollState.range) } }) {
            Text("Scroll to end")
        }
    }
}

例子中提到的内容是大致是一个带有滑动功能的控件,传递了 ScrollStateScrollControls 控制,点击按钮后可以滚动到开始或者结尾。其中核心方法是 scroll() ,这种场景并且结合注释的说法,不难想到这个 MutatorMutex 的作用其实类似用属性动画手撸动画的时候的某种动画 start() 的场景(开始和继续得对动画判断是否正在运行,正在运行就先取消掉)

Mutator 这个玩意在这里起始就是类似任务的意思, 下文会统一表示为任务【其本质上是执行任务的一些必要条件的包装类】,方便理解(不信你把Mutator 当成任务再读一遍注释嘿嘿)


基本用法:

  • 1、定义一个全局的 MutatorMutex 变量
  • 2、使用 mutatorMutex.mutate { 函数体 } 即可

源码分析

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

        fun cancel() = job.cancel()
    }

    private val currentMutator = AtomicReference(null)
    private val mutex = Mutex()

    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")
        }
    }

    /**
     * Enforce that only a single caller may be active at a time.
     *
     * If [mutate] is called while another call to [mutate] or [mutateWith] is in progress, their
     * [priority] values are compared. If the new caller has a [priority] equal to or higher than
     * the call in progress, the call in progress will be cancelled, throwing
     * [CancellationException] and the new caller's [block] will be invoked. If the call in
     * progress had a higher [priority] than the new caller, the new caller will throw
     * [CancellationException] without invoking [block].
     *
     * @param priority the priority of this mutation; [MutatePriority.Default] by default. Higher
     * priority mutations will interrupt lower priority mutations.
     * @param block mutation code to run mutually exclusive with any other call to [mutate] or
     * [mutateWith].
     */
    suspend fun  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)
            }
        }
    }

MutatorMutex 主要组成

  • 1、MutatePriority:是个枚举类,本次变化的优先级,有三级,Default 最低,PreventUserInput 最高,用低优先级的 Mutator 任务去打断高优先级的 Mutator 任务会抛出 CancellationException 异常
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
}

  • 2、Mutator:包装了 MutatePriority 和协程 Job 变量,提供判断是否需要中断 Mutator 任务的方法和控制 Job 去 cancel 的方法,可以认为它是类似 Runable 的任务
  • 3、tryMutateOrCancel(): 中文翻译 balabla 是尝试改变或者取消?改变啥?我觉得应该叫做是否要取消老任务。看源码的操作,其就是判断老家伙 Mutator 和 新家伙Mutator 的优先级,新家伙优先级有没有大于或者等于老家伙的优先级,如果符合条件那么 cancel 掉老家伙相关的 Job。如果没有小于的话,那对不起,要炸!!
  • 4、mutate(): 使用传入的优先级执行新的任务
    suspend fun  mutate(
        priority: MutatePriority = MutatePriority.Default,
        block: suspend () -> R
    ) = coroutineScope {
        //用传入的 MutatePriority 构造新的 Mutator
        val mutator = Mutator(priority, coroutineContext[Job]!!)

        //看要不要把上次正在运行的老家伙 job 干掉,如果干不掉,这里会抛出 CancellationException 异常
        tryMutateOrCancel(mutator)

        //协程 mutex 锁执行任务
        mutex.withLock {
            try {
                block()
            } finally {
                currentMutator.compareAndSet(mutator, null)
            }
        }
    }


应用场景

总体概括的大体场景:需要执行最新的任务,并且干掉老任务的业务场景

  • 控制滑动列表滚动到头部或者底部(官方例子)
  • 下拉刷新 loading 下拉到最大距离后回弹回固定刷新距离
  • LottieAnimatable 中开始动画和结束动画(防止多次动画)

你可能感兴趣的:(5 分钟学废 Compose MutatorMutex)