扒一扒 Jetpack Compose 实现原理_compose 更新中it,2024最新中高级HarmonyOS鸿蒙面试题目

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新HarmonyOS鸿蒙全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img

img
img
htt

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上鸿蒙开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip204888 (备注鸿蒙)
img

正文

@Composable
private static final void Node1(final String name, Composer $composer, final int $changed, final int var3) {
$composer = $composer.startRestartGroup(1815931657);

ScopeUpdateScope var10 = $composer.endRestartGroup();
if (var10 != null) {
var10.updateScope((Function2)(new Function2() {
public final void invoke(@Nullable Composer $composer, int $force) {
MainKt.Node1(name, $composer, $changed | 1, var3);
}
}));
}
}

第一次看到上面的代码可能会有点懵,生成的 compose 函数内部插入了很多 $composer.startXXXGroup$composer.endXXXGroup 模板代码,通过查看 Composer 实现类 ComposerImpl ,会发现所有 startXXXGroup 代码最终调用下面这个 start 方法

/**

  • @param key: 编译器生成Group唯一值
  • @param objectKey: 辅助key,某些Group中会用到
  • @param isNode: 是否有Node节点
  • @param data:
    */
    private fun start(key: Int, objectKey: Any?, isNode: Boolean, data: Any?) {

    // slotTable操作逻辑
    }

start 方法内部核心逻辑是通过 SlotReaderSlotWriter 操作 SlotTable,上述 Compose 函数内部生成的 $composer.startXXXGroup$composer.endXXXGroup 模板代码就是构建 NodeTree,在 Composer 中针对不同的场景,可以生成不同类型的 Group。

startXXXGroup说明
startNode /startResueableNode插入一个包含 Node 的 Group。例如文章开头 ReusableComposeNode 的例子中,显示调用了 startResueableNode ,而后调用 createNode 在 Slot 中插入 LayoutNode
startRestartGroup插入一个可重复执行的 Group,它可能会随着重组被再次执行,因此 RestartGroup 是重组的最小单元
startReplacableGroup插入一个可以被替换的 Group,例如一个 if/else 代码块就是一个 ReplaceableGroup,它可以在重组中被插入后者从 SlotTable 中移除
startMovableGroup插入一个可以移动的 Group,在重组中可能在兄弟 Group 之间发生位置移动
startReusableGroup插入一个可复用的 Group,其内部数据可在 LayoutNode 之间复用,例如 LazyList 中同类型的 Item

接下来我们来看看 SlotTable 内部结构:

SlotTable

SlotTable 内部存储结构核心的就是 groups ( group 分组信息,NodeTree 树管理)和 slots ( group 所对应的数据),那 SlotTable 是怎么实现树结构和如何管理的呢?

internal class SlotTable : CompositionData, Iterable {
/**

  • An array to store group information that is stored as groups of [Group_Fields_Size]
  • elements of the array. The [groups] array can be thought of as an array of an inline
  • struct.
    */
    var groups = IntArray(0)
    private set

/**

  • An array that stores the slots for a group. The slot elements for a group start at the
  • offset returned by [dataAnchor] of [groups] and continue to the next group’s slots or to
  • [slotsSize] for the last group. When in a writer the [dataAnchor] is an anchor instead of
  • an index as [slots] might contain a gap.
    */
    var slots = Array<Any?>(0) { null }
    private set
    }

