当前位置: 首页 > news >正文

扬州 网站建设/佛山网站设计实力乐云seo

扬州 网站建设,佛山网站设计实力乐云seo,做网站的财务会涉及到的科目,东莞市最新疫情最新消息前言 本文是笔者学习Compose是如何自动触发UI刷新的笔记,可能缺乏一定可读性和教导性.(建议阅读参考文献更具启发性) 使用以下BOM作为研究环境. composeBom "2024.04.01" androidx-compose-bom { group "androidx.compose", name "compose-bom…

前言

本文是笔者学习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

相关文章:

Android compose 重建流程1

前言 本文是笔者学习Compose是如何自动触发UI刷新的笔记,可能缺乏一定可读性和教导性.(建议阅读参考文献更具启发性) 使用以下BOM作为研究环境. composeBom "2024.04.01" androidx-compose-bom { group "androidx.compose", name "compose-bom…...

C++:模板(2)

目录 非类型模板参数 模板的特化 概念 函数模板特化 类模板特化 全特化 偏特化 模板的分离编译 分离编译的概念 模板的分离编译 ​编辑 模板总结 非类型模板参数 模板参数分为类型形参与非类型形参。 类型形参&#xff1a;在模板参数列表中&#xff0c;跟在class…...

Golang 并发编程:Context 包的使用与并发控制

文章目录 一、简介二、Context 的基本概念1. context 包常用函数 三、Context 的基本用法1. WithCancel&#xff1a;取消任务的上下文 四、超时控制&#xff1a;WithTimeout 和 WithDeadline1. 使用 WithTimeout 控制任务超时2. 使用 WithDeadline 设定截止时间 五、传递上下文…...

QGraphics类型学习使用【Qt】【C++】

QGraphics类型学习使用 需求过程全部完整代码 首先已知&#xff0c;QGraphicsView&#xff0c;QGraphicsScene, QGraphicsItem&#xff0c;分别称为&#xff1a;视图&#xff0c;场景&#xff0c;图元&#xff0c;图表就是各种各样的元素&#xff0c;图片元素&#xff0c;线条元…...

迁移学习和在线学习小结

迁移学习 英文小名: transform learning 简介: 把已经训练好的模型A为基本, 在新场景中, 根据新数据建立模型B 目的: 将某个领域或任务上学习到的知识/模式, 应用到不同但相关的领域/问题中 方法: 1.结构引用 适用情况: 新数据多, 场景相似度高, 可以基于原模型重新训练 2.特征…...

克里金插值(Kriging interpolation)

原理可参考该文件&#xff1a;克里金(Kriging)插值的原理与公式推导 - xg1990 matlab code可参考&#xff1a;Ordinary Kriging - File Exchange - MATLAB Central Some notes: 采用普通克里金时&#xff0c;采样的密度对结果影响非常大。若采样密度不够&#xff0c;误差会非…...

sealed class-kotlin中的封闭类

在 Kotlin 中&#xff0c;sealed class&#xff08;密封类&#xff09;是一种特殊的类&#xff0c;用于限制继承的类的数量。密封类可以被用来表示一组有限的类型&#xff0c;通常用于状态管理或表达多种可能的错误类型。 密封类用 sealed 关键字定义&#xff0c;这意味着只能…...

MongoDB Shell 基本命令(一)

MongoDB Shell 基本命令(一&#xff09; 1. 基本概念 SQL术语/概念MongoDB术语/概念解释/说明databasedb数据库tablecollection数据库表/集合rowdocument数据记录行/文档columnfield数据字段/域indexindex索引table joins表连接,MongoDB不支持primary keyprimary key主键,Mon…...

Flink时间语义和时间窗口

前言 在实际的流计算业务场景中&#xff0c;我们会发现&#xff0c;数据和数据的计算往往都和时间具有相关性。 举几个例子&#xff1a; 直播间右上角通常会显示观看直播的人数&#xff0c;并且这个数字每隔一段时间就会更新一次&#xff0c;比如10秒。电商平台的商品列表&a…...

在wpf中登录成功之后怎么设置主页布局及点击不同的菜单跳转到不同的页面,这个是我们做wpf项目必要会的一个功能

通过frame与page实现在mvvm下的页面跳转 在wpf中登录成功之后怎么设置主页布局及点击不同的菜单跳转到不同的页面_哔哩哔哩_bilibili 1、MainWindow代码 <DockPanel><StackPanel DockPanel.Dock"Top" Height"40"><Grid><Grid.ColumnD…...

基于opencv的人脸闭眼识别疲劳监测

1. 项目简介 本项目旨在实现基于眼部特征的眨眼检测&#xff0c;通过监测眼睛开闭状态来计算眨眼次数&#xff0c;从而应用于疲劳监测、注意力检测等场景。使用了面部特征点检测算法&#xff0c;以及眼部特征比率&#xff08;EAR, Eye Aspect Ratio&#xff09;来判断眼睛的闭…...

aeo认证需要什么材料

AEO&#xff08;Authorized Economic Operator&#xff09;认证&#xff0c;即经认证的经营者认证&#xff0c;是企业信用管理体系的一种高级认证。申请AEO认证时&#xff0c;企业需要准备一系列的材料以证明其符合认证标准。以下是一份详细的AEO认证申请材料清单&#xff1a; …...

【iOS】YYModel

目录 什么是YYModel &#xff1f; 如何使用YYModel &#xff1f; 最简单的Model 与网络请求结合 属性为容器类的Model 白名单和黑名单 Model的嵌套 结语 什么是YYModel &#xff1f; YYModel是一个用于 iOS 和 macOS 开发的高性能的模型框架&#xff0c;主要用于对象和…...

Cadence元件A属性和B属性相互覆盖

最近在使用第三方插件集成到Cadence,协助导出BOM到平台上&#xff0c;方便对BOM进行管理和修改&#xff0c;结果因为属性A和属性B不相同&#xff0c;导致导出的BOM错误。如下图&#xff1a; ​​ 本来我们需要导出Q12&#xff0c;结果给我们导出了Q13&#xff0c;或者反之&…...

【火山引擎】语音合成 | HTTP接口 | 一次性合成 | python

目录 一 准备工作 二 HTTP接口(一次性合成-非流式) 1 接口说明 2 身份认证 3 请求方式 三 实践 四 注意事项 火山引擎语音合成TTS(Text-to-Speech)是一种基于云计算的语音合成服务,可以将文本转化为自然、流畅的语音。以下是火山引擎TTS的主要功能和特点: ①多种语音…...

YOLOv11改进-卷积-空间和通道重构卷积SCConv

本篇文章将介绍一个新的改进模块——SCConv&#xff08;小波空间和通道重构卷积&#xff09;&#xff0c;并阐述如何将其应用于YOLOv11中&#xff0c;显著提升模型性能。为了减少YOLOv11模型的空间和通道维度上的冗余&#xff0c;我们引入空间和通道重构卷积。首先&#xff0c;…...

记录一次从nacos配置信息泄露到redis写计划任务接管主机

经典c段打点开局。使用dddd做快速的打点发现某系统存在nacos权限绕过 有点怀疑是蜜罐&#xff0c;毕竟nacos这实在是有点经典 nacos利用 老规矩见面先上nacos利用工具打一波看看什么情况 弱口令nacos以及未授权访问&#xff0c;看这记录估计被光顾挺多次了啊 手动利用Nacos-…...

Unity加载界面制作

效果 UI部分 结构 说下思路: 因为是加载界面,所以最上层是一个Panel阻止所有的UI交互,这个Panel如果有图片就加一个图片,如果没有可以把透明度调到最大,颜色设为黑色. 下面最核心的就是一个进度条了,有图片的话,将进度条的底放进来,将进度条锚点设为下中,将滑动块的尺寸设为0.…...

最好的ppt模板网站是哪个?做PPT不可错过的18个网站!

现在有很多PPT模板网站&#xff0c;但真正免费且高质量的不多&#xff0c;今天我就分享主流的国内外PPT模板下载网站&#xff0c;并且会详细分析这些网站的优缺点&#xff0c;这些网站都是基于个人实际使用经验的&#xff0c;免费站点会特别标注&#xff0c;让你可以放心下载&a…...

煤矿安全监测监控作业题库

第一部分 安全法律法规知识子题库 单选题 1.《安全生产法》规定&#xff0c;生产经营单位应当向从业人员如实告知作业场所和工作岗位存在的(A)、防范措施以及事故应急措施。 A. 危险因素    B. 人员状况    C. 设备状况    D. 环境状况 2.《安全生产法》规定&…...

【记录】Django数据库的基础操作

数据库连接 在Django中使用 mysqlclient 这个包用于数据库的连接&#xff0c;切换至 Django环境中直接 pip install mysqlclient 安装此包 1 数据库连接配置 在项目目录下的setting.py中配置 DATABASES {default: {ENGINE: django.db.backends.mysql,NAME: mini,#数据库名US…...

XHCI 1.2b 规范摘要(五)

系列文章目录 XHCI 1.2b 规范摘要&#xff08;一&#xff09; XHCI 1.2b 规范摘要&#xff08;二&#xff09; XHCI 1.2b 规范摘要&#xff08;三&#xff09; XHCI 1.2b 规范摘要&#xff08;四&#xff09; XHCI 1.2b 规范摘要&#xff08;五&#xff09; 文章目录 系列文章目…...

小程序短链接生成教程

文章目录 一、小程序短链接&#xff08;必须发布正式的小程序才能生成短链接&#xff01;&#xff01;&#xff01;&#xff09;二、使用步骤1.获取token信息2.获取短链接 总结 一、小程序短链接&#xff08;必须发布正式的小程序才能生成短链接&#xff01;&#xff01;&#…...

C++进阶之路:再谈构造函数、static成员、友元(类与对象_下篇)

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢&#xff0c;在这里我会分享我的知识和经验。&am…...

C 函数指针与回调函数

C 函数指针与回调函数 在C语言中&#xff0c;函数指针和回调函数是两个非常强大的概念&#xff0c;它们在提高代码的灵活性和模块化方面发挥着重要作用。本文将详细介绍C语言中的函数指针和回调函数&#xff0c;包括它们的定义、用法和实际应用场景。 函数指针 定义 函数指…...

CTF(九)

导言&#xff1a; 本文主要讲述在CTF竞赛网鼎杯中&#xff0c;web类题目AreUSerialz。 靶场链接&#xff1a;BUUCTF在线评测 一&#xff0c;分析代码。 看到了一大段php代码。 <?php// 引入flag.php文件 include("flag.php");// 高亮显示当前文件 highlight…...

三种单例实现

1、不继承Mono的单例 实现 使用 注&#xff1a; 使用需要继承BaseManager 泛型填写自己本身 需要实现无参构造函数 2、挂载式的Mono单例 实现 使用 注&#xff1a; 使用需要继承SingletonMono 泛型填写自己本身 需要挂载在unity引擎面板 3、不用挂载式的单例 实现 使…...

Spring XML配置方式和Spring Boot注解方式的详细对照关系

功能/配置项Spring XML配置方式Spring Boot注解方式定义Beanxml <bean id"myBean" class"com.example.MyBean"/>javaBeanpublic MyBean myBean() { return new MyBean(); }注入Beanxml <bean id"myBean" class"com.example.MyBea…...

leetcode 无重复字符串的排列和组合

1.题目要求: 2.题目代码&#xff1a; class Solution { public:vector<string> result;string str;//利用回溯去解决此问题void backtricking(string S,vector<bool>& used){if(str.size() S.size()){result.push_back(str);}//用used去掉重复的排列与组合f…...

7. 配置

三种获取配置的方法 返回 /config/config.php 、/config/autoload/xxx.php 中的值 <?php namespace App\Controller;use Hyperf\Config\Annotation\Value; use Hyperf\Contract\ConfigInterface; use Hyperf\Di\Annotation\Inject; use Hyperf\HttpServer\Annotation\AutoC…...