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 <R> 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<Mutator?>(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 <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)
            }
        }
    }

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 <R> 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 中开始动画和结束动画(防止多次动画)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345