groups 是一个 IntArray,每 5 个 Int 为一组构成一个 Group 的信息

  • key : Group 在 SlotTable 中的标识,在 Parent Group 范围内唯一
  • Group info: Int 的 Bit 位中存储着一些 Group 信息,例如是否是一个 Node,是否包含 Data 等,这些信息可以通过位掩码来获取。
  • Parent anchor: Parent 在 groups 中的位置,即相对于数组指针的偏移(树结构
  • Size: Group: 包含的 Slot 的数量
  • Data anchor:关联 Slot 在 slots 数组中的起始位置(位置信息

我们可以通过 SlotTable#asString() 方法打印对应的树结构信息,通过前面分析,我们知道树结构是在 Kotlin Compiler Plugin 编译器生成的,通过 $composer#startXXXGroup$composer#endXXXGroup 配对生成 Group 树结构。

Group(0) key=100, nodes=2, size=16, slots=[0: {}]
Group(1) key=1000, nodes=2, size=15
Group(2) key=200, nodes=2, size=14 objectKey=OpaqueKey(key=provider)
Group(3) key=-985533309, nodes=2, size=13, slots=[2: androidx.compose.runtime.RecomposeScopeImpl@4fb4ae6, androidx.compose.runtime.internal.ComposableLambdaImpl@3b52827]
Group(4) key=-337788314, nodes=2, size=12 aux=C(Content), slots=[5: androidx.compose.runtime.RecomposeScopeImpl@b882ad4]
Group(5) key=-3687241, nodes=0, size=1 aux=C(remember):Composables.kt#9igjgp, slots=[7: MutableState(value=false)@167707773]
Group(6) key=-3686930, nodes=0, size=1 aux=C(remember)P(1):Composables.kt#9igjgp, slots=[9: MutableState(value=false)@167707773, Function2<kotlinx.coroutines.CoroutineScope, kotlin.coroutines.Continuation<? super kotlin.Unit>, java.lang.Object>]
Group(7) key=1036442245, nodes=0, size=2 aux=C(LaunchedEffect)P(1)336@14101L58:Effects.kt#9igjgp
Group(8) key=-3686930, nodes=0, size=1 aux=C(remember)P(1):Composables.kt#9igjgp, slots=[13: kotlin.Unit, androidx.compose.runtime.LaunchedEffectImpl@8d3f428]
Group(9) key=-337788167, nodes=1, size=4
Group(10) key=1815931657, nodes=1, size=3, slots=[15: androidx.compose.runtime.RecomposeScopeImpl@7421fc3]
Group(11) key=1546164276, nodes=1, size=2 aux=C(ReusableComposeNode):Composables.kt#9igjgp
Group(12) key=125, nodes=0, size=1 node=Node1(name=node1), slots=[18: node1]
Group(13) key=1815931930, nodes=1, size=3, slots=[19: androidx.compose.runtime.RecomposeScopeImpl@81cf51f]
Group(14) key=1546164276, nodes=1, size=2 aux=C(ReusableComposeNode):Composables.kt#9igjgp
Group(15) key=125, nodes=0, size=1 node=Node2(name=node2), slots=[22: node2]

GapBuffer

GapBuffer(间隙缓冲区)这个概念一般在很多地方有用到,比如文本编辑器,它在内存中使用扁平数组(flat array)实现,这个数组比真正存储数据的集合要大,而且在插入数据的会判断数据大小进行 gap 扩容,通过移动 gap index 可以将 insert(增)、delete(删)、update(改)、get(查)操作的时间复杂度降到 O(n)常数量级。

SlotTable 中移动 gap 的方法详见 moveGroupGapTo 和 moveSlotGapTo

下面我们来对比下没有 GapBuffer 和 GapBuffer 两种场景下删除一个节点和多个节点的效率,可以看到删除多个节点情况下 GapBuffer的效率要远高于没有 GapBuffer;在没有 GapBuffer 的情况下,在 Array 中只能每次移动一个 Node,insert 和 delete 节点时间效率是 O(nLogN),但是有 GapBuffer 情况下,可以通过移动 gap 的位置,将时间效率优化到 O(n)。

没有GapBuffer有GapBuffer
删除一个节点
删除多个节点
Snapshot

Snapshot 是一个 **MVCC(Multiversion Concurrency Control,多版本并发控制)**的实现,一般 MVCC 用于数据库中实现事务并发,还有分布式版本控制系统(常见的 Git 和 SVN),下面简单看下 Snapshot 使用。

fun test() {
// 创建状态(主线开发)
val state = mutableStateOf(1)

// 创建快照(开分支)
val snapshot = Snapshot.takeSnapshot()

// 修改状态(主线修改状态)
state.value = 2

println(state.value) // 打印1

snapshot.enter {//进入快照(切换分支)
// 读取快照状态(分支状态)
println(state.value) // 打印1
}
// snapshot.apply() 保存快照(下面print statr打印1)

// 读取状态(主线状态)
println(state.value) // 打印2

// 废弃快照(删除分支)
snapshot.dispose()
}

另外Snapshot提供了 registerGlobalWriteObserverregisterApplyObserver 用来监听全局 Snapshot 写入和 apply 回调,实际同时在 MutableSnapshot 构造函数传入的。

open class MutableSnapshot internal constructor(
id: Int,
invalid: SnapshotIdSet,
override val readObserver: ((Any) -> Unit)?, // 读取监听
override val writeObserver: ((Any) -> Unit)? // 写入监听
) : Snapshot(id, invalid)

如果不直接复用系统封装好的,我们也可以自己创建 Snapshot,并注册通知。

class ViewModel {
val state = mutableStateOf(“initialized”)
}

fun main() {
val viewModel = ViewModel()
Snapshot.registerApplyObserver { changedSet, snapshot ->
changedSet.forEach {
println(“registerApplyObserver:” + it)
}
}
viewModel.state.value = “one”
Snapshot.sendApplyNotifications() //
}

回到我们之前提到的 GlobalSnapshotManager.ensureStarted(),实际上就是通过 Snapshot 状态改变通知 Composition 重组。

internal object GlobalSnapshotManager {
private val started = AtomicBoolean(false)

fun ensureStarted() {
if (started.compareAndSet(false, true)) {
val channel = Channel(Channel.CONFLATED)
CoroutineScope(AndroidUiDispatcher.Main).launch {
channel.consumeEach {
Snapshot.sendApplyNotifications() // 发送通知applyChanges
}
}
Snapshot.registerGlobalWriteObserver {
channel.trySend(Unit) // 监听全局Snapshot写入
}
}
}
}

上面大概了解了 SlotTable 结构和 NodeTree 构建流程,下面看看这段代码:

@Composable
fun Content() {
var state by remember { mutableStateOf(true) }
LaunchedEffect(Unit) {
delay(3000)
state = false
}

}

估计大家应该能看懂这段代码逻辑是创建一个 state,然后在3秒后更新 state 的值,但是大家一定存在几个疑惑

  • remember 函数的作用是什么
  • LaunchedEffect 函数作用是啥,里面可以调用 delay 函数,是不是与协程有关系
  • 通过 mutableStateOf 创建的 State,为啥可以通知 Compose 进行重组

上面涉及到的 remember | LaunchedEffect | State 与 Compose 重组存在紧密联系,下面让我们一起来看看 Compose 重组是如何实现的

Compose重组

@Composable 函数是纯函数,纯函数是幂等的,唯一输入对应唯一输出,且不应该包含任何副作用(比如修改全局变量或反注册监听等),为了维护 @Composable 纯函数语义,Compose提供了 state、remember、SideEffect、CompositionLocal 这些实现,类似于 React 提供的各种 Hook。

Remember

直接来看下 remember 函数定义,主要参数是 key 和 calculation,Composer 根据 key 变化判断是否重新调用 calculation 计算值

inline fun remember(calculation: @DisallowComposableCalls () -> T): T
inline fun remember(key1: Any?, calculation: @DisallowComposableCalls () -> T): T
inline fun remember(key1: Any?, key2: Any?, calculation: @DisallowComposableCalls () -> T): T
inline fun remember(key1: Any?, key2: Any?, key3: Any?, calculation: @DisallowComposableCalls () -> T): T
inline fun remember(vararg keys: Any?, calculation: @DisallowComposableCalls () -> T): T

remember 内部调用的 composer#cache 方法,key 是否变化调用的 composer#changed 方法。

inline fun Composer.cache(invalid: Boolean, block: () -> T): T {
@Suppress(“UNCHECKED_CAST”)
return rememberedValue().let {
if (invalid || it === Composer.Empty) {
val value = block()
updateRememberedValue(value)
value
} else it
} as T
}

@ComposeCompilerApi
override fun changed(value: Any?): Boolean {
return if (nextSlot() != value) {
updateValue(value)
true
} else {
false
}
}

rememberedValue 直接调用 nextSlot 方法,updateRememberedValue 直接调用 updateValue 方法,核心逻辑就是通过SlotReaderSlotWriter 操作 SlotTable 存储数据,而且这些数据是可以跨 Group 的,具体细节可以自己查看源码。

State

State 接口定义很简单,实际开发过程中都是调用 mutableStateOf 创建 MutableState

fun mutableStateOf(
value: T,
policy: SnapshotMutationPolicy = structuralEqualityPolicy() // snapshot比较策略
): MutableState = createSnapshotMutableState(value, policy)

internal actual fun createSnapshotMutableState(
value: T,
// SnapshotMutationPolicy有三个实现StructuralEqualityPolicy(值相等)|ReferentialEqualityPolicy(同一个对象)|NeverEqualPolicy(永不相同)
policy: SnapshotMutationPolicy
): SnapshotMutableState = ParcelableSnapshotMutableState(value, policy)

ParcelableSnapshotMutableState 继承自 SnapshotMutableStateImpl,自身实现 Parcelable 内存序列化,所以我们直接分析 SnapshotMutableStateImpl

internal open class SnapshotMutableStateImpl(
value: T,
override val policy: SnapshotMutationPolicy
) : StateObject, SnapshotMutableState {
@Suppress(“UNCHECKED_CAST”)
override var value: T
get() = next.readable(this).value
set(value) = next.withCurrent { // 内部
if (!policy.equivalent(it.value, value)) {
next.overwritable(this, it) { this.value = value }
}
}

private var next: StateStateRecord = StateStateRecord(value) // 继承StateRecord

override val firstStateRecord: StateRecord
get() = next

override fun prependStateRecord(value: StateRecord) {
@Suppress(“UNCHECKED_CAST”)
next = value as StateStateRecord
}

@Suppress(“UNCHECKED_CAST”)
override fun mergeRecords(
previous: StateRecord,
current: StateRecord,
applied: StateRecord
): StateRecord? {

// snapshot分支冲突解决合并逻辑,最终结果与policy相关
}
}

可以看到真正的核心类是 StateObject,StateObject 内部存储结构是 StateRecord,内部使用链表存储,通过 Snapshot 管理 State 值,最终调用 mergeRecords 处理冲突逻辑(与 SnapshotMutationPolicy 值相关)。

abstract class StateRecord {

internal var snapshotId: Int = currentSnapshot().id // snapshotId,版本管理

internal var next: StateRecord? = null // 内部存储结构是链表

abstract fun assign(value: StateRecord) // 将value赋值给当前StateRecord

abstract fun create(): StateRecord // 创建新的StateRecord
}

SideEffect

副作用是指 Compose 内部除了状态变化之外的应用状态的变化,比如页面声明周期 Lifecycle 或广播等场景,需要在页面不可见或广播注销时改变一些应用状态避免内存泄漏等,类似于 Coroutine 协程中提供的 suspendCancellableCoroutineinvokeOnCancel 中做一些状态修改的工作,Effect 分为以下三类:

第一类是 SideEffect,实现方式比较简单,调用流程是 composer#recordSideEffect -> composer#record, 直接往 Composer 中 changes 插入 change,最终会在 Composition#applychanges 回调 effect 函数。

@Composable
@NonRestartableComposable
@OptIn(InternalComposeApi::class)
fun SideEffect(
effect: () -> Unit
) {
currentComposer.recordSideEffect(effect)
}

internal class CompositionImpl(

) : ControlledComposition {

override fun applyChanges() {
synchronized(lock) {
val manager = RememberEventDispatcher(abandonSet) // RememberManager实现类
try {
applier.onBeginChanges()

// Apply all changes
slotTable.write { slots ->
val applier = applier
// 遍历changes然后invoke注入,可以查看ComposerImpl#recordSideEffect方法
changes.fastForEach { change ->
change(applier, slots, manager)
}
changes.clear()
}

applier.onEndChanges()

// Side effects run after lifecycle observers so that any remembered objects
// that implement RememberObserver receive onRemembered before a side effect
// that captured it and operates on it can run.
manager.dispatchRememberObservers() // RememberObserver的onForgotten或onRemembered被调用
manager.dispatchSideEffects() // SideEffect调用

if (pendingInvalidScopes) {
pendingInvalidScopes = false
observations.removeValueIf { scope -> !scope.valid }
derivedStates.removeValueIf { derivedValue -> derivedValue !in observations }
}
} finally {
manager.dispatchAbandons() // RememberObserver的onAbandoned被调用
}
drainPendingModificationsLocked()
}
}

}

第二类是 DisposableEffect,DisposableEffectImpl 实现了 RememberObserver 接口,借助于 remember 存储在 SlotTable 中,并且 Composition 发生重组时会通过 RememberObserver#onForgotten 回调到 effectonDispose 函数。

@Composable
@NonRestartableComposable
fun DisposableEffect(
key1: Any?,
effect: DisposableEffectScope.() -> DisposableEffectResult
) {
remember(key1) { DisposableEffectImpl(effect) }
}

第三类是 LaunchedEffect,与 DisposableEffect 的主要区别是内部开启了协程,用来异步计算的。

@Composable
@NonRestartableComposable
@OptIn(InternalComposeApi::class)
fun LaunchedEffect(
key1: Any?,
block: suspend CoroutineScope.() -> Unit
) {
val applyContext = currentComposer.applyCoroutineContext
remember(key1) { LaunchedEffectImpl(applyContext, block) }
}

CompositionLocal

WrappedComposition#setContent 我们看到有调用 CompositionLocalProvider,在 ProvideCommonCompositionLocals 内部中定义了很多 CompositionLocal,主要功能是在 content 函数内部调用其他 Compose 函数时,可以快捷获取一些全局服务。

private class WrappedComposition(
val owner: AndroidComposeView,
val original: Composition
) : Composition, LifecycleEventObserver {

private var disposed = false
private var addedToLifecycle: Lifecycle? = null

@OptIn(InternalComposeApi::class)
override fun setContent(content: @Composable () -> Unit) {
owner.setOnViewTreeOwnersAvailable {
if (!disposed) {
val lifecycle = it.lifecycleOwner.lifecycle
lastContent = content
if (addedToLifecycle == null) {
addedToLifecycle = lifecycle
// this will call ON_CREATE synchronously if we already created
lifecycle.addObserver(this)
} else if (lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) {
original.setContent {

CompositionLocalProvider(LocalInspectionTables provides inspectionTable) {
ProvideAndroidCompositionLocals(owner, content) // CompositionLocal注入
}
}
}
}
}
}
}

@Composable
internal fun ProvideCommonCompositionLocals(
owner: Owner,
uriHandler: UriHandler,
content: @Composable () -> Unit
) {
CompositionLocalProvider(
LocalAccessibilityManager provides owner.accessibilityManager,
LocalAutofill provides owner.autofill,
LocalAutofillTree provides owner.autofillTree,
LocalClipboardManager provides owner.clipboardManager,
LocalDensity provides owner.density,
LocalFocusManager provides owner.focusManager,
LocalFontLoader provides owner.fontLoader,
LocalHapticFeedback provides owner.hapticFeedBack,
LocalLayoutDirection provides owner.layoutDirection,
LocalTextInputService provides owner.textInputService,
LocalTextToolbar provides owner.textToolbar,
LocalUriHandler provides uriHandler,
LocalViewConfiguration provides owner.viewConfiguration,
LocalWindowInfo provides owner.windowInfo,
content = content
)
}

CompositionLocal 作用是为了避免组合函数间传递显式参数,这样可以通过隐式参数传递给被调用的组合函数,其内部实现也是利用了 SlotTable 存储数据。

@Stable
sealed class CompositionLocal constructor(defaultFactory: () -> T) {
@Suppress(“UNCHECKED_CAST”)
internal val defaultValueHolder = LazyValueHolder(defaultFactory)

@Composable
internal abstract fun provided(value: T): State //

@OptIn(InternalComposeApi::class)
inline val current: T
@ReadOnlyComposable
@Composable
get() = currentComposer.consume(this) // 获取当前CompositionLocalScope对应的值
}

定义好 CompositionLocal 之后,需要通过 CompositionLocalProvider 方法绑定数据,ProvidedValue 可以通过 ProvidableCompositionLocal 提供的中缀方法 provides 返回。

@Composable
@OptIn(InternalComposeApi::class)
fun CompositionLocalProvider(vararg values: ProvidedValue<*>, content: @Composable () -> Unit) {
currentComposer.startProviders(values) // 在SlotTable的groups插入key为providerKey和providerValuesKey的group数据
content()
currentComposer.endProviders()
}

接着来看下 CompositionLocal 如何获取数据,通过代码看到直接通过 composer#consume 返回,而 consume 方法内部最终还是通过 CompositionLocalMap (实际是一个 PersistentMap<CompositionLocal<Any?>, State<Any?>> 结构)获取数据,其在 SlotTable 中对应的 groupKey 是 compositionLocalMapKey

@Stable
sealed class CompositionLocal constructor(defaultFactory: () -> T) {

inline val current: T
@ReadOnlyComposable
@Composable
get() = currentComposer.consume(this)
}

internal class ComposerImpl(…) {

override fun consume(key: CompositionLocal): T =
resolveCompositionLocal(key, currentCompositionLocalScope())

private fun resolveCompositionLocal(
key: CompositionLocal,
scope: CompositionLocalMap
): T = if (scope.contains(key)) {
scope.getValueOf(key)
} else {
key.defaultValueHolder.value
}

}

看到这里我们大概明白了 CompositionLocal 实现逻辑:

  • 首先定义 CompositionLocal
  • 通过 CompositionLocalProvoder 方法在 compose 函数嵌入插入 composer#startProviderscomposer#endProviders ,最终在 SlotTable 存入数据

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注鸿蒙)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

ositionLocalScope())

private fun resolveCompositionLocal(
key: CompositionLocal,
scope: CompositionLocalMap
): T = if (scope.contains(key)) {
scope.getValueOf(key)
} else {
key.defaultValueHolder.value
}

}

看到这里我们大概明白了 CompositionLocal 实现逻辑:

  • 首先定义 CompositionLocal
  • 通过 CompositionLocalProvoder 方法在 compose 函数嵌入插入 composer#startProviderscomposer#endProviders ,最终在 SlotTable 存入数据

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注鸿蒙)
[外链图片转存中…(img-MZeaRxzd-1713405935726)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值