关于后端:Jetpack-Compose-快照系统

45次阅读

共计 10831 个字符,预计需要花费 28 分钟才能阅读完成。

Jetpack Compose 引入了一种解决可察看状态的新办法 —— Snapsot(快照)。在 Compose 中咱们通过 state 的变动来触发重组,那么请思考以下几个问题:

  • 为什么 state 变动能触发重组呢?
  • 它是如何确定重组范畴呢?
  • 只有 state 变动就肯定会重组吗?

让咱们带着问题去学习!

Snapshot API

个别状况下咱们不须要理解快照如何应用,这些都是框架应该做的事件,咱们手动操作很可能搞出问题。所以这里只是演示快照的应用(不波及底层实现),这样有助于了解 Compose 重组的机制。

Snapshot(快照),简略比喻就是给所有 state 拍了个照,因而你能获取到拍摄之前的状态。

咱们通过代码演示来看看 Snapshot 到底是做什么的: 首先定义一个 Dog 类, 蕴含一个 state:

class Dog {var name: MutableState<String> = mutableStateOf("")
}

创立快照

val dog = Dog()
dog.name.value =“Spot”val snapshot = Snapshot.takeSnapshot()  
dog.name.value =“Fido”println(dog.name.value)
snapshot.enter {println(dog.name.value)
} 
println(dog.name.value)
  
// Output:
Fido
Spot
Fido

  • takeSnapshot()"拍摄" 程序中所有 State 值的快照,无论它们是在何处创立的
  • enter 函数会把快照状态复原并利用到函数体中

因而咱们看到仅在 enter 中是旧值。

可变快照

咱们尝试在 enter 块中更改狗狗的名字:

fun main() {val dog = Dog()
    dog.name.value = "Spot"

    val snapshot = Snapshot.takeSnapshot()

    println(dog.name.value)
    snapshot.enter {println(dog.name.value)
        dog.name.value = "Fido"
        println(dog.name.value)
    }    
    println(dog.name.value)
}

// Output:
Spot
Spot

java.lang.IllegalStateException: Cannot modify a state object in a read-only snapshot
  

会发现当咱们尝试批改值时报错了,因为 takeSnapshot() 是只读的, 因而在 enter 外部咱们能够读但不能写,如果想要创立一个可变快照应应用 takeMutableSnapshot() 办法。

fun main() {val dog = Dog()
    dog.name.value = "Spot"

    val snapshot = Snapshot.takeSnapshot()

    println(dog.name.value)
    snapshot.enter {println(dog.name.value)
        dog.name.value = "Fido"
        println(dog.name.value)
    }    
    println(dog.name.value)
}

// Output:
Spot
Spot

java.lang.IllegalStateException: Cannot modify a state object in a read-only snapshot

能够看到程序没有解体了,然而在 enter 里的操作并没有在其范畴之外失效!这是一个很重要的隔离机制,如果咱们想要利用 enter 外部的变更须要调用 apply() 办法:

 fun main() {val dog = Dog()
     dog.name.value = "Spot"

     val snapshot = Snapshot.takeMutableSnapshot()
     println(dog.name.value)
     snapshot.enter {
         dog.name.value = "Fido"
         println(dog.name.value)
     }
     println(dog.name.value)
     snapshot.apply()
     println(dog.name.value)
}

// Output:
Spot
Fido
Spot
Fido 

能够看到调用 apply 之后,新值在 enter 之外也失效了。咱们还能够使 Snapshot.withMutableSnapshot() 来简化调用:

fun main() {val dog = Dog()
    dog.name.value = "Spot"

    Snapshot.withMutableSnapshot {println(dog.name.value)
        dog.name.value = "Fido"
        println(dog.name.value)
    }
    println(dog.name.value)
}

到目前为止咱们晓得了:

  • 拍摄咱们所有状态的快照
  • “复原”状态到特定的代码块
  • 扭转状态值

但咱们还不晓得如何感知读写,接下来让咱们搞清楚这个。

察看读取和写入

无论是 LiveData,Flow 还是 State 都是观察者模式,那么就要有观察者和被观察者。对于快照零碎,被观察者就是咱们的 state,而观察者有两个,一个是读取观察者,一个是写入观察者。

实际上 takeMutableSnapshot有两个可选参数的,别离在读和写时回调:

fun takeMutableSnapshot(readObserver: ((Any) -> Unit)? = null,
            writeObserver: ((Any) -> Unit)? = null
        ): MutableSnapshot =
            (currentSnapshot() as? MutableSnapshot)?.takeNestedMutableSnapshot(
                readObserver,
                writeObserver
            ) ?: error("Cannot create a mutable snapshot of an read-only snapshot")

