Android compose 重建流程1

前言

本文是笔者学习Compose是如何自动触发UI刷新的笔记,可能缺乏一定可读性和教导性.(建议阅读参考文献更具启发性)

使用以下BOM作为研究环境.

composeBom = "2024.04.01"
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }

我们看一下下面的程序,再点击OneComposable按钮的时候为什么仅仅TwoComposable重组,其他的Composable不会?背后是如何实现特定作用域定向刷新?

@Composable
fun MainCompose3() {Column {Logd("invoke MainCompose3")val displayState = remember { mutableIntStateOf(1) }OneComposable(displayState)TwoComposable(displayState)}
}@Composable
fun TwoComposable(flagState: MutableState<Int>) {Logd("invoke TwoComposable")Text("hello world ${flagState.value}")
}@Composable
fun OneComposable(flagState: MutableState<Int>) {Logd("invoke OneComposable")Button(onClick = {flagState.value = ++flagState.value}) {Text("Change flagState")}}
fun Logd(msg:String){Log.d("test",msg)
}

当点击OneComposable的按钮重组输出:

invoke TwoComposable

使用原始View模拟自动刷新

我们借用原始 View 系统配合快照完成一个类似 Compose 自动局部刷新.
建议读者先自行阅读快照文献:
一文看懂 Jetpack Compose 快照系统
我们布局如下:

在这里插入图片描述

//MainActivity.kt
class MainActivity : ComponentActivity() {//private val displayOneState = mutableIntStateOf(1)private val displayTwoState = mutableIntStateOf(1)override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)val layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,FrameLayout.LayoutParams.MATCH_PARENT)val rootView = FrameLayout(this)setContentView(rootView, layoutParams)//Composer是我们自定义的Composer.setContentView(rootView){//LinearLayoutOneColumnComposable(Composer, rootView) { view ->//Texview 1 read and show displayOneStateOneTvComposable(Composer, view, displayOneState)//Textview 2 => read and show displayTwoStateTwoTvComposable(Composer, view, displayTwoState)//Button => modify displayOneStateOneBtnComposable(Composer, view, displayOneState)}}}
}    

布局展示如下:

在这里插入图片描述

多次点击按钮后Texview 1更新文案

在这里插入图片描述

我们首先需要了解Composer#setContentView做什么.

