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.mzph.cn/pingmian/57332.shtml

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

相关文章

HarmonyOS Next应用开发——图像PixelMap压缩保存

【高心星出品】 图片编码保存 图片编码指将PixelMap编码成不同格式的存档图片&#xff0c;当前支持打包为JPEG、WebP、png和 HEIF(不同硬件设备支持情况不同) 格式&#xff0c;用于后续处理&#xff0c;如保存、传输等。图片编码是图片解码-图片处理-图片保存的最后环节&…

C#中的接口的使用

定义接口 public interface IMyInterface {int MyProperty { get; set; }void MyMethod(); } 实现类 internal class MyClass : IMyInterface {public int MyProperty { get; set; }public void MyMethod(){Console.WriteLine("MyMethod is called");} } 目录结构…

负载箱的作用?

负载箱&#xff0c;顾名思义&#xff0c;就是用来承载电力设备的箱子。在电力系统中&#xff0c;负载箱的作用非常重要&#xff0c;它不仅可以模拟实际的电力负载&#xff0c;还可以对电力设备进行测试和调试&#xff0c;确保其正常运行。下面详细介绍负载箱的作用。 1. 模拟实…

深入了解Spring重试组件spring-retry

在我们的项目中&#xff0c;为了提高程序的健壮性&#xff0c;很多时候都需要有重试机制进行兜底&#xff0c;最多就场景就比如调用远程的服务&#xff0c;调用中间件服务等&#xff0c;因为网络是不稳定的&#xff0c;所以在进行远程调用的时候偶尔会产生超时的异常&#xff0…

这几次比赛题解

因为考虑到再看&#xff0c;所以将所有题目都做成了pdf格式 梦熊十三连测 T1 这道题其实什么也不用想&#xff0c;就按照题目给的意思来打代码就行&#xff0c;这就有40分可以拿。懒人做法 #include<bits/stdc.h> using namespace std; typedef long long ll; ll read…

MP9928模块分析

MP9928 是一款高性能的同步降压 DC/DC 转换器控制器 IC&#xff0c;具有宽输入范围。以下是其操作和关键特性的总结&#xff1a; 概述 电流模式控制&#xff1a;MP9928 使用电流模式、可编程开关频率控制架构&#xff0c;通过外部 N 沟道 MOSFET 开关来调节输出电压。 反馈和…

Golang | Leetcode Golang题解之第500题键盘行

题目&#xff1a; 题解&#xff1a; func findWords(words []string) (ans []string) {const rowIdx "12210111011122000010020202" next:for _, word : range words {idx : rowIdx[unicode.ToLower(rune(word[0]))-a]for _, ch : range word[1:] {if rowIdx[unico…

【uni-app学习-2】

一、跳转 方法&#xff1a;在methods中去定义方法&#xff1a; 上述为直接跳转&#xff0c;但是当你要跳转页面是由多个可切换页面组成比如&#xff1a; 这个页面其实是由两个页面组成&#xff0c;一个主页&#xff0c;一个我的&#xff0c;两个页面 路由配置需要用到toob…

房屋租赁网站毕业设计基于SpringBootSSM框架的计算机毕业设计

计算机毕业设计/springboot/javaWEB/J2EE/MYSQL数据库/vue前后分离小程序 目录 一、项目背景与目的‌ ‌二、系统需求分析‌ 2.1功能需求 2.2 技术需求 2.3 可执行性 ‌三、系统设计与实现‌ ‌3.1系统架构设计‌&#xff1a; ‌3.2功能模块开发‌&#xff1a; ‌3.3…

golang生成并分析cpu prof文件

1. 定义一个接口&#xff0c;请求接口时&#xff0c;生成cpu.prof文件 在主协程中新启一个协程&#xff0c;当请求接口时&#xff0c;生成一个60秒的cpu.prof文件 go func() {http.HandleFunc("/prof", startProfileHandler)http.ListenAndServe(":9092"…

