Jetpack Compose Runtime 与 NodeTree 管理
Posted fundroid_方卓
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Jetpack Compose Runtime 与 NodeTree 管理相关的知识,希望对你有一定的参考价值。
Jetpack Compose 不只是一个 UI 框架,更是一个通用的 NodeTree 管理引擎。本文介绍
compose.runtime
如何通过NodeTree
为compose.ui
提供支持。
大家知道 Jetpack Compose 不仅限于在 android 中使用 ,Compose For Desktop、 Compose For Web 等项目也已相继发布,未来也许还会出现 Compose For ios 。Compose 能够在不同平台上实现相似的声明式UI开发体验,这得益于其分层的设计。
Compose 在代码上自下而上依次分为6层:
Modules | Description |
---|---|
compose.compiler | 基于 Kotlin compiler plugin 对 @Composable 进行编译期代码生成和优化 |
compose.runtime | 提供 NodeTree管理、State管理等,声明式UI的基础运行时 |
compose.ui | Android设备相关的基础UI能力,例如 layout、measure、drawing、input 等 |
compose.foundation | 通用的UI组件,包括 Column、Row 等容器、以及各种 Shape 等 |
compose.animation | 负责动画的实现、提升用户体验 |
compose.material | 提供符合 Material Design 标准的UI组件 |
其中 compose.runtime
和 compose.compiler
最为核心,它们是支撑声明式UI的基础。
Jake Wharton 在他的博客提到 :
What this means is that Compose is, at its core, a general-purpose tool for managing a tree of nodes of any type. Well a “tree of nodes” describes just about anything, and as a result Compose can target just about anything.
– https://jakewharton.com/a-jetpack-compose-by-any-other-name/
compose.runtime
提供了 NodeTree 管理等基础能力,此部分与平台无关,在此基础上各平台只需实现UI的渲染就是一套完整的声明式UI框架。而 compose.compiler
通过编译期的优化,帮助开发者书写更简单的代码调用 runtime 的能力。
从 Composable 到 NodeTree
“Compose 也好,React、Flutter 也好,其代码本质上都是对一颗树形结构的描述。”
所谓“数据启动UI",就是当state变化时,重建这颗树型结构并基于这棵NodeTree刷新UI。 当然,处于性能考虑,当 NodeTree 需要重建时,各框架会使用 VirtualDom 、GapBuffer(或称SlotTable) 等不同技术对其进行“差量”更新,避免“全量”重建。compose.runtime
的重要工作之一就是负责 NodeTree 的创建与更新。
如上,React 基于 VDOM “差量” 更新右侧的DOM树。
Compose 中的 NodeTree
对于 OOP 语言,我们通常使用如下方式描述一颗树:
fun TodoApp(items: List<TodoItem>): Node {
return Stack(Orientation.Vertical).apply {
for (item in items) {
children.add(Stack(Orientation.Horizontal).apply {
children.add(Text(if (item.completed) "x" else " "))
children.add(Text(item.title))
})
}
}
}
TodoApp
返回 Node
对象,可以被父 Node 继续 add,循环往复构成一棵完整的树。
但是 OOP 的写法模板代码多,不够简洁,且缺乏安全性。返回值 Node 成为句柄被随意引用甚至修改,这破坏了声明式UI中 “不可变性” 的原则,如果 UI 可以随意修改,diff 算法的准确性将无法保证。
因此,为了保证 UI 的不可变性,我们设法抹去返回值 Node:
fun Composer.TodoApp(items: List<TodoItem>) {
Stack(Orientation.Vertical) {
for (item in items) {
Stack(Orientation.Horizontal) {
Text(if (item.completed) "x" else " ")
Text(item.title)
}
}
}
}
fun Composer.Stack(orientation:Int, content: Composer.() -> Unit) {
emit(StackNode(orientation)) {
content()
}
}
fun Composer.Text() {
...
}
通过 Composer
提供的上下文, 将创建的 Node emit
到树上的合适位置。
interface Composer {
// add node as a child to the current Node, execute
// `content` with `node` as the current Node
fun emit(node: Node, content: () -> Unit = {})
}
Composer.Stack()
作为一个无返回值的函数,使得 NodeTree 的构建从 OOP 方式变为了 FP(函数式编程) 方式。
Compose Compiler 的加持
compose.compiler
的意义是让 FP 的写法进一步简单,添加一个 @Composable
注解, TodoApp 不必定义成 Composer
的扩展函数, 但是在编译期会修改 TodoApp 的签名,添加 Composer 参数。
@Composable
fun TodoApp {
Stack {
for (item in items) {
Stack(Orientation.Horizontal){
Text(if (item.completed) "x" else " ")
Text(item.title))
})
}
}
}
在 Compiler 的加持下,我们可以使用 @Composable 高效地写代码。 抛开语言上的差异不讲,Compose 比 Flutter 写起来要舒服得多。但无论写法上有多少差别,其归根结度还是会转换为对 NodeTree 的操作
NodeTree操作:Applier、ComposeNode、Composition
Compose 的 NodeTree 管理涉及 Applier
、Composition
和 Compose Nodes
的工作:
Composition 作为起点,发起首次的 composition,通过 Composalbe 的执行,填充 Slot Table,并基于 Table 创建 NodeTree。渲染引擎基于 Compose Nodes 渲染 UI, 每当 recomposition 发生时,都会通过 Applier 对 NodeTree 进行更新。 因此
“Composable 的执行过程就是创建 Node 并构建 NodeTree 的过程。 ”
Applier:变更 NodeTree 的节点
前文提到,出于性能考虑,NodeTree 会使用 “差量” 方式自我更新,而这正是基于 Applier
实现的。 Applier 使用 Visitor 模式遍历树上的 Node ,每种 NodeTree 的运算都需要配套一个 Applier
。
Applier
提供回调,基于回调我们可以对 NodeTree 进行自定义修改:
interface Applier<N> {
val current: N // 当前处理的节点
fun onBeginChanges() {}
fun onEndChanges() {}
fun down(node: N)
fun up()
fun insertTopDown(index: Int, instance: N) // 添加节点(自顶向下)
fun insertBottomUp(index: Int, instance: N)// 添加节点(自底向上)
fun remove(index: Int, count: Int) //删除节点
fun move(from: Int, to: Int, count: Int) // 移动节点
fun clear()
}
insertTopDown
与 insertBottomUp
都用来添加节点,针对不同的树形结构选择不同的添加顺序有助于提高性能。
参考: insertTopDown
insertTopDown(自顶向下) | insertBottomUp(自底向上) |
---|---|
我们可以实现自定义的 NodeApplier
, 如下:
class Node {
val children = mutableListOf<Node>()
}
class NodeApplier(node: Node) : AbstractApplier<Node>(node) {
override fun onClear() {}
override fun insertBottomUp(index: Int, instance: Node) {}
override fun insertTopDown(index: Int, instance: Node) {
current.children.add(index, instance) // `current` is set to the `Node` that we want to modify.
}
override fun move(from: Int, to: Int, count: Int) {
current.children.move(from, to, count)
}
override fun remove(index: Int, count: Int) {
current.children.remove(index, count)
}
}
Applier
需要在 composition/recomposition 过程中被调用。composition 是通过 Composition
中对 Root Composable 的调用发起的,进而调用全部 Composalbe 最终形成NodeTree。
Composition:Composalbe 执行的起点
fun Composition(applier: Applier<*>, parent: CompositionContext)
创建 Composition对象,参数传入 Applier
和 Recomposer
val composition = Composition(
applier = NodeApplier(node = Node()),
parent = Recomposer(Dispatchers.Main)
)
composition.setContent {
// Composable function calls
}
Recomposer
非常重要,他负责 Compose 的 recomposiiton 。当 NodeTree 首次创建之后,与 state 建立关联,监听 state 的变化发生重组。这个关联的建立是通过 Recomposer 的 “快照系统” 完成的。重组后,Recomposer 通过调用 Applier 完成 NodeTree 的变更 。
关于 Recomposer
的机制可以参考这篇文章:https://compose.net.cn/principle/recompose_working_principle/
Composition#setContent
为后续 Compodable 的调用提供了容器:
interface Composition {
val hasInvalidations: Boolean
val isDisposed: Boolean
fun dispose()
fun setContent(content: @Composable () -> Unit)
}
ComposeNode:创建 UiNode 并进行更新
理论上每个 Composable 的执行都对应一个 Node 的创建, 但是由于 NodeTree 无需全量重建,所以也不是每次都需要创建新 Node。大多的 Composalbe 都会调用 ComposeNode()
接受一个 factory,仅在必要的时候创建 Node。
以 Layout
的实现为例,
@Composable inline fun Layout(
content: @Composable () -> Unit,
modifier: Modifier = Modifier,
measurePolicy: MeasurePolicy
) {
val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current
ComposeNode<ComposeUiNode, Applier<Any>>(
factory = ComposeUiNode.Constructor,
update = {
set(measurePolicy, ComposeUiNode.SetMeasurePolicy)
set(density, ComposeUiNode.SetDensity)
set(layoutDirection, ComposeUiNode.SetLayoutDirection)
},
skippableUpdate = materializerOf(modifier),
content = content
)
}
- factory:创建 Node 的工厂
- update:接受 receiver 为
Updater<T>
的 lambda,用来更新当前 Node 的属性 - content:调用子 Composable
ComposeNode()
的实现非常简单:
inline fun <T, reified E : Applier<*>> ComposeNode(
noinline factory: () -> T,
update: @DisallowComposableCalls Updater<T>.() -> Unit,
noinline skippableUpdate: @Composable SkippableUpdater<T>.() -> Unit,
content: @Composable () -> Unit
) {
if (currentComposer.applier !is E) invalidApplier()
currentComposer.startNode()
if (currentComposer.inserting) {
currentComposer.createNode(factory)
} else {
currentComposer.useNode()
}
Updater<T>(currentComposer).update()
SkippableUpdater<T>(currentComposer).skippableUpdate()
currentComposer.startReplaceableGroup(0x7ab4aae9)//在编译期决定真正的GroupId
content()
currentComposer.endReplaceableGroup()
currentComposer.endNode()
}
在 composition 过程中,通过 Composer上下文,更新 SlotTable, content()
递归创建子 Node
SlotTable 在更新过程中,通过 diff 决定是否需要对 Node 进行 add/update/remove 等操作。 此处的 startNode
,useNode
,endNode
等就是对 SlotTable 的遍历过程。
有关 SlotTable(GapBuffer) 的介绍,可以参考文章:《under-the-hood-of-jetpack-compose-part-2-of-2》
SlotTable 的 diff 结果通过 Applier
的回调处理 NodeTree 结构的变化;通过调用 Updater<T>.update()
来处理 Node 属性的变化
Jake Wharton 的实验项目 Mosica
基于 compose.runtime
可以实现任意一套声明式UI框架。 J神有一个实验性的项目 Mosica,就很好地展示了这一点 :https://github.com/JakeWharton/mosaic
fun main() = runMosaic {
var count by mutableStateOf(0)
setContent {
Text("The count is: $count")
}
for (i in 1..20) {
delay(250)
count = i
}
}
上面是 Mosica 中的一个 Counter 的例子。
Mosica Composition
runMosaic()
创建 Composition、Recomposer 和 Applier
fun runMosaic(body: suspend MosaicScope.() -> Unit) = runBlocking {
//...
val job = Job(coroutineContext[Job])
val composeContext = coroutineContext + clock + job
val rootNode = BoxNode() //根节点Node
val recomposer = Recomposer(composeContext) //Recomposer
val composition = Composition(MosaicNodeApplier(rootNode), recomposer) //Composition
coroutineScope {
val scope = object : MosaicScope, CoroutineScope by this {
override fun setContent(content: @Composable () -> Unit) {
composition.setContent(content)//调用@Composable
hasFrameWaiters = true
}
}
//...
val snapshotObserverHandle = Snapshot.registerGlobalWriteObserver(observer)
try {
scope.body()//CoroutineScope中执行setContent{}
} finally {
snapshotObserverHandle.dispose()
}
}
}
而后,在 Composition 的 setContent{}
中,调用 @Composable。
Mosaic Node
看一下 Mosaic 中的 @Composalbe 和其对应的 Node
@Composable
private fun Box(flexDirection: YogaFlexDirection, children: @Composable () -> Unit) {
ComposeNode<BoxNode, MosaicNodeApplier>(
factory = ::BoxNode,
update = {
set(flexDirection) {
yoga.flexDirection = flexDirection
}
},
content = children,
)
}
@Composable
fun Text(
value: String,
color: Color? = null,
background: Color? = null,
style: TextStyle? = null,
) {
ComposeNode<TextNode, MosaicNodeApplier>(::TextNode) {
set(value) {
this.value = value
}
set(color) {
this.foreground = color
}
set(background) {
this.background = background
}
set(style) {
this.style = style
}
}
}
ComposeNode 通过泛型关联对应的 Node 和 Applier 类型
Box 和 Text 内部都使用 ComposeNode()
创建对应的 Node 对象。其中 Box 是容器类的 Composalbe,在 conent 中进一步创建子 Node。 Box 和 Text 在Updater<T>.update()
中更新 Node 属性 。
看一下 BoxNode
:
internal class BoxNode : MosaicNode() {
val children = mutableListOf<MosaicNode>()
override fun renderTo(canvas: TextCanvas) {
for (child in children) {
val childYoga = child.yoga
val left = childYoga.layoutX.toInt()
val top = childYoga.layoutY.toInt()
val right = left + childYoga.layoutWidth.toInt() - 1
val bottom = top + childYoga.layoutHeight.toInt() - 1
child.renderTo(canvas[top..bottom, left..right])
}
}
override fun toString() = children.joinToString(prefix = "Box(", postfix = ")")
}
internal sealed class MosaicNode {
val yoga: YogaNode = YogaNodeFactory.create()
abstract fun renderTo(canvas: TextCanvas)
fun render(): String {
val canvas = with(yoga) {
calculateLayout(UNDEFINED, UNDEFINED)
TextSurface(layoutWidth.toInt(), layoutHeight.toInt())
}
renderTo(canvas)
return canvas.toString()
}
}
BoxNode
继承自 MosaicNode
, MosaicNode 在 render()
中,通过 yoga
实现UI的绘制。通过调用 renderTo()
在 Canvas中 递归绘制子 Node,类似 AndroidView 的绘制逻辑。
理论上需要在首次 composition 或者 recomposition 时,调用 Node 的 render()
进行 NodeTree 的绘制, 为简单起见,Mosica 只是使用了定时轮询的方式调用 render()
launch(context = composeContext) {
while (true) {
if (hasFrameWaiters) {
hasFrameWaiters = false
output.display(rootNode.render())
}
delay(50)
}
}
//counter的state变化后,重新setContent,hasFrameWaiters更新后,重新render
coroutineScope {
val scope = object : MosaicScope, CoroutineScope by this {
override fun setContent(content: @Composable () -> Unit) {
composition.setContent(content)
hasFrameWaiters = true
}
}
}
MosaicNodeApplier
最后看一下 MosaicNodeApplier
:
internal class MosaicNodeApplier(root: BoxNode) : AbstractApplier<MosaicNode>(root) {
override fun insertTopDown(index: Int, instance: MosaicNode) {
// Ignored, we insert bottom-up.
}
override fun insertBottomUp(index: Int, instance: MosaicNode) {
val boxNode = current as BoxNode
boxNode.children.add(index, instance)
boxNode.yoga.addChildAt(instance.yoga, index)
}
override fun remove(index: Int, count: Int) {
val boxNode = current as BoxNode
boxNode.children.remove(index, count)
repeat(count) {
boxNode.yoga.removeChildAt(index)
}
}
override fun move(from: Int, to: Int, count: Int) {
val boxNode = current as BoxNode
boxNode.children.move(from, to, count)
val yoga = boxNode.yoga
val newIndex = if (to > from) to - count else to
if (count == 1) {
val node = yoga.removeChildAt(from)
yoga.addChildAt(node, newIndex)
} else {
val nodes = Array(count) {
yoga.removeChildAt(from)
}
nodes.forEachIndexed { offset, node ->
yoga.addChildAt(node, newIndex Jetpack Compose 深入探索系列六:Compose runtime 高级用例
自学Jetpack Compose 系列Compose控件Text与TextStyle的学习与使用
Android Jetpack Compose学习—— Jetpack compose入门