//MainActivity.kt
object Composer {fun setContentView(rootView:ViewGroup,content: (ViewGroup) -> Unit){//创建一个快照,用于感知content对于 state 的读取,val snapshot = Snapshot.takeSnapshot(readObserver = { mutableState ->//每次在 content 函数中任意读取 state 都会回调到此.//content有多个函数OneTvComposable,TwoTvComposable都会读取不同的state.//我们如何标记当前state被那个函数读取?})//进入快照中enter函数才可感知state读写snapshot.enter {content.invoke(rootView)}}
}

为了在readObserver回调,为感知是那个函数读取,我们设计一个栈算法,每次调用xxxxComposable 函数的时候构建一个UpdateScope,并压入栈中.在函数结束的时候弹出栈.
为了方便我们把UpdateScope称为更新域.

我们首先查看读取栈的代码:


val state2Scope = MutableScatterMap<Any, MutableSet<UpdateScope>>()
val scopeStack: Deque<UpdateScope> = java.util.ArrayDeque<UpdateScope>()Snapshot.takeSnapshot(readObserver = { mutableState ->//一个state 可能会被多个UpdateScope读取var updateScopes = state2Scope[mutableState]if (updateScopes.isNullOrEmpty()) {updateScopes = mutableSetOf()state2Scope[mutableState] = updateScopes}//查看栈顶的updateScopes然后放入一个映射中.//这样我们就可以知道 state 更新了哪些 updateScopes 需要被重新重组val updateScope = scopeStack.peek();if (updateScope != null) {updateScopes.add(updateScope)}})
//略

最后我们看看如何构造这些updateScopes栈对象.

//id 标记某个composable函数方便索引
//update回调composable在数据更新的时候
data class UpdateScope(val id:Int, val update:()->Unit)
fun OneTvComposable(composer: Composer, parent: ViewGroup, state: MutableIntState) {// 创建scope 然后赋予一个唯一 id 方便查找.val scope = UpdateScope(0x00001){//数据更新的时候OneTvComposable(composer,parent,state)}scopeStack.push(scope)//创建一个 Textview 去展示val viewId = "OneText"MyText(viewId, "${state.intValue}", parent)scopeStack.pop()
}

其他函数类似就不演示,我们图展示下列Composable代码运行流程

OneColumnComposable(Composer, rootView) { view ->OneTvComposable(Composer, view, displayOneState)TwoTvComposable(Composer, view, displayTwoState)OneBtnComposable(Composer, view, displayOneState)
}

OneColumnComposable 函数内部不会读取任何状态,所以仅仅会压入栈不会触发 snapshot 读取.图示例如下:

fun OneColumnComposable(composer: Composer,parent: ViewGroup,content: (ViewGroup) -> Unit
) {// 创建scope 然后赋予一个唯一 id 方便查找.val scope = UpdateScope(0x00004){//数据更新的时候OneColumnComposable(composer,parent,state)}scopeStack.push(scope)//创建一个 LinearLayoutMyColumn("oneColumn", parent, { view ->content.invoke(view)})scopeStack.pop()}

在这里插入图片描述

运行OneTvComposable,会压入一个新的 Scope,由于在这个函数读取了 state,会触发 snapshot 读取回调,更新updateScope映射信息

在这里插入图片描述

运行TwoTvComposable时,OneTvComposable会弹出之前的栈.会压入一个新的 Scope,由于在这个函数读取了 state,会触发snapshot 读取回调,更新updateScope映射信息


fun TwoTvComposable(composer: Composer, parent: ViewGroup, state: MutableIntState) {// 创建scope 然后赋予一个唯一 id 方便查找.val scope = UpdateScope(0x00002){//数据更新的时候TwoTvComposable(composer,parent,state)}scopeStack.push(scope)//创建一个 Textview 去展示val viewId = "TwoText"MyText(viewId, "${state.intValue}", parent)scopeStack.pop()
}

在这里插入图片描述

OneBtnComposable函数并不会读取 state 而是简单的写入.所以并不会影响 state2Scope

fun OneBtnComposable(composer: Composer, parent: ViewGroup, state: MutableIntState) {// 创建scope 然后赋予一个唯一 id 方便查找.val scope = UpdateScope(0x00003){//数据更新的时候OneBtnComposable(composer,parent,state)}val viewId = "OneBtn"MyButton(viewId, "changeState", parent, {state.intValue += 1})scopeStack.pop()}

在这里插入图片描述

OneBtnComposable函数结束的时候OneColumnComposable也对应结束了函数周期.所有 信息将会从scopestack将会弹出

在这里插入图片描述

现在我们有了state2Scope存储信息,在 state 更改时调用对应的UpdateScope的回调即可完成更新.

Snapshot.registerGlobalWriteObserver {//全局作用于的快照被写入的时候回调//调用通知.此时会触发registerApplyObserver回调Snapshot.sendApplyNotifications()
}
Snapshot.registerApplyObserver { anies, snapshot ->for (any in anies) {//any 就是我们的 stateval updateScopes = state2Scope[any]//重新调用函数触发更新updateScopes.update()}
}

上面的设计方案有一个比较致命的性能问题比如我们看一下下面的代码,根布局会根据backgroundColorState修改自身背景颜色

private val backgroundColorState = mutableIntStateOf(Color.BLUE)
//OneColumnComposable会读取backgroundColorState变量去设置背景色
OneColumnComposable(Composer, rootView,backgroundColorState) { view ->OneTvComposable(Composer, view, displayOneState)TwoTvComposable(Composer, view, displayTwoState)//按钮会修改背景色 OneBtnComposable(Composer, view, backgroundColorState)
}
fun OneColumnComposable(composer: Composer,parent: ViewGroup,content: (ViewGroup) -> Unit
) {// 创建scope 然后赋予一个唯一 id 方便查找.val scope = UpdateScope(0x00004){//数据更新的时候OneColumnComposable(composer,parent,state)}scopeStack.push(scope)//创建一个 LinearLayout,并制定背景色颜色MyColumn("oneColumn", parent,backgroundColorState.value, { view ->content.invoke(view)})scopeStack.pop()
}

这时候触发切换颜色的时候我们期待仅有OneColumnComposable会被回调.但是实际上OneColumnComposable,OneTvComposable,TwoTvComposable,OneBtnComposable全部会重新触发.我们可以在建立一个新的树称为 Group树,这个树中每个节点存储是否Dirty,然后更新的时候选择性更新判断.

树中节点如下

data class Group(val id: Int,var parent: Group?,val child: MutableScatterMap<Int, Group> = mutableScatterMapOf()
) {//标记节点是否需要更新var dirtyFlag: Int = DIRTY_STATE_INITcompanion object {val ROOT_NODE = Group(0x0000, null, mutableScatterMapOf())//节点未重组过,需要重组val DIRTY_STATE_INIT = 0//节点是干净的不需要被重组val DIRTY_STATE_CLEAN = DIRTY_STATE_INIT + 1//节点数据过时需要重组val DIRTY_STATE_DECAY = DIRTY_STATE_CLEAN + 1}override fun toString(): String {return """ self ${this.id} -> child [${child.joinToString(transform = { ke, v -> v.toString() })}]""".trimMargin()}
}

我们可以在创建scope栈的时候结合一起构建这个 group 树.我们举例OneTvComposable来说明.我们顺带把所有这类任务的代码放入一个叫Composer对象中

fun OneTvComposable(composer: Composer, parent: ViewGroup, state: MutableIntState) {//创建一个 scope 栈对象,并且创建一个 group 树节点val group = composer.startGroup(0x00001)if (group.change()) {Logd("invoke OneTvComposable")val viewId = "OneText"MyText(viewId, "${state.intValue}", parent)} else {}//结束的时候时候我们我们弹出 scope 栈对象,并维护 group 树节点group.endScope {OneTvComposable(composer, parent, state)}
}//Composer对象内嵌函数
class Composer{//标记由于 state 写入需要重新刷新的 groupval dirtyGroup: MutableScatterMap<Int, Group> = mutableScatterMapOf<Int, Group>()fun startGroup(composableId: Int): UpdateScope {//调用startGroup此 Group已经被重组,移除标记val dirtyGroup = dirtyGroup.remove(composableId)//构建好 group 树节点,这个树用于判断数据变化时更新策略.提升重组性能val group = if (dirtyGroup == null) {val parentGroup =scopeStack.peek()?.group ?: rootNodeval group = dirtyGroup ?: parentGroup.child[composableId] ?: Group(composableId,scopeStack.peek()?.group,mutableScatterMapOf())parentGroup.child[composableId] = groupgroup} else {dirtyGroup}//构造 scope 栈对象,方便感知刷新域val updateScope = UpdateScope(composableId, group, null)scopeStack.push(updateScope)return updateScope}//弹出栈,并重新标记 group 为干净fun endScope(update: (() -> Unit)) {this.update = updateComposer.scopeStack.pop()group.dirtyFlag = DIRTY_STATE_CLEAN}
}

最后我们在查阅下写入回调处的处理.

Snapshot.registerGlobalWriteObserver {Snapshot.sendApplyNotifications()
}
Snapshot.registerApplyObserver { anies, snapshot ->for (any in anies) {val updateScopes = state2Scope[any]updateScopes?.forEach { scope ->dirtyGroup[scope.id] = (scope.group)//仅标记被污染的 group,可以避免子group也过度参与.scope.group.dirtyFlag = DIRTY_STATE_DECAYupdateFrame(scope)}}
}
//开始重组
private fun updateFrame(updateScope: UpdateScope) {while (scopeStack.isNotEmpty()) {val popScope = scopeStack.pop()if (updateScope == popScope) {break}}updateScope.update?.invoke()
}

上面便是一个简易版本 View 下模拟 compose 流程.Group树用数据变时怎么样刷新,UpdateSope用于在哪刷新,而Composable描述了怎么样的一个 View

在这里插入图片描述

最后我们贴出完整相关代码

data class Group(val id: Int,var parent: Group?,val child: MutableScatterMap<Int, Group> = mutableScatterMapOf()
) {var dirtyFlag: Int = DIRTY_STATE_INITcompanion object {val ROOT_NODE = Group(0x0000, null, mutableScatterMapOf())val DIRTY_STATE_INIT = 0val DIRTY_STATE_CLEAN = DIRTY_STATE_INIT + 1val DIRTY_STATE_DECAY = DIRTY_STATE_CLEAN + 1}override fun toString(): String {return """ self ${this.id} -> child [${child.joinToString(transform = { ke, v -> v.toString() })}]""".trimMargin()}
}class UpdateScope(val id: Int, val group: Group, var update: (() -> Unit)? = null) {override fun equals(other: Any?): Boolean {if (other !is UpdateScope) {return false}return other.id == this.id}override fun hashCode(): Int {return this.id}fun endScope(update: (() -> Unit)) {this.update = updateComposer.scopeStack.pop()group.dirtyFlag = DIRTY_STATE_CLEAN}fun change(): Boolean {return group.dirtyFlag == DIRTY_STATE_DECAY || group.dirtyFlag == DIRTY_STATE_INIT}
}object Composer {val state2Scope = MutableScatterMap<Any, MutableSet<UpdateScope>>()val scopeStack: Deque<UpdateScope> = java.util.ArrayDeque<UpdateScope>()val dirtyGroup: MutableScatterMap<Int, Group> = mutableScatterMapOf<Int, Group>()val rootNode: Group = ROOT_NODEinit {Snapshot.registerGlobalWriteObserver {Snapshot.sendApplyNotifications()}Snapshot.registerApplyObserver { anies, snapshot ->for (any in anies) {val updateScopes = state2Scope[any]updateScopes?.forEach { scope ->dirtyGroup[scope.id] = (scope.group)scope.group.dirtyFlag = DIRTY_STATE_DECAYupdateFrame(scope)}}}}private fun updateFrame(updateScope: UpdateScope) {while (scopeStack.isNotEmpty()) {val popScope = scopeStack.pop()if (updateScope == popScope) {break}}updateScope.update?.invoke()}fun startGroup(composableId: Int): UpdateScope {val dirtyGroup = dirtyGroup.remove(composableId)val group = if (dirtyGroup == null) {val parentGroup =scopeStack.peek()?.group ?: rootNodeval group = dirtyGroup ?: parentGroup.child[composableId] ?: Group(composableId,scopeStack.peek()?.group,mutableScatterMapOf())parentGroup.child[composableId] = groupgroup} else {dirtyGroup}val updateScope = UpdateScope(composableId, group, null)scopeStack.push(updateScope)return updateScope}fun setContentView(rootView: ViewGroup, content: (ViewGroup) -> Unit) {val snapshot = Snapshot.takeSnapshot(readObserver = { mutableState ->var updateScopes = state2Scope[mutableState]if (updateScopes.isNullOrEmpty()) {updateScopes = mutableSetOf()state2Scope[mutableState] = updateScopes}val updateScope = scopeStack.peek();if (updateScope != null) {updateScopes.add(updateScope)}})snapshot.enter {content.invoke(rootView)}}
}class MainActivity : ComponentActivity() {private val displayOneState = mutableIntStateOf(1)private val displayTwoState = mutableIntStateOf(1)private val backgroundColorState = mutableIntStateOf(android.graphics.Color.BLUE)override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)val layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,FrameLayout.LayoutParams.MATCH_PARENT)val rootView = FrameLayout(this)setContentView(rootView, layoutParams)Composer.setContentView(rootView) {OneColumnComposable(Composer, rootView, backgroundColorState) { view ->OneTvComposable(Composer, view, displayOneState)TwoTvComposable(Composer, view, displayTwoState)OneBtnComposable(Composer, view, backgroundColorState)}}Log.d("fmy", "tree : ${Composer.rootNode}")}fun OneColumnComposable(composer: Composer,parent: ViewGroup,backgroundColorState: MutableIntState,content: (ViewGroup) -> Unit) {val group = composer.startGroup(0x00004)if (group.change()) {Logd("invoke OneColumnComposable")MyColumn("oneColumn", parent, backgroundColorState.intValue) { view ->content.invoke(view)}} else {}group.endScope {OneColumnComposable(composer, parent, this.backgroundColorState, content)}}fun MyColumn(viewId: String,parent: ViewGroup,backgroundColor: Int,content: (ViewGroup) -> Unit) {val llView = parent.findViewWithTag<LinearLayout>(viewId) ?: LinearLayout(this)if (llView.parent == null) {llView.tag = viewIdval layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT)parent.addView(llView, layoutParams)llView.orientation = LinearLayout.VERTICAL}
//        llView.setBackgroundResource(R.color.teal_200)llView.setBackgroundColor(backgroundColor)content.invoke(llView)}fun MyText(viewId: String, content: String, parent: ViewGroup) {val oldText = parent.findViewWithTag<TextView>(viewId)val textView = if (oldText == null) {val textView = TextView(this)textView.tag = viewIdparent.addView(textView)textView} else {oldText}textView.text = content}fun MyButton(viewId: String, content: String, parent: ViewGroup, click: () -> Unit) {val oldBtn = parent.findViewWithTag<Button>(viewId)val btn = if (oldBtn == null) {val btn = Button(this)btn.tag = viewIdparent.addView(btn)btn} else {oldBtn}btn.text = contentbtn.setOnClickListener { click.invoke() }}fun OneTvComposable(composer: Composer, parent: ViewGroup, state: MutableIntState) {//创建一个 scope 栈对象,并且创建一个 group 树节点val group = composer.startGroup(0x00001)if (group.change()) {Logd("invoke OneTvComposable")val viewId = "OneText"MyText(viewId, "${state.intValue}", parent)} else {}//结束的时候时候我们我们弹出 scope 栈对象,并维护 group 树节点group.endScope {OneTvComposable(composer, parent, state)}}fun TwoTvComposable(composer: Composer, parent: ViewGroup, state: MutableIntState) {val group = composer.startGroup(0x00002)if (group.change()) {val viewId = "TwoText"Logd("invoke TwoTvComposable")MyText(viewId, "${state.intValue}", parent)} else {}group.endScope {TwoTvComposable(composer, parent, state)}}fun OneBtnComposable(composer: Composer, parent: ViewGroup, state: MutableIntState) {val group = composer.startGroup(0x00003)if (group.change()) {val id = "OneBtn"Logd("invoke OneBtnComposable")MyButton(id, "changeState", parent, {
//                state.intValue += 1state.intValue = Color.RED})} else {}group.endScope {OneBtnComposable(composer, parent, state)}}}

Compose 源码阅读

我们有如下Demo作为讲解说明.
一个按钮和一个文本,每次点击按钮触发数字单调递增
在这里插入图片描述

示例代码如下:

//MainActivity.kt
class MainActivity : ComponentActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContent {Home()}}
}
@Composable
private fun Home() {ComposeDemoTheme {Surface {Column {val displayState = remember { mutableIntStateOf(1) }MainCompose(displayState)Button(onClick = {displayState.intValue = ++displayState.intValue}) {Text("increase displayState")}}}}
}
@Composable
fun MainCompose(displayState: MutableState<Int>) {val value = displayState.valueText("display $value")
}@Composable
@Preview
fun HomePreview(){Home()
}

本文需要有基础的快照SlotTable概念以避免重复造轮子.

手撸 View 下局部自动更新

MainCompose原始的函数会在编译后变为以下代码.

   @Composable@ComposableTarget(applier = "androidx.compose.ui.UiComposable")public static final void MainCompose(@NotNull final MutableState displayState, @Nullable Composer $composer, final int $changed) {//每一个 compose 都会构建一个 Group,最终Group也会组成一个树.(一定要注意这个不是渲染树 LayoutNode,Compose 里有多颗树,这颗树用做数据处理) //而startRestartGroup也会创建一个 Group 放入树中$composer = $composer.startRestartGroup(-1327587884);ComposerKt.sourceInformation($composer, "C(MainCompose)47@1341L22:MainActivity.kt#ffoge4");//结合一些数据判断当前是否可以跳过重组int $dirty = $changed;if (($changed & 14) == 0) {$dirty |= $composer.changed(displayState) ? 4 : 2;}//如果当前 Composeable是skippable那么会结合当前入参判断是否能跳过//skippable本文后面会简单介绍if (($dirty & 11) == 2 && $composer.getSkipping()) {$composer.skipToGroupEnd();} else {if (ComposerKt.isTraceInProgress()) {ComposerKt.traceEventStart(-1327587884, $dirty, -1, "com.example.composedemo.MainCompose (MainActivity.kt:45)");}//如果需要重组那么进行int value = ((Number)displayState.getValue()).intValue();TextKt.Text--4IGK_g("display " + value, (Modifier)null, 0L, 0L, (FontStyle)null, (FontWeight)null, (FontFamily)null, 0L, (TextDecoration)null, (TextAlign)null, 0L, 0, false, 0, 0, (Function1)null, (TextStyle)null, $composer, 0, 0, 131070);if (ComposerKt.isTraceInProgress()) {ComposerKt.traceEventEnd();}}//标记当前 Group 在树中结束,并返回一个 Compose 更新域(ScopeUpdateScope).//ScopeUpdateScope会在displayState更新时调用updateScope进而发生重组ScopeUpdateScope var5 = $composer.endRestartGroup();if (var5 != null) {var5.updateScope((Function2)(new Function2() {public final void invoke(Composer $composer, int $force) {//如果数据变更会会回调MainActivityKt.MainCompose(displayState, $composer, RecomposeScopeImplKt.updateChangedFlags($changed | 1));}// $FF: synthetic method// $FF: bridge methodpublic Object invoke(Object p1, Object p2) {this.invoke((Composer)p1, ((Number)p2).intValue());return Unit.INSTANCE;}}));}}

你会惊讶的发现函数背后做的事情和我们自己实现在 View 下差不多.
我们这里额外补充一个细节,你会注意到有一个$composer.getSkipping()函数才会判断当前 Composeable 是否会跳过,否则一定会触发重组.

那么时候函数getSkipping才为 true 呢?
Compose 编译器会为每个Composable做一个标记.如果利用可以利用入参和之前传入参数判断相等那么可以被标记skippable.

我们比较下下面的两个函数是否都可以被标记skippable?

//可以被标记skippable,因为displayState数值可以取出来和之前的比较
@Composable
fun MainCompose(displayState: MutableState<Int>) {val value = displayState.valueText("display $value")
}//不可以被标记skippable,因为list的实例可以比较,但是内部的内容和顺序不可推断
@Composable
fun MainCompose2(list: MutableList<String>) {Text("display $${list.joinToString { it }}")
}

相关具体知识点建议阅读
what-do-the-stable-and-immutable-annotations-mean-in-jetpack-compose

有相关工具可以打印出编译视角下函数结构,这里直接给出结果:

//标记skippable
restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun MainCompose(stable displayState: MutableState<Int>
)
//不标记skippable,这个函数被重组的时候一定会重新触发.
restartable scheme("[androidx.compose.ui.UiComposable]") fun MainCompose2(unstable list: MutableList<String>
)

我们看下 MainCompose2被编译后的代码是不会存在skipToGroupEnd函数的调用.重组时直接触发不存在跳过逻辑.

@Composable
fun MainCompose2(list: MutableList<String>) {Text("display $${list.joinToString { it }}")
}@Composable@ComposableTarget(applier = "androidx.compose.ui.UiComposable")public static final void MainCompose2(@NotNull final List list, @Nullable Composer $composer, final int $changed) {$composer = $composer.startRestartGroup(1711764239);ComposerKt.sourceInformation($composer, "C(MainCompose2)51@1428L44:MainActivity.kt#ffoge4");if (ComposerKt.isTraceInProgress()) {ComposerKt.traceEventStart(1711764239, $changed, -1, "com.example.composedemo.MainCompose2 (MainActivity.kt:50)");}TextKt.Text--4IGK_g("display $" + CollectionsKt.joinToString$default((Iterable)list, (CharSequence)null, (CharSequence)null, (CharSequence)null, 0, (CharSequence)null, (Function1)null.INSTANCE, 31, (Object)null), (Modifier)null, 0L, 0L, (FontStyle)null, (FontWeight)null, (FontFamily)null, 0L, (TextDecoration)null, (TextAlign)null, 0L, 0, false, 0, 0, (Function1)null, (TextStyle)null, $composer, 0, 0, 131070);if (ComposerKt.isTraceInProgress()) {ComposerKt.traceEventEnd();}ScopeUpdateScope var3 = $composer.endRestartGroup();if (var3 != null) {var3.updateScope((Function2)(new Function2() {public final void invoke(Composer $composer, int $force) {MainActivityKt.MainCompose2(list, $composer, RecomposeScopeImplKt.updateChangedFlags($changed | 1));}// $FF: synthetic method// $FF: bridge methodpublic Object invoke(Object p1, Object p2) {this.invoke((Composer)p1, ((Number)p2).intValue());return Unit.INSTANCE;}}));}}

我们 Compose 下的startRestartGroup是如何实现,

//Composer.ktclass ComposerImpl(@ComposeCompilerApioverride fun startRestartGroup(key: Int): Composer {//创造一个 Group 树节点,由于这块比较复杂不展开细说start(key, null, GroupKind.Group, null)//创建一个重组域addRecomposeScope()return this}//创建一个重组域放入栈中private fun addRecomposeScope() {//...略val scope = RecomposeScopeImpl(composition as CompositionImpl)invalidateStack.push(scope)//...略}@ComposeCompilerApioverride fun endRestartGroup(): ScopeUpdateScope? {//...略//弹出栈val scope = if (invalidateStack.isNotEmpty()) invalidateStack.pop()//...略}}

我们最后官方源码中,入口快照 take函数调用处如下所示

 //ReComposer.ktclass Recomposer{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实现

//Composition.kt
//以 state 为 key,RecomposeScopeImpl为 value
//value内部还有一层List封装,因为 state 可以映射多个RecomposeScopeImpl
private val observations = ScopeMap<RecomposeScopeImpl>()override fun recordReadOf(value: Any) {//value 就是 state 对象//currentRecomposeScope就是更新域composer.currentRecomposeScope?.let {//存储state 和RecomposeScopeImpl关系observations.add(value, it)}
}internal val currentRecomposeScope: RecomposeScopeImpl?
//查阅栈顶 scopeget() = invalidateStack.let {if (childrenComposing == 0 && it.isNotEmpty()) it.peek() else null}

封装的ScopeMap如下:

package androidx.compose.runtime.collectioninternal class ScopeMap<T : Any> {val map = mutableScatterMapOf<Any, Any>()val size get() = map.size//内部会构建 Set 集合放入多个 value 去对应一个 keyfun add(key: Any, scope: T) {map.compute(key) { _, value ->when (value) {null -> scopeis MutableScatterSet<*> -> {@Suppress("UNCHECKED_CAST")(value as MutableScatterSet<T>).add(scope)value}else -> {if (value !== scope) {val set = MutableScatterSet<T>()@Suppress("UNCHECKED_CAST")set.add(value as T)set.add(scope)set} else {value}}}}}
}

我们知道快照有两个作用域一个全局的和 snapshot.enter后绑定的. 而我们业务中往往在全局作用域去写入state,所以本文我们先不阅读writeObserverOf代码.(如果对快照概念模糊建议阅读参考文献)
Compose全局写入观察位于如下代码中:

//Recomposer.kt
private suspend fun recompositionRunner(block: suspend CoroutineScope.(parentFrameClock: MonotonicFrameClock) -> Unit
) {//...Snapshot.registerApplyObserver { changed, _ ->//这里 Lamba最后的deriveStateLocked会返回一个协程的Continuation//Continuation.resume 调用会恢对应协程继续运行synchronized(stateLock) {if (_state.value >= State.Idle) {changed.fastForEach {//it 是 state 对象//将所有被修改 state 放入集合中snapshotInvalidations.add(it)}//最后通知某个协程函数,去触发重组deriveStateLocked()} else null}?.resume(Unit)}//...
}private var workContinuation: CancellableContinuation<Unit>? = null
private fun deriveStateLocked(): CancellableContinuation<Unit>? {return if (newState == State.PendingWork) {//这里高阶函数的作用是先workContinuation返回,再将workContinuation设置为 nullworkContinuation.also {workContinuation = null}} else null
}

我们通过上面的分析workContinuation赋值点就是就是Compose开始重组核心点

//Composer.kt
suspend fun runRecomposeAndApplyChanges() = recompositionRunner { parentFrameClock ->//..略//为简化流程shouldKeepRecomposing可以视为永远为 truewhile (shouldKeepRecomposing) {//判断当前是否dirty 的 scope,如果没有那么将当前协程挂起,并将continuation 赋值给workContinuation//可以简单判断snapshotInvalidations为空就执行挂起awaitWorkAvailable()//等候下一个 VSYNC 回调执行实际重组.parentFrameClock.withFrameNanos { frameTime ->//这里会取出 dirty 的scope 开始进行重组工作//...略//toRecompose是一个CompositionImpl集合.//Mainwhile (toRecompose.isNotEmpty() || toInsert.isNotEmpty()) {try {toRecompose.fastForEach { composition ->alreadyComposed.add(composition)//最终会取出对应 scope回调 递归回调函数performRecompose(composition, modifiedValues)?.let {toApply += it}}} catch (e: Exception) {processCompositionError(e, recoverable = true)clearRecompositionState()return@withFrameNanos} finally {toRecompose.clear()}}}}

我们最后看看awaitWorkAvailable相关代码

//Recomposer.ktprivate val hasSchedulingWork: Booleanget() = synchronized(stateLock) {//是否有 dirty 的 scopesnapshotInvalidations.isNotEmpty() ||compositionInvalidations.isNotEmpty() ||hasBroadcastFrameClockAwaitersLocked}private suspend fun awaitWorkAvailable() {if (!hasSchedulingWork) {suspendCancellableCoroutine<Unit> { co ->synchronized(stateLock) {//如果有 dirty 的数据那么直接恢复协程完成重组if (hasSchedulingWork) {co} else {//挂起协程workContinuation = conull}}?.resume(Unit)}}
}

参考

一文看懂 Jetpack Compose 快照系统
探索 Jetpack Compose 内核:深入 SlotTable 系统
what-do-the-stable-and-immutable-annotations-mean-in-jetpack-compose

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/455715.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【linux】物理卷、卷组、逻辑卷

概述 初次了解物理卷、卷组和逻辑卷这些概念&#xff0c;大概理了下这三个概念之间的关系&#xff0c;只是一点皮毛&#xff0c;用于大致理解&#xff1a; 个人感觉很像虚拟化的过程&#xff0c;物理卷就相当于物理设备&#xff1b;卷组相当于把这些物理设备分组了&#xff1…

有效三角形的个数---双指针法

目录 一&#xff1a;题目 二&#xff1a;算法原理 三&#xff1a;编写代码 一&#xff1a;题目 题目链接&#xff1a;. - 力扣&#xff08;LeetCode&#xff09; 二&#xff1a;算法原理 三&#xff1a;编写代码 int triangleNumber(vector<int>& nums) {//1.优…

解锁PDF权限密码

目录 背景: 定义与功能&#xff1a; 过程&#xff1a; 主要功能&#xff1a; 使用方式&#xff1a; 使用限制&#xff1a; 注意事项&#xff1a; 总结&#xff1a; 背景: 前段时间自己设置了PDF文件的许可口令&#xff0c;忘了口令导致自己无法编辑内容等&#xff0c;这…

养宠家庭必备,双十一特辑——性价比高的宠物空气净化器推荐

对于养宠家庭来说&#xff0c;宠物空气净化器简直就是仅次于空调的人类最伟大发明。尤其是到了宠物疯狂掉毛的换毛季节&#xff0c;宠物空气净化器成为铲屎官们抵御满屋浮毛纷飞必不可少的清洁神器&#xff0c;除了价格有点高之外&#xff0c;可以说是没有什么缺点了。 养宠七年…

WEB前端使用标签制作网页

需要使用HTML的一些基本标签制作网页 基本代码如下: <!DOCTYPE html> <html><head><meta charset"utf-8"><title></title></head><body><form action"#" method"post" enctype"text/…

激活函数(sigmoid、tanh、ReLu)

1️⃣ 激活函数的作用 激活函数为神经网络引入非线性&#xff0c;如果没有激活函数&#xff0c;即使网络层数再多&#xff0c;也只能处理线性可分问题。 在机器学习中&#xff0c;线性可分问题指的是可以通过一条直线&#xff08;或高维空间的一个超平面&#xff09;将数据完全…

GS-SLAM Dense Visual SLAM with 3D Gaussian Splatt 论文阅读

项目主页 2024 CVPR (highlight) https://gs-slam.github.io/ 摘要 本文提出了一种基于3D Gaussian Splatting方法的视觉同步定位与地图构建方法。 与最近采用神经隐式表达的SLAM方法相比&#xff0c;本文的方法利用实时可微分泼溅渲染管道&#xff0c;显著加速了地图优化和…

Django学习- ORM基础操作_创建数据

ORM操作&#xff1a; 管理器对象&#xff1a; 创建数据&#xff1a; Django shell 想要操作模型对象&#xff0c;首先我们需要把它引进Django shell中 >>> from bookstore.models import Book >>> b1 Book.objects.create(titleAI, pub清华大学出版社, pr…

开挖 Domain - 前奏

WPF App 主机配置 Microsot.Extension.Hosting 一键启动&#xff08;配置文件、依赖注入&#xff0c;日志&#xff09; // App.xaml.cs 中定义 IHost private readonly IHost _host Host.CreateDefaultBuilder().ConfigureAppConfiguration(c > {_ c.SetBasePath(Envi…

电脑必备快捷键大全

#1024程序员节|征文# 小伙伴们&#xff01;想要提高学习效率&#xff0c;成为电脑高手吗&#xff1f;今天&#xff0c;我为大家整理了一份超实用的电脑快捷键清单&#xff01;无论是写论文、做PPT还是数据录入&#xff0c;这些快捷键都能帮你事半功倍&#xff01;快收藏起来吧&…

PDF.js的使用及其跨域问题解决

目录 一、PDF.js 简介 二、使用配置和步骤 1.引入PDF.js 2.加载PDF文件 3.渲染PDF页面 三、在Vue中使用PDF.js示例 1.安装PDF.js 2.在Vue组件中使用 四、在原生js中使用PDF.js示例 1.加载PDF文件并渲染页面 五、解决跨域问题 1.服务器配置 2.使用代理服务器 下面介…

编辑器、节点树、基础设置

目录 节点 查看当前节点拥有的属性 Position&#xff08; 父节点&#xff09; 保存 主场景 运行 编辑器操作 添加子节点 收藏节点 Sprite2D节点 控制节点是否可见 当父节点不可见&#xff0c;它的子节点也会不可见 基础编辑工具&#xff08;场景浏览器左上角&#x…

052_python基于Python高校岗位招聘和分析平台

目录 系统展示 开发背景 代码实现 项目案例 获取源码 博主介绍&#xff1a;CodeMentor毕业设计领航者、全网关注者30W群落&#xff0c;InfoQ特邀专栏作家、技术博客领航者、InfoQ新星培育计划导师、Web开发领域杰出贡献者&#xff0c;博客领航之星、开发者头条/腾讯云/AW…

Lesson10---list

Lesson10—list 第10章 c的list的使用和实现 文章目录 Lesson10---list前言一、list的初始化二、list的遍历1.迭代器2.范围for 三、list常用的内置函数1.sort&#xff08;慎用&#xff09;2.unique3.reverse4.merge5.splice 四、模拟实现1.基本框架2.构造函数3.push_back4. 遍…

PON架构(全光网络)

目前组网架构 世界上有一种最快的速度又是光&#xff0c;以前传统以太网络规划满足不了现在的需求。 有线网 无线网 全光网络方案 场景 全光网络分类 以太全光网络 PON&#xff08;Pas-sive-Optical Network 无源光网络&#xff09; 再典型的中大型高校网络中 推荐万兆入…

Java项目-基于springboot框架的原创歌曲分享系统项目实战(附源码+文档)

作者&#xff1a;计算机学长阿伟 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、ElementUI等&#xff0c;“文末源码”。 开发运行环境 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringBoot、Vue、Mybaits Plus、ELementUI工具&#xff1a;IDEA/…

【功能安全】系统架构设计

目录 01 系统架构介绍 02 投票逻辑架构介绍 03 SIS架构 04 ADS域控制器架构设计 01 系统架构介绍 法规GBT 34590 Part4 part10定义的软件要求、设计和测试子阶段之间的关系&#xff08;其中的3-7个人建议翻译为初始架构设计更合理 &#xff09; 系统架构的作用&#xf…

工具:Typora自定义高效率主题

1 分享主题 工欲善其事必先利其器。分享一个文档编辑器主题。 1.1 特点 &#xff08;1&#xff09;大纲放在右侧、目录放在左侧&#xff0c;互不干扰 &#xff08;2&#xff09;标题颜色特殊处理 1.2 使用方式 打开Typora --> 文件 --> 偏好设置 --> 外观 -->…

给已经写好的裸机程序移植freeRTOS操作系统

接了公司一个项目&#xff0c;这是一个采用Dante模块把I2S数据通过网络交换机转发的音频控制器。包含两个串口配置。一开始以为使用裸机即可满足项目要求&#xff0c;实际上如果只有一个串口确实能满足要求了&#xff0c;现在发现Dante模块也需要串口通讯&#xff0c;2个串口同…

《Windows PE》6.4.2 远程注入DLL

实验四十七&#xff1a;远程注入DLL 写一个窗口程序&#xff0c;将一个dll通过远程注入的方法&#xff0c;注入到第三章的示例程序PEHeader.exe中&#xff0c;支持32位和64位PE。 ●dll.c /*------------------------------------------------------------------------FileNam…