因而咱们能够在回调中执行一些操作, 在 Compose 中就是值读取时记录 ComposeScope, 写入时如果有变动则将对应的 Scope 标记为 invalid

全局快照

全局快照是位于快照树根部的可变快照。与必须 apply 能力失效的惯例可变快照相比,全局快照没有 apply 操作。比方咱们会在 ViewModel 里定义 state, 并且在 repository申请数据并给 state 赋值。此时就会由 GlobalSnapshot 去发送告诉:

它通过调用:

  • Snapshot.notifyObjectsInitialized。这会为自上次调用以来更改的任何状态发送告诉。
  • Snapshot.sendApplyNotifications()。这相似于 notifyObjectsInitialized,但只有在理论产生更改时才会推动快照。在第一种状况下,只有将任何可变快照利用于全局快照,就会隐式调用此函数。
internal object GlobalSnapshotManager {private val started = AtomicBoolean(false)

    fun ensureStarted() {if (started.compareAndSet(false, true)) {val channel = Channel<Unit>(Channel.CONFLATED)
            CoroutineScope(AndroidUiDispatcher.Main).launch {
                channel.consumeEach {Snapshot.sendApplyNotifications()
                }
            }
            Snapshot.registerGlobalWriteObserver {channel.offer(Unit)
            }
        }
    }
}  

能够看到在 android 平台上注册了 writeObserver, 它还有 ApplyObserver 咱们前面再说。

多线程

在给定线程的快照中,在利用该快照之前,不会看到其余线程对状态值所做的更改。快照与其余快照“隔离”。在利用快照并主动推动全局快照之前,对快照内的状态所做的任何更改对其余线程都将不可见。看这个类名大家就懂了 SnapshotThreadLocal:

internal actual class SnapshotThreadLocal<T> {private val map = AtomicReference<ThreadMap>(emptyThreadMap)
    private val writeMutex = Any()

    @Suppress("UNCHECKED_CAST")
    actual fun get(): T? = map.get().get(Thread.currentThread().id) as T?

    actual fun set(value: T?) {val key = Thread.currentThread().id
        synchronized(writeMutex) {val current = map.get()
            if (current.trySet(key, value)) return
            map.set(current.newWith(key, value))
        }
    }
}

抵触

如果咱们 ” 拍摄 ” 了多个快照并且均利用批改会怎么呢?

fun main() {val dog = Dog()
  dog.name.value = "Spot"

  val snapshot1 = Snapshot.takeMutableSnapshot()
  val snapshot2 = Snapshot.takeMutableSnapshot()

  println(dog.name.value)
  snapshot1.enter {
    dog.name.value = "Fido"
    println("in snapshot1:" + dog.name.value)
  }
  // Don’t apply it yet, let’s try setting a third value first.

  println(dog.name.value)
  snapshot2.enter {
    dog.name.value = "Fluffy"
    println("in snapshot2:" + dog.name.value)
  }

  // Ok now we can apply both.
  println("before applying:" + dog.name.value)
  snapshot1.apply()
  println("after applying 1:" + dog.name.value)
  snapshot2.apply()
  println("after applying 2:" + dog.name.value)
}

// Output:
Spot
in snapshot1: Fido
Spot
in snapshot2: Fluffy
before applying: Spot
after applying 1: Fido
after applying 2: Fido

会发现第二个快照的更改无奈利用,因为它们都视图以雷同的初始值进行批改,因而第二个快照要么再执行一次 enter,要么通知如何解抵触。

Compose 实际上有一个用于解决合并抵触的 API!mutableStateOf() 须要一个可选的 SnapshotMutationPolicy. 该策略定义了如何比拟特定类型的值 (equivalent) 以及如何解决抵触 (merge)。并且提供了一些开箱即用的策略:

  • structuralEqualityPolicy– 应用对象的 equals 办法 (==)比拟对象,所有写入都被认为是非抵触的。
  • referentialEqualityPolicy– 通过援用 (===)比拟对象,所有写入都被认为是非抵触的。
  • neverEqualPolicy– 将所有对象视为不相等,所有写入都被认为是非抵触的。

咱们也能够构建本人的规定:

class Dog {
  var name: MutableState<String> =
    mutableStateOf("", policy = object : SnapshotMutationPolicy<String> {override fun equivalent(a: String, b: String): Boolean = a == b

      override fun merge(previous: String, current: String, applied: String): String =
        "$applied, briefly known as $current, originally known as $previous"
    })
}

