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