Android compose 重建流程1

news2024/10/24 20:27:22

前言

本文是笔者学习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){
        	//LinearLayout
            OneColumnComposable(Composer, rootView) { view ->
            	//Texview 1 read and show displayOneState
                OneTvComposable(Composer, view, displayOneState)
                //Textview 2 => read and show displayTwoState
                TwoTvComposable(Composer, view, displayTwoState)
                //Button => modify displayOneState
                OneBtnComposable(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)
    
    //创建一个 LinearLayout
    MyColumn("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 就是我们的 state
        val 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_INIT

    companion 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 写入需要重新刷新的 group
    val 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 ?: rootNode
            val group = dirtyGroup ?: parentGroup.child[composableId] ?: Group(
                composableId,
                scopeStack.peek()?.group,
                mutableScatterMapOf()
            )
            parentGroup.child[composableId] = group
            group
        } else {
            dirtyGroup
        }
        //构造 scope 栈对象,方便感知刷新域
        val updateScope = UpdateScope(composableId, group, null)
        scopeStack.push(updateScope)
        return updateScope
    }
    
    //弹出栈,并重新标记 group 为干净
    fun endScope(update: (() -> Unit)) {
        this.update = update
        Composer.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_DECAY
            updateFrame(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_INIT

    companion 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()
    }
}

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 = update
        Composer.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_NODE

    init {
        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_DECAY
                    updateFrame(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 ?: rootNode
            val group = dirtyGroup ?: parentGroup.child[composableId] ?: Group(
                composableId,
                scopeStack.peek()?.group,
                mutableScatterMapOf()
            )
            parentGroup.child[composableId] = group
            group
        } 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 = viewId
            val 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 = viewId
            parent.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 = viewId
            parent.addView(btn)
            btn
        } else {
            oldBtn
        }
        btn.text = content
        btn.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 += 1
                state.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.value
    Text("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 method
            public 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.value
    Text("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 method
            public Object invoke(Object p1, Object p2) {
               this.invoke((Composer)p1, ((Number)p2).intValue());
               return Unit.INSTANCE;
            }
         }));
      }

   }

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

//Composer.kt

class ComposerImpl(
    @ComposeCompilerApi
    override 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)
            //...略
    }

    @ComposeCompilerApi
    override fun endRestartGroup(): ScopeUpdateScope? {
        //...略
           //弹出栈
        val scope = if (invalidateStack.isNotEmpty()) invalidateStack.pop()
        //...略

    }

}

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

 //ReComposer.kt
 class 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?
//查阅栈顶 scope
    get() = invalidateStack.let {
        if (childrenComposing == 0 && it.isNotEmpty()) it.peek() else null
    }

封装的ScopeMap如下:

package androidx.compose.runtime.collection

internal class ScopeMap<T : Any> {
    val map = mutableScatterMapOf<Any, Any>()