fun main() {// Same as before.}

// Output:
Spot
in snapshot1: Fido
Spot
in snapshot2: Fluffy
before applying: Spot
after applying 1: Fido
after applying 2: Fluffy, briefly known as Fido, originally known as Spot

总结

以上就是 Snapshot (快照)的根本应用, 它就相当于高级的 DiffUtil。它的特点总结起来就是:

  • 响应式:有状态的代码始终主动放弃最新。咱们无需放心订阅和反订阅。
  • 隔离性:有状态代码能够对状态进行操作,而不用放心在不同线程上运行的代码会扭转该状态。Compose 能够利用这一点来实现旧的 View 零碎无奈实现的成果,例如将重构放到多个后盾线程下来执行。

解惑

  • 为什么 state 变动能触发重组呢?

Jetpack Compose 在执行时注册了 readObserverOfwriteObserverOf :

private inline fun <T> composing(
        composition: ControlledComposition,
        modifiedValues: IdentityArraySet<Any>?,
        block: () -> T): T {
      val snapshot = Snapshot.takeMutableSnapshot(readObserverOf(composition), writeObserverOf(composition, modifiedValues)
      )
      try {return snapshot.enter(block)
      } finally {applyAndCheck(snapshot)
      }
}

其中在读取状态的中央会执行:

  • readObserverOf 来记录哪些 scope 应用了此state\` :
override fun recordReadOf(value: Any) {if (!areChildrenComposing) {
          composer.currentRecomposeScope?.let {
              it.used = true
              observations.add(value, it)
              ...
          }
      }
 }
  • writeObserverOf而写入时会找出对应应用此 statescope 使其 invalidate :
override fun recordWriteOf(value: Any) = synchronized(lock) {invalidateScopeOfLocked(value)          derivedStates.forEachScopeOf(value) {invalidateScopeOfLocked(it)       }   }      private fun invalidateScopeOfLocked(value: Any) {observations.forEachScopeOf(value) {scope ->           if (scope.invalidateForResult(value) == InvalidationResult.IMMINENT) {observationsProcessed.add(value, scope)           }       }   }   

在下次帧信号达到时对于这些 scope 执行重组。

  • 它是如何确定重组范畴呢?

可能被标记为 Invalid 的代码必须是非 inline 且无返回值的 @Composalbe function/lambda,必须遵循 重组范畴最小化 准则。具体参见:Compose 如何确定重组范畴

  • 只有 state 变动就肯定会重组吗?
    不肯定,具体案例请看以下例子:

例子①

val darkMode = mutableStateOf("hello")

override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)
    setContent {
        lifecycleScope.launch {delay(100)
            val text= darkMode.value
            darkMode.value = "Compose"
        }
    }
}

不会重组,因为 delay 导致状态的读取是在 snap.apply 办法之外执行的, 因而也就不会注册 readObserverOf , 天然也就不会与 composeScope 挂钩,也就不会触发重组,在这个例子里如果是在 delay 之前读取则会重组。

例子②

val darkMode = mutableStateOf("hello")

override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)
    setContent {
        thread {
            val text =  darkMode.value
            darkMode.value = "Compose"
        }
    }
}

thread 中的 state 在不同线程读取,因为 SnapshotThreadLocal 机制,如果此线程无快照,则获取GlobalSnapshot

internal fun currentSnapshot(): Snapshot =
    threadSnapshot.get() ?: currentGlobalSnapshot.get()

因为没有对应的 readObserver, 因而此例子不会重组。然而如果在composable 内读取了此 state 是会重组的,因为 ReComposer 注册了 ApplyObserver, 在apply 时也会对 globalModified 进行记录,在下一帧信号达到时去查找对应的scope(大家能够断点跟一下流程):

val unregisterApplyObserver = Snapshot.registerApplyObserver { changed, _ ->
      synchronized(stateLock) {if (_state.value >= State.Idle) {
              snapshotInvalidations += changed
              deriveStateLocked()} else null
      }?.resume(Unit)
 }

例子③

override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)
    setContent {val darkMode = mutableStateOf("hello")
        Text(darkMode.value)
        darkMode.value = "Compose"
    }
}

这个也没触发重组,可能大家会纳闷,这个没异步,断点也有 readObserverwriteObserver 为啥不会触发重组呢?不是说状态变更会将应用它的 scope 记为 invalid 吗?

然而理论运行中,InvalidationResultIGNORE

fun invalidate(scope: RecomposeScopeImpl, instance: Any?): InvalidationResult {
     ...
     if (anchor == null || !slotTable.ownsAnchor(anchor) || !anchor.valid)
         // The scope has not yet entered the composition
         return InvalidationResult.IGNORED 
     ...
}

首先咱们的确记录下了应用 statescope, 不然也不会在批改时触发 invalidate 行为。但此时 slotTable 里并还没有可重组的区域锚点信息,只有在组合实现之后能力拿到每个区域的锚点anchors。简略形容就是 Compose 应用 SlotTable 来记录数据信息,此时第一次残缺的组合都没实现,不晓得该从哪下手。

无关 SlotTable 的更多信息请参阅:深刻详解 JetpackCompose| 实现原理

其次就是因为 state 的创立是在 enter 代码块中,此时 state.snapshotId\==Snapshot.id , 并不会记录 state 的变动。毕竟快照的 diff 是作用在两个快照之间。

internal fun <T : StateRecord> T.overwritableRecord(
    state: StateObject,
    snapshot: Snapshot,
    candidate: T
): T {
    ...
    val id = snapshot.id
    // 此时间接返回,并没有记录 state 变动
    if (candidate.snapshotId == id) return candidate
     ...
}

然而如果你把 state 的创立放到 setContent 之外呢?

例子④

val darkMode = mutableStateOf("hello")

override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)
    setContent {Text(darkMode.value)
        darkMode.value = "Compose"
    }
}

答案是会重组

因为这个状态是在拍摄之前创立的,此时 state.snapshotId!=Snapshot.id, 此期间对 state 的批改尽管不会立刻标记为 invalid , 然而会计入  modified , apply 之后,由全局快照进行告诉:

internal fun <T : StateRecord> T.overwritableRecord(
    state: StateObject,
    snapshot: Snapshot,
    candidate: T
): T {
    ...

    val id = snapshot.id

    if (candidate.snapshotId == id) return candidate

    val newData = newOverwritableRecord(state, snapshot)
    newData.snapshotId = id

   // 记录变动
    snapshot.recordModified(state)

    return newData
}

会在 apply 时告诉到观察者 ApplyObserver(方才还提到 writerObserver),记录下 changed:

val unregisterApplyObserver = Snapshot.registerApplyObserver { changed, _ ->
    synchronized(stateLock) {if (_state.value >= State.Idle) {
            // here
            snapshotInvalidations += changed
            deriveStateLocked()} else null
    }?.resume(Unit)
}

composation 则会找出察看了对应变动状态的 scope 标记为 invalid 期待重组:

private fun addPendingInvalidationsLocked(values: Set<Any>) {
     var invalidated: HashSet<RecomposeScopeImpl>? = null

    fun invalidate(value: Any) {observations.forEachScopeOf(value) { scope ->
            if (!observationsProcessed.remove(value, scope) &&
                scope.invalidateForResult(value) != InvalidationResult.IGNORED
            ) {
                val set = invalidated
                    ?: HashSet<RecomposeScopeImpl>().also {invalidated = it}
                 set.add(scope)
            }
        }
    }

    for (value in values) {if (value is RecomposeScopeImpl) {value.invalidateForResult(null)
        } else {invalidate(value)
            derivedStates.forEachScopeOf(value) {invalidate(it)
            }
        }
    }
    invalidated?.let {observations.removeValueIf { scope -> scope in it}
    }
}

例子⑤

var onlyDisplay = mutableStateOf("onlyDisplay")

class MainActivity : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)
        setContent {
            Text(
                text = onlyDisplay.value,
                fontSize = 50.sp,
            )
            onlyDisplay.value = "Display"
        }
    }
}

如果把 state 申明放到 kt 文件最外层,是否会重组?

答案是不会,因为在 kotlin 中如果把变量不放到类里,间接放到文件顶层。编译之后其实会生成一个文件,而这个属性则变成 static 的。

public final class MainActivityKt {static MutableState<String> onlyDisplay = SnapshotStateKt.mutableStateOf$default("onlyDisplay", null, 2, null);
}

因而这个例子就波及了类的初始化问题:

只有被动申请一个类, 这个类才会初始化, 仅蕴含动态变量, 函数, 等动态的货色.


也就是说在这个例子里只有在调用 onlyDisplay 时,才执行初始化,所以其 state.snapshotId==snapshot.Id , 此时首次组合尚未执行结束,本次的 invalidateResult==IGNORE,也不会记为 modified,就和例子③ 一样的问题了。

我的库存,须要的小伙伴请点击我的 GitHub 收费支付

正文完
 0