Spring Boot助力:构建响应式论坛网站

3系统分析 3.1可行性分析 通过对本论坛网站实行的目的初步调查和分析&#xff0c;提出可行性方案并对其一一进行论证。我们在这里主要从技术可行性、经济可行性、操作可行性等方面进行分析。 3.1.1技术可行性 本论坛网站采用SSM框架&#xff0c;JAVA作为开发语言&#xff0c;是…

华为云CodeArts Pipeline架构与内容双重优化,高效助力持续交付!

点击下方链接进入帮助中心 成长地图_流水线 CodeArts Pipeline_华为云

unity学习-全局光照(GI)

在全局光照&#xff08;Lighting&#xff09;界面有两个选项 Realtime Light&#xff08;实时光照&#xff09;&#xff1a;在项目中会提前计算好光照以及阴影的程序&#xff0c;当你需要调用实时全局光照的时候会将程序调用出来使用 Mixed Light&#xff08;烘焙光照&#x…

HBuilder X 中Vue.js基础使用1(三)

一、 模板语法 Vue 使用一种基于 HTML 的模板语法&#xff0c;使我们能够声明式地将其组件实例的数据绑定到呈现的 DOM 上。所有的 Vue 模板都是语法层面合法的 HTML&#xff0c;可以被符合规范的浏览器和 HTML 解析器解析。 英文官网: Vue.js - The Progressive JavaScript Fr…

DPRNN 学习

DPRNN介绍 双路径循环语音分离神经网络&#xff08;Dual-Path RNN&#xff09;由三个处理阶段组成, 编码器、分离和解码器。首先&#xff0c;编码器模块用于将混合波形的短段转换为它们在中间特征空间中的对应表示。然后&#xff0c;该表示用于在每个时间步估计每个源的乘法函…

HCIP-HarmonyOS Application Developer 习题(十四)

&#xff08;多选&#xff09;1、HarmonyOs为应用提供丰富的Al(Artificial Intelligence)能力&#xff0c;支持开箱即用。下列哪些是它拥有的AI能力? A、通用文字识别 B、词性标注 C、实体识别 D、语音播报 答案&#xff1a;ABCD 分析&#xff1a; AI能力简介二维码生成根据开…

(JAVA)贪心算法、加权有向图与求得最短路径的基本论述与实现

1. 贪心算法 1.1 贪心算法的概述&#xff1a; 贪心算法是一种对某些求最优解问题的更简单、更迅速的设计技术。 贪心算法的特点是一步一步地进行&#xff0c;常以当前情况为基础根据某个优化测度作最优选择&#xff0c;而不考虑各种可能的整体情况&#xff0c;省去了为找最优…

【深度学习中的注意力机制6】11种主流注意力机制112个创新研究paper+代码——加性注意力(Additive Attention)

【深度学习中的注意力机制6】11种主流注意力机制112个创新研究paper代码——加性注意力&#xff08;Additive Attention&#xff09; 【深度学习中的注意力机制6】11种主流注意力机制112个创新研究paper代码——加性注意力&#xff08;Additive Attention&#xff09; 文章目录…

【C#】调用本机AI大模型流式返回

【python】AI Navigator的使用及搭建本机大模型_anaconda ai navigator-CSDN博客 【Python】AI Navigator对话流式输出_python ai流式返回-CSDN博客 前两章节我们讲解了使用AI Navigator软件搭建本机大模型&#xff0c;并使用python对大模型api进行调用&#xff0c;使其流式返…

“智能科研写作:结合AI与ChatGPT提升SCI论文和基金申请质量“

基于AI辅助下的高效高质量SCI论文撰写及投稿实践 科学研究的核心在于将复杂的思想和实验成果通过严谨的写作有效地传递给学术界和工业界。对于研究生、青年学者及科研人员&#xff0c;如何高效撰写和发表SCI论文&#xff0c;成为提升学术水平和科研成果的重要环节。系统掌握从…