    val size get() = map.size
    //内部会构建 Set 集合放入多个 value 去对应一个 key
    fun add(key: Any, scope: T) {
        map.compute(key) { _, value ->
            when (value) {
                null -> scope
                is 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设置为 null
        workContinuation.also {
            workContinuation = null
        }
        } else null
}

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

//Composer.kt
suspend fun runRecomposeAndApplyChanges() = recompositionRunner { parentFrameClock ->

    //..略
    //为简化流程shouldKeepRecomposing可以视为永远为 true
    while (shouldKeepRecomposing) {
        //判断当前是否dirty 的 scope,如果没有那么将当前协程挂起,并将continuation 赋值给workContinuation
        //可以简单判断snapshotInvalidations为空就执行挂起
        awaitWorkAvailable()
       
        //等候下一个 VSYNC 回调执行实际重组.
        parentFrameClock.withFrameNanos { frameTime ->
            //这里会取出 dirty 的scope 开始进行重组工作
            //...略
            //toRecompose是一个CompositionImpl集合.
            //Main
            while (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.kt

private val hasSchedulingWork: Boolean
    get() = synchronized(stateLock) {
        //是否有 dirty 的 scope
        snapshotInvalidations.isNotEmpty() ||
            compositionInvalidations.isNotEmpty() ||
            hasBroadcastFrameClockAwaitersLocked
    }

private suspend fun awaitWorkAvailable() {
    if (!hasSchedulingWork) {
        suspendCancellableCoroutine<Unit> { co ->
            synchronized(stateLock) {
            //如果有 dirty 的数据那么直接恢复协程完成重组
                if (hasSchedulingWork) {
                    co
                } else {
                //挂起协程
                    workContinuation = co
                    null
                }
            }?.resume(Unit)
        }
    }
}

参考

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

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

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

相关文章

实习冲刺Day2

算法题 反转链表 206. 反转链表 - 力扣&#xff08;LeetCode&#xff09; /*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNode() : val(0), next(nullptr) {}* ListNode(int x) : val(x), next(nullptr) {}*…

AI大模型应用(3)开源框架Vanna: 利用RAG方法做Text2SQL任务

AI大模型应用(3)开源框架Vanna: 利用RAG方法做Text2SQL任务 RAG&#xff08;Retrieval-Augmented Generation&#xff0c;如下图所示&#xff09;检索增强生成&#xff0c;即大模型LLM在回答问题时&#xff0c;会先从大量的文档中检索出相关信息&#xff0c;然后基于这些检索出…

【LLaMA-Facrory】【模型评估】:代码能力评估——Qwen-Coder-7B 和 deepseek-coder-7b-base-v1.5

目录 序言 1 拉取 Qwen2.5-Coder-7B 模型 2 编写python测试模型 3 启动webui导入模型测试 4 模型评估 4.1 前期准备工作 4.2 Qwen2.5-Coder-7B 模型评估 数据说明 综合分析 4.3 deepseek-coder-7b-base-v1.5 模型评估 数据说明 综合分析 4.4 模型比较 1. 文本生成…

软件安全测试报告如何编写?CMA、CNAS软件安全测试机构推荐

随着软件产品的增多&#xff0c;产品安全成为软件企业留住用户的方法之一。安全测试是验证和检查软件安全的重要手段&#xff0c;而软件安全测试报告则是测试人员工作成果的最好体现&#xff0c;那么软件安全测试报告该如何编写呢?权威的CMA、CNAS软件安全测试机构又有哪些? …

WeMapEngine开发实战,创建你的第一个GIS项目

我们在《WeMapEngine可快速构建的GIS应用功能》一文中为你分享了WeMapEngine可快速建的GIS应用功能。 今天再为你分享基于WeMapEngine的开发实战&#xff0c;演示如何快速创建第一个GIS项目。 创建你的第一个GIS项目 现在&#xff0c;我们开始构建第一个项目。 在这个项目中…

emulator -version报错解决方案

使用android studio安装安卓环境之后&#xff0c;会发现emulator -version报如下错 [14960]:ERROR:android/android-emu/android/qt/qt_setup.cpp:28:Qt library not found at ..\emulator\lib64\qt\lib Could not launch C:\Users\litbai\..\emulator\qemu\windows-x86_64\qe…

【Java小白图文教程】-04-循环结构

精品专题&#xff1a; 01.《C语言从不挂科到高绩点》课程详细笔记 https://blog.csdn.net/yueyehuguang/category_12753294.html?spm1001.2014.3001.5482 02. 《SpringBoot详细教程》课程详细笔记 https://blog.csdn.net/yueyehuguang/category_12789841.html?spm1001.20…

深入理解计算机系统--计算机系统漫游

对于一段最基础代码的文件hello.c&#xff0c;解释程序的运行 #include <stdio.h>int main() {printf ( "Hello, world\n") ;return 0; }1.1、信息就是位上下文 源程序是由值 0 和 1 组成的位&#xff08;比特&#xff09;序列&#xff0c;8 个位被组织成一组…

springboot048校园资料分享平台(论文+源码)_kaic

校园资料分享平台 摘要 随着信息互联网购物的飞速发展&#xff0c;国内放开了自媒体的政策&#xff0c;一般企业都开始开发属于自己内容分发平台的网站。本文介绍了校园资料分享平台的开发全过程。通过分析企业对于校园资料分享平台的需求&#xff0c;创建了一个计算机管理校园…

无人机的电池放电详解!

一、定义与表示方法 无人机的电池放电率是指电池在一定时间内放出其储存电能的能力&#xff0c;这一参数通常用C数来表示。C数越大&#xff0c;表示放电速率越快。 例如&#xff0c;一个2C的电池可以在1/2小时内放完其全部电量&#xff0c;而一个10C的电池则可以在1/10小时内…

喜讯!望繁信科技荣膺2022年中国超自动化先锋企业TOP20

36氪重磅发布「2022中国超自动化先锋企业」调研结果。凭借多年在流程挖掘领域过硬的技术实力和突出的产品创新力&#xff0c;望繁信科技在众多调研样本企业中脱颖而出&#xff0c;赢得了专家评委的充分认可&#xff0c;成功入选2022年中国超自动化先锋企业TOP20。 什么是超自动…

【书生大模型实战营】闯关任务1-入关岛

这里写自定义目录标题 第一关 L0G1000 Linux 基础知识 第一关 L0G1000 Linux 基础知识 SSH连接与端口映射并运行hello_world.py 关键截图&#xff1a;

502 错误码通常出现在什么场景?

服务器过载场景 高流量访问&#xff1a;当网站遇到突发的高流量情况&#xff0c;如热门产品促销活动、新闻热点事件导致网站访问量激增时&#xff0c;服务器可能会因承受过多请求而无法及时响应。例如&#xff0c;电商平台在 “双十一” 等购物节期间&#xff0c;大量用户同时…

Windows解决localhost拒绝了连接请求

最近&#xff0c;在开发前端Vue项目时&#xff0c;Vue项目启动成功&#xff0c;没有任何报错&#xff0c;服务控制台已出现APP访问地址&#xff0c;如下图所示。 览器打开后页面先是空白&#xff0c;然后过了一会儿显示无法访问此网站&#xff0c;localhost拒绝了我们的连接请…

WPF MVVM模式实现DataGrid编辑

本文是一个MVVM模式开发的基础教程&#xff0c;完全手写实现&#xff0c;未借助三方框架&#xff0c;适用于初学者 要实现DataGrid的编辑&#xff0c;步骤如下&#xff1a; 1、创建两个窗口&#xff0c;第一个窗口用于显示DataGrid&#xff0c; 布局如下&#xff1a; 这个界…

Data+AI下的云数仓未来已来!

DataAI下的云数仓未来已来&#xff01; 前言云数仓是什么&#xff1f;云数仓的概念与背景数据孤岛问题与云数仓的优势 现代数仓如何建设&#xff1f;灵活架构与弹性扩展实时与离线处理并存安全与合规性&#xff1a;现代数仓的基石 AI如何助力数仓建设&#xff1f;AI驱动的数据处…

《探索 NESMA:软件度量领域的璀璨之星》

《探索 NESMA&#xff1a;软件度量领域的璀璨之星》 一、NESMA 初印象 NESMA&#xff08;Netherland Software Measurement Association&#xff09;&#xff0c;作为荷兰软件度量协会的简称&#xff0c;在软件规模度量领域占据着重要地位。它是五种 ISO 国际功能点标准之一&am…

C++学习路线(二十二)

构造函数 构造函数作用 在创建一个新的对象时&#xff0c;自动调用的函数&#xff0c;用来进行“初始化”工作:对这个对象内部的数据成员进行初始化。 构造函数特点 1.自动调用(在创建新对象时&#xff0c;自动调用) 2.构造函数的函数名&#xff0c;和类名相同 3.构造函数…

visual studio设置修改文件字符集方法

该方法来自网文&#xff0c;特此记录备忘。 添加两个组件&#xff0c;分别是Force UTF-8,FileEncoding。 截图如下&#xff1a; 方法如下&#xff1a;vs中点击“扩展”->“管理扩展”&#xff0c;输入utf搜索&#xff0c;安装如下两个插件&#xff0c;然后重启vs&#xf…

Cursor零基础小白教程系列 - 创建你的第一个Cursor 项目

最适合小白零基础的Cursor教程 网站lookai.top相同作者&#xff0c;最新文章会在网站更新&#xff0c;欢迎收藏书签 创建你的第一个Cursor 项目 实操视频 概述 开始使用Cursor进行编程的第一步是创建或导入一个项目。本指南将帮助您了解如何在Cursor中创建新项目、导入现有项…