目录

  • jetpack compose原理解析
    • jetpack compse
    • 声明式ui开发
    • 原理分析
      • 整体框架介绍
        • compose LayoutNode布局介绍
        • @Composeable注解实现细节
        • 属性更新
    • 小结

jetpack compose原理解析

jetpack compse

Jetpack Compose是Google在2019 I/O大会上公布开源的一个非捆绑工具包。Jetpack Compose是用于构建原生Android UI的现代工具包。 Jetpack Compose使用更少的代码,强大的工具和直观的Kotlin API,简化并加速了Android上的UI开发。最为重要的是jetpack compose基于响应式架构构建,完美支持响应式开发。不过目前仅有预览版,正式版还没确定,本文也是基于当前预览版对其原理进行简单分析。

声明式ui开发

在我们了解其原理之前,我们需要明白一个概念–声明式ui开发。何为声明式ui,与之相对应的还有一个命令式ui开发,目前我们安卓ui开发的大多数模式即为命令式ui开发,即我们创建了一个widget之后,需要重新获取这个widget实例,然后通过调用相关函数(即命令)改变其属性,比如以下我们常见的代码

var times = 0
val button:Button = findViewById(R.id.button)
val textView:TextView = findViewById(R.id.text)
button.setOnClickListener{++timestextView.text="click times:${times}"}

上诉代码非常简单,实现功能也非常清楚,即一个textview用来展示button的点击次数,我们可以发现每次改变textview的文本,我们需要获取textView的实例然后调用setText这个命令去改变其文本,而声明式ui则不同,通常他会有一个状态(如flutter的widget)用来描述当前界面(状态通常是不可变的,每次变化均会产生新的实例),然后我们只需要根据其状态声明下当前界面,比如使用flutter来实现上诉功能的代码如下

class MyHomePage extends StatefulWidget {MyHomePage({Key key, this.title}) : super(key: key);final String title;@override_MyHomePageState createState() => _MyHomePageState();
}class _MyHomePageState extends State<MyHomePage> {int _counter = 0;void _incrementCounter() {setState(() {_counter++;});}@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text(widget.title),),body: Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,children: <Widget>[Text('click times:',),Container(width: 20,height: 20,child: Text('$_counter',style: Theme.of(context).textTheme.headline4,)),],),),floatingActionButton: FloatingActionButton(onPressed: _incrementCounter,tooltip: 'Increment',child: Icon(Icons.add),),);}
}

(这里为什么使用flutter代码来展示,因为我觉得jetpack compose通过注解隐藏了太多实现细节,初学者可能不太好理解,而flutter则不同,从直观上更好理解)
可以看到每次状态改变,我们都是通过build函数重新声明了界面布局,然后重新展示即可(这里StatefulWidget和State感兴趣的同学可以去了解下flutter开发,不属于本文所要讲解的范围,只需要通过该例子了解声明式开发的特点即可,可以简单的理解下每次button click的setState都会触发State.build函数重新声明布局)。这就是声明式ui和命令式ui的不同之处,从中我们也可以很快的发现声明式ui的好处,可以让我们开发更聚焦于状态的变化以及逻辑的实现,而不是在界面上,我们只需要根据当前呢的状态声明对应的ui布局即可,而且我们可以根据状态方便的进行界面重建等工作,这对于移动式开发非常友好。不过我们也可以发现其缺点,即每次重建带来的性能损耗,即使所有声明式框架都会有对应的算法在底层对组件(这里指的是渲染以及布局相关的组件,不是flutter的widget,对应于flutter即是element和renderobject)进行最大程度的复用(如react的virutal dom ,flutter的element diff 以及jetpack compose的gap buffert等),不过即使如此,在性能上对比命令式ui还是有一定损耗(不过目前来说,性能已经不是界面开发的首要考虑因素)。
接下来我们就来探索下安卓未来的ui构建方式jetpack compose的底层实现原理,不过为了更好的了解本文,大家可以先去阅读下面这篇文章
jetpack compose思想介绍
这篇文章从实现原理的层面讲解了jetpack compose,我也是基于这篇文章以及源码来去探究jetpack compose的。同时大家也会发现jetpack compose和flutter一些api的相似之处,本篇文章我也会对两者进行对比来分析下两者实现的异同

原理分析

jetpack compose从名字就可以看出,该框架基于组合优于继承的理念构建,这也是函数式响应式编程所提倡的。一开始接触jetpack compose,我第一直觉是基于安卓现有的view体系架构进行封装,尽可能的复用当前组件,但是了解后发现其实不然,jetpack compose抛弃了原有安卓view的体系,完全重新实现了一套新的ui体系(目前jetpack compose不过也提供了使用兼容原有view的方法),对此谷歌给出的解释是原有的view体系过于庞大,并且理念过于陈旧,不如借当前机会不破不立,完全基于新的理念来重新实现一套现代的ui体系。

整体框架介绍

我们大家清楚,ui最终可以使用树的形式来描述,compose也是一样,他最终是LayoutNode的一棵树,不过为了实现声明式ui的特点我们需要将配置和绘制进行分离,同时还需要在适当的时机进行node复用,所以compose借助于注解用来实现相关细节,对于compose来说整体的ui框架大致是这样的

1.@Composable 注解的函数 这里是ui的声明和配置,这也是直接面向开发者的
2.基于注解和Composer 的具体细节实现 这里实现了LayoutNode的缓存以及复用,同时也实现了对于属性变化的监听
3.LayoutNode主要用于布局和绘制

熟悉flutter开发的同学都知道,flutter中有重要的三棵树 widget 、element和renderobject。

1.widget是对ui的描述
2.element是对组件的复用
3.renderobject用于布局和绘制

我们将两者进行对比,其实发现两者有很多的相似之处,虽然实现的方式天差地别,但是其思想却是相通的(不过我个人觉得flutter的实现方式更容易理解点,compose隐藏了太多细节和运用了太多高级封装,在进行原理分析上难度可能比较大)

compose LayoutNode布局介绍

其实flutter和compose两者不仅仅思想上比较类似,在布局的实现上两者也都差不多,compose采用了和flutter一样的布局方式即盒约束(盒约束是指widget可以按照指定限制条件来决定自身如何占用布局空间,所谓的“盒”即指自身的渲染框。有关于盒约束介绍,可以查看这篇文章flutter盒约束),这个我们可以在LayoutNode的源码中看到

这里即用来计算布局空间约束,同样在布局过程中传递的参数Constraints 可以看出
这里和flutter的盒约束类似,了解flutter的同学应该知道,flutter布局过程中有一个relayout boundary(重布局边界约束)这个优化条件用来加快布局,即我们的view在重布局过程中如果遇到重布局边界,将不会继续向上传递布局请求,因为这个view无论怎么变化,将不会影响父view的布局,所以父view将不需要重新布局,一开始基于两者布局的相似性我也认为compose也会采用相关优化方法,不过继续追踪代码发现并没有,这里不清楚为什么,或者后续正式版有可能会加上这个优化,这部分代码在MeasureAndLayoutDelegate 的requestRelayout中可以看到

  /*** Requests remeasure for this [layoutNode] and nodes affected by its measure result.** @return returns true if the [measureAndLayout] execution should be scheduled as a result* of the request.*/fun requestRemeasure(layoutNode: LayoutNode): Boolean {return trace("AndroidOwner:onRequestMeasure") {layoutNode.requireOwner()if (layoutNode.isMeasuring) {// we're already measuring it, let's swallow. example when it happens: we compose// DataNode inside WithConstraints, this calls onRequestMeasure on DataNode's// parent, but this parent is WithConstraints which is currently measuring.return false}if (layoutNode.needsRemeasure) {// requestMeasure has already been called for this nodereturn false}if (layoutNode.isLayingOut) {// requestMeasure is currently laying out and it is incorrect to request remeasure// now, let's postpone it.layoutNode.markRemeasureRequested()postponedMeasureRequests.add(layoutNode)consistencyChecker?.assertConsistent()return false}// find root of layout request:var layout = layoutNodewhile (layout.affectsParentSize && layout.parent != null) {val parent = layout.parent!!if (parent.isMeasuring || parent.isLayingOut) {if (!layout.needsRemeasure) {layout.markRemeasureRequested()// parent is currently measuring and we set needsRemeasure to true so if// the parent didn't yet try to measure the node it will remeasure it.// if the parent didn't plan to measure during this pass then needsRemeasure// stay 'true' and we will manually call 'onRequestMeasure' for all// the not-measured nodes in 'postponedMeasureRequests'.postponedMeasureRequests.add(layout)}consistencyChecker?.assertConsistent()return false} else {layout.markRemeasureRequested()if (parent.needsRemeasure) {// don't need to do anything else since the parent is already scheduled// for a remeasuringconsistencyChecker?.assertConsistent()return false}layout = parent}}layout.markRemeasureRequested()requestRelayout(layout.parent ?: layout)}}

虽然在往上传递布局请求时候会有affectsParentSize判断,但是这个属性赋值代码如下

        // The more idiomatic, `if (parentLayoutNode?.isMeasuring == true)` causes boxingaffectsParentSize = parent != null && parent.isMeasuring == true

经过分析代码发现这个属性只是简单判断父亲有没有正在测量布局,并不是重布局边界,并且我定义了一个固定大小的Text当改变其属性时,依然会将测量请求传递至rootview验证了我的结论(这里我不是非常确定,只是基于我的代码和所看到的进行分析)

@Composeable注解实现细节

接下来我们再来看下@Composeable注解到底做了啥,这部分代码不好直接查看,因为他是基于koltin注解去动态生成的,我在studio中并没有直接找到生成的相关代码,我是采用这种方法去查看的,先编译出一个apk 然后将其中的classes.dex文件进行反编译成jar文件,再将jar文件引入任意一个安卓工程中,即可查看相关代码
我们先来看下Layout所对应的代码,这是compose布局的基础类,如Column都是基于它实现,它对应的原函数如下

/*** [Layout] is the main core component for layout. It can be used to measure and position* zero or more children.** Intrinsic measurement blocks define the intrinsic sizes of the current layout. These* can be queried by the parent in order to understand, in specific cases, what constraints* should the layout be measured with:* - [minIntrinsicWidthMeasureBlock] defines the minimum width this layout can take, given*   a specific height, such that the content of the layout will be painted correctly* - [minIntrinsicHeightMeasureBlock] defines the minimum height this layout can take, given*   a specific width, such that the content of the layout will be painted correctly* - [maxIntrinsicWidthMeasureBlock] defines the minimum width such that increasing it further*   will not decrease the minimum intrinsic height* - [maxIntrinsicHeightMeasureBlock] defines the minimum height such that increasing it further*   will not decrease the minimum intrinsic width** For a composable able to define its content according to the incoming constraints,* see [WithConstraints].** Example usage:* @sample androidx.ui.core.samples.LayoutWithProvidedIntrinsicsUsage** @param children The children composable to be laid out.* @param modifier Modifiers to be applied to the layout.* @param minIntrinsicWidthMeasureBlock The minimum intrinsic width of the layout.* @param minIntrinsicHeightMeasureBlock The minimum intrinsic height of the layout.* @param maxIntrinsicWidthMeasureBlock The maximum intrinsic width of the layout.* @param maxIntrinsicHeightMeasureBlock The maximum intrinsic height of the layout.* @param measureBlock The block defining the measurement and positioning of the layout.** @see Layout* @see WithConstraints*/
@Composable
/*inline*/ fun Layout(/*crossinline*/children: @Composable () -> Unit,/*crossinline*/minIntrinsicWidthMeasureBlock: IntrinsicMeasureBlock,/*crossinline*/minIntrinsicHeightMeasureBlock: IntrinsicMeasureBlock,/*crossinline*/maxIntrinsicWidthMeasureBlock: IntrinsicMeasureBlock,/*crossinline*/maxIntrinsicHeightMeasureBlock: IntrinsicMeasureBlock,modifier: Modifier = Modifier,/*crossinline*/measureBlock: MeasureBlock
) {val measureBlocks = object : LayoutNode.MeasureBlocks {override fun measure(measureScope: MeasureScope,measurables: List<Measurable>,constraints: Constraints,layoutDirection: LayoutDirection) = measureScope.measureBlock(measurables, constraints, layoutDirection)override fun minIntrinsicWidth(intrinsicMeasureScope: IntrinsicMeasureScope,measurables: List<IntrinsicMeasurable>,h: IntPx,layoutDirection: LayoutDirection) = intrinsicMeasureScope.minIntrinsicWidthMeasureBlock(measurables, h, layoutDirection)override fun minIntrinsicHeight(intrinsicMeasureScope: IntrinsicMeasureScope,measurables: List<IntrinsicMeasurable>,w: IntPx,layoutDirection: LayoutDirection) = intrinsicMeasureScope.minIntrinsicHeightMeasureBlock(measurables, w, layoutDirection)override fun maxIntrinsicWidth(intrinsicMeasureScope: IntrinsicMeasureScope,measurables: List<IntrinsicMeasurable>,h: IntPx,layoutDirection: LayoutDirection) = intrinsicMeasureScope.maxIntrinsicWidthMeasureBlock(measurables, h, layoutDirection)override fun maxIntrinsicHeight(intrinsicMeasureScope: IntrinsicMeasureScope,measurables: List<IntrinsicMeasurable>,w: IntPx,layoutDirection: LayoutDirection) = intrinsicMeasureScope.maxIntrinsicHeightMeasureBlock(measurables, w, layoutDirection)}Layout(children, measureBlocks, modifier)
}/*** [Layout] is the main core component for layout. It can be used to measure and position* zero or more children.** The intrinsic measurements of this layout will be calculated by running the measureBlock,* while swapping measure calls with appropriate intrinsic measurements. Note that these* provided implementations will not be accurate in all cases - when this happens, the other* overload of [Layout] should be used to provide correct measurements.** For a composable able to define its content according to the incoming constraints,* see [WithConstraints].** Example usage:* @sample androidx.ui.core.samples.LayoutUsage** @param children The children composable to be laid out.* @param modifier Modifiers to be applied to the layout.* @param measureBlock The block defining the measurement and positioning of the layout.** @see Layout* @see WithConstraints*/
@Composable
/*inline*/ fun Layout(/*crossinline*/children: @Composable () -> Unit,modifier: Modifier = Modifier,/*noinline*/measureBlock: MeasureBlock
) {val measureBlocks = remember(measureBlock) { MeasuringIntrinsicsMeasureBlocks(measureBlock) }Layout(children, measureBlocks, modifier)
}/*@PublishedApi*/ @Composable internal /*inline*/ fun Layout(/*crossinline*/children: @Composable () -> Unit,measureBlocks: LayoutNode.MeasureBlocks,modifier: Modifier
) {LayoutNode(modifier = currentComposer.materialize(modifier), measureBlocks = measureBlocks) {children()}
}

注解生成后的代码如下

public static final void Layout(final Function3 var0, final MeasureBlocks var1, final Modifier var2, Composer var3, int var4, final int var5) {Intrinsics.checkNotNullParameter(var0, "children");Intrinsics.checkNotNullParameter(var1, "measureBlocks");Intrinsics.checkNotNullParameter(var2, "modifier");var3.startRestartGroup(var4);Modifier var8 = ComposedModifierKt.materialize(var3, var2);UiComposer var7 = (UiComposer)var3;var7.startNode(1043845699);LayoutNode var6;if (var7.getInserting()) {var6 = new LayoutNode();var7.emitNode(var6);} else {var6 = (LayoutNode)var7.useNode();}ComposerUpdater var11 = new ComposerUpdater((Composer)var7, var6);Composer var9 = var11.getComposer();if (var9.getInserting() || !Intrinsics.areEqual(var9.nextSlot(), var8)) {var9.updateValue(var8);((LayoutNode)var11.getNode()).setModifier(var8);}Composer var12 = var11.getComposer();if (var12.getInserting() || !Intrinsics.areEqual(var12.nextSlot(), var1)) {var12.updateValue(var1);((LayoutNode)var11.getNode()).setMeasureBlocks(var1);}var0.invoke(var3, 495126159, var5 & 6);var7.endNode();ScopeUpdateScope var10 = var3.endRestartGroup();if (var10 != null) {var10.updateScope((Function3)(new Function3() {// $FF: synthetic method// $FF: bridge methodpublic Object invoke(Object var1x, Object var2x, Object var3) {this.invoke((Composer)var1x, ((Number)var2x).intValue(), ((Number)var3).intValue());return Unit.INSTANCE;}public final void invoke(Composer var1x, int var2x, int var3) {LayoutKt.Layout(var0, var1, var2, var1x, var2x, var5 | 1);}}));}}

我们可以看到注解帮我们自动处理了node的创建 复用 以及更新,这里面我们主要借助Composer类来对node进行管理,而@Composeable注解的函数最终会生成一个RestartableFunction函数,即上述代码的var0
关于 LayoutNode的复用,compose这里比较复杂,使用了一个叫gap buffer的方法来进行layoutnode的缓存和基于位置记忆来判断是否复用,这一块我目前了解的还不是非常透彻,不过大家可以先简单的认为是一个数组用来保存不同位置的信息,然后取出信息进行对比,这块相关的代码在SlotTable这个类以及相关类中,大家感兴趣的话可以了解下,后面如果我对这块有更深的了解会专门写一篇文章进行介绍。

属性更新

compose使用state来标记属性状态,state标记的属性只要发生变化即会自动通知界面更新,我们接下来来探索这部分的实现原理,先看以下代码

@Composable
fun Greeting(name: String) {var count by state { 0 }Column {Text(text = "click times:${count}")Button(onClick = {++count}) {Text("button")}}
}

其注解生成的代码如下

public static final void Greeting(final String var0, Composer var1, int var2, final int var3) {Intrinsics.checkParameterIsNotNull(var0, "name");var1.startRestartGroup(var2);if ((var3 & 6) == 0) {byte var7;if (var1.changed(var0)) {var7 = 4;} else {var7 = 2;}var2 = var7 | var3;} else {var2 = var3;}if ((var2 & 3 ^ 2) == 0 && var1.getSkipping()) {var1.skipToGroupEnd();} else {var1.startReplaceableGroup(-1469557643);Function2 var4;if (true & true) {var4 = MutableStateKt.getReferentiallyEqual();} else {var4 = null;}var1.startReplaceableGroup(701502689);Object var5 = var1.nextSlot();Object var8;if (var5 != SlotTable.Companion.getEMPTY()) {var8 = var5;} else {var8 = MutableStateKt.mutableStateOf(0, var4);var1.updateValue(var8);}var1.endReplaceableGroup();final MutableState var9 = (MutableState)var8;var1.endReplaceableGroup();ColumnKt.Column((Modifier)null, (Vertical)null, (Horizontal)null, (Function4)RestartableFunctionKt.restartableFunction(var1, -756387618, true, new Function4() {// $FF: synthetic method// $FF: bridge methodpublic Object invoke(Object var1, Object var2, Object var3, Object var4) {this.invoke((ColumnScope)var1, (Composer)var2, ((Number)var3).intValue(), ((Number)var4).intValue());return Unit.INSTANCE;}public final void invoke(ColumnScope var1, Composer var2, int var3, int var4) {Intrinsics.checkParameterIsNotNull(var1, "<this>");if (((var4 | 6) & 11 ^ 10) == 0 && var2.getSkipping()) {var2.skipToGroupEnd();} else {TextKt.Text-bHUNS4Y(Intrinsics.stringPlus("click times:", MainActivityKt.Greeting$lambda-1(var9)), (Modifier)null, Color.constructor-VKZWuLQ(ULong.constructor-impl(0L)), TextUnit.constructor-impl(0L), (FontStyle)null, (FontFamily)null, TextUnit.constructor-impl(0L), (TextDecoration)null, (TextAlign)null, TextUnit.constructor-impl(0L), (TextOverflow)null, false, 0, (Map)null, (Function1)null, (TextStyle)null, var2, 537342775, 0, 0, 65534);final MutableState var5 = var9;var2.startReplaceableGroup(537342836);Object var6 = var2.nextSlot();if (var6 == SlotTable.Companion.getEMPTY()) {var6 = new Function0() {// $FF: synthetic method// $FF: bridge methodpublic Object invoke() {this.invoke();return Unit.INSTANCE;}public final void invoke() {MutableState var1 = var5;MainActivityKt.Greeting$lambda-2(var1, MainActivityKt.Greeting$lambda-1(var1) + 1);MainActivityKt.Greeting$lambda-1(var5);}};var2.updateValue(var6);}var2.endReplaceableGroup();ButtonKt.Button-AidQf7c((Function0)var6, (Modifier)null, false, Dp.constructor-impl(0.0F), Dp.constructor-impl(0.0F), (Shape)null, (Border)null, Color.constructor-VKZWuLQ(ULong.constructor-impl(0L)), Color.constructor-VKZWuLQ(ULong.constructor-impl(0L)), Color.constructor-VKZWuLQ(ULong.constructor-impl(0L)), Color.constructor-VKZWuLQ(ULong.constructor-impl(0L)), (InnerPadding)null, (Function3)RestartableFunctionKt.restartableFunction(var2, -756387738, true, new Function3() {// $FF: synthetic method// $FF: bridge methodpublic Object invoke(Object var1, Object var2, Object var3) {this.invoke((Composer)var1, ((Number)var2).intValue(), ((Number)var3).intValue());return Unit.INSTANCE;}public final void invoke(Composer var1, int var2, int var3) {if ((var3 & 3 ^ 2) == 0 && var1.getSkipping()) {var1.skipToGroupEnd();} else {TextKt.Text-bHUNS4Y("button", (Modifier)null, Color.constructor-VKZWuLQ(ULong.constructor-impl(0L)), TextUnit.constructor-impl(0L), (FontStyle)null, (FontFamily)null, TextUnit.constructor-impl(0L), (TextDecoration)null, (TextAlign)null, TextUnit.constructor-impl(0L), (TextOverflow)null, false, 0, (Map)null, (Function1)null, (TextStyle)null, var1, -1162860348, 6, 0, 65534);}}}), var2, 537342819, 0, 4094);}}}), var1, -1469557627, 0, 7);}ScopeUpdateScope var6 = var1.endRestartGroup();if (var6 != null) {var6.updateScope((Function3)(new Function3() {// $FF: synthetic method// $FF: bridge methodpublic Object invoke(Object var1, Object var2, Object var3x) {this.invoke((Composer)var1, ((Number)var2).intValue(), ((Number)var3x).intValue());return Unit.INSTANCE;}public final void invoke(Composer var1, int var2, int var3x) {MainActivityKt.Greeting(var0, var1, var2, var3 | 1);}}));}}private static final int Greeting$lambda_1/* $FF was: Greeting$lambda-1*/(MutableState var0) {State var2 = (State)var0;KProperty var1 = $$delegatedProperties[0];return ((Number)var2.getValue()).intValue();}private static final void Greeting$lambda_2/* $FF was: Greeting$lambda-2*/(MutableState var0, int var1) {KProperty var2 = $$delegatedProperties[0];var0.setValue(var1);}

代码非常简单,即点击button 改变text文字,我们跟踪state可以发现,state最终是生成一个MutableState的变量,而我们改变其值最终都会调用起setValue函数

    @Suppress("UNCHECKED_CAST")override var value: Tget() = next.readable(this).valueset(value) = next.withCurrent {if (!areEquivalent(it.value, value)) {next.writable(this).value = value}}

这里会对属性进行判断,如果一样则不做变化,如果不一样则改变并记录值,这里next.writable来自于Frames类,写入新的值会触发writeObserver回调

/*** Return a writable frame record for the given record. It is assumed that this is called for the* first framed record in a frame object. If the frame is read-only calling this will throw. A* record is writable if it was created in the current writable frame. A writable record will always* be the readable record (as all newer records are invalid it must be the newest valid record).* This means that if the readable record is not from the current frame, a new record must be* created. To create a new writable record, a record can be reused, if possible, and the readable* record is applied to it. If a record cannot be reused, a new record is created and the readable* record is applied to it. Once the values are correct the record is made live by giving it the* current frame id.*/
fun <T : Record> T.writable(framed: Framed, frame: Frame): T {if (frame.readonly) throw IllegalStateException("In a readonly frame")val id = frame.idval readData = readable<T>(this, id, frame.invalid)// If the readable data was born in this frame, it is writable.if (readData.frameId == frame.id) return readData// The first write to an framed in frameframe.writeObserver?.let { it(framed, false) }// Otherwise, make a copy of the readable data and mark it as born in this frame, making it// writable.val newData = synchronized(framed) {// Calling used() on a framed object might return the same record for each thread calling// used() therefore selecting the record to reuse should be guarded.// Note: setting the frameId to Int.MAX_VALUE will make it invalid for all frames. This// means we can release the lock on the object as used() will no longer select it. Using id// could also be used but it puts the object into a state where the reused value appears to// be the current valid value for the the frame. This is not an issue if the frame is only// being read from a single thread but using Int.MAX_VALUE allows multiple readers, single// writer, of a frame. Note that threads reading a mutating frame should not cache the// result of readable() as the mutating thread calls to writable() can change the result of// readable().@Suppress("UNCHECKED_CAST")(used(framed, id, frame.invalid) as T?)?.apply { frameId = Int.MAX_VALUE }?: readData.create().apply {frameId = Int.MAX_VALUE; framed.prependFrameRecord(this as T)} as T}newData.assign(readData)newData.frameId = idframe.modified?.add(framed)return newData
}

而writeObserver回调由FramesManager注册

    private val writeObserver: (write: Any, isNew: Boolean) -> Unit = { value, isNew ->if (!commitPending) {commitPending = trueschedule {commitPending = falsenextFrame()}}recordWrite(value, isNew)}

这里会通知下一帧对数据进行改变,在nextFrame函数中

    fun nextFrame() {if (inFrame) {commit()open()}}

回调用commit提交当前帧的改变

/*** Commit the given frame. Throws FrameAborted if changes in the frame collides with the current* committed frame.*/
fun commit(frame: Frame) {// NOTE: the this algorithm is currently does not guarantee a serializable frame operation as it// doesn't prevent crossing writes as described here https://arxiv.org/pdf/1412.2324.pdf// Just removing the frame from the open frame set is enough to make it visible, however, this// should only be done after first determining that there are no colliding writes in the commit.// A write is considered colliding if any write occurred on the object in a frame committed// since the frame was last opened. There is a trivial cases that can be dismissed immediately,// no writes occurred.val modified = frame.modifiedval id = frame.idval listeners = synchronized(sync) {if (!openFrames.get(id)) throw IllegalStateException("Frame not open")if (modified == null || modified.size == 0) {closeFrame(frame)emptyList()} else {// If there are modifications we need to ensure none of the modifications have// collisions.// A record is guaranteed not collide if no other write was performed to the record by a// committed frame since this frame was opened. No writes to a framed object occurred// if, ignoring this frame, the readable records for the framed object are the same. If// they are different, and the records could be merged, (such as considering writes to// different fields as not colliding) could be allowed here but, for now, the all writes// to a record are considered atomic. Additionally, if the field values can be merged// (e.g. using a conflict-free data type) this could also be allowed here.val current = openFramesval nextFrame = maxFrameIdval start = frame.invalid.set(id)for (framed in frame.modified) {val first = framed.firstFrameRecordif (readable(first,nextFrame,current) != readable(first, id, start)) {abort(frame)}}closeFrame(frame)commitListeners.toList()}}if (modified != null)for (commitListener in listeners) {commitListener(modified, frame)}
}

最终在commitListener 通知commitObserver进行重建,这里通过两个map获取需要重建的RecomposerScope,最终调用composer的invalidate对界面进行更新

private val commitObserver: (committed: Set<Any>, frame: Frame) -> Unit = { committed, frame ->trace("Model:commitTransaction") {val currentInvalidations = synchronized(lock) {val deferred = deferredMap.getValueOf(frame)val immediate = immediateMap.getValueOf(frame)// Ignore the object if its invalidations were all immediate for the frame.invalidations[committed.filter {!immediate.contains(it) || deferred.contains(it)}]}if (currentInvalidations.isNotEmpty()) {if (!isMainThread()) {schedule {currentInvalidations.forEach { scope -> scope.invalidate() }}} else {currentInvalidations.forEach { scope -> scope.invalidate() }}}}}

这里invalidations的值会在composer获取值的时候记录每一个composer,所以当该值发生变化即可通知到composer刷新

    private val readObserver: (read: Any) -> Unit = { read ->currentComposerInternal?.currentRecomposeScope?.let {synchronized(lock) {it.used = trueinvalidations.add(read, it)}}}

小结

本篇文章简单的对jetpack compose ui大体架构实现原理进行简单的分析,不过由于目前我对这部分代码还没有完全搞清楚,只是大概了解其部分流程和架构,所以分析起来可能有点乱,部分地方并没有完全讲解清楚,后面我会继续对其源码进行进一步分析,会继续整理总结,争取完全理清相关原理

jetpack compose原理解析相关推荐

  1. 【koa系列】koa洋葱模型及其compose原理解析

    什么是洋葱模型 先来看一个 demo const Koa = require('koa'); const app = new Koa();// 中间件1 app.use((ctx, next) =&g ...

  2. Jetpack Room 使用及原理解析

    深入学习 Jetpack 系列的 Android Architecture Components 中的一些列组件,记录一下学习过程,本文是 Room 的使用及原理解析,通过一个实际的例子,来体验 Ro ...

  3. Android 开发新技术:Jetpack Compose当仁不让

    前言 Jetpack Compose是用于构建原生Android 界面的新款工具包. 平时我们开发Android界面都是靠XML画出来,但是Compose 则是用代码来写界面,和Flutter写法有点 ...

  4. Jetpack Compose 深入探索系列四: Compose UI

    通过 Compose runtime 集成 UI Compose UI 是一个 Kotlin 多平台框架.它提供了通过可组合函数发出 UI 的构建块和机制.除此之外,这个库还包括 Android 和 ...

  5. Jetpack Compose学习笔记

    在前不久的 Android Dev Summit '19 上,Jetpack Compose 终于发布了一个可直接获得的预览版.现在的版本还是 0.1.0-dev02,处于非常早期的版本,官方也再三强 ...

  6. 随输入动态改变ui_深入详解 Jetpack Compose | 优化 UI 构建

    人们对于 UI 开发的预期已经不同往昔.现如今,为了满足用户的需求,我们构建的应用必须包含完善的用户界面,其中必然包括动画 (animation) 和动效 (motion),这些诉求在 UI 工具包创 ...

  7. 在 Jetpack Compose 中安全地使用数据流

    /   今日科技快讯   / 11月17日下午,暴雪中国官方微博发布公告称,各位暴雪游戏的国服玩家,我们很遗憾地通知大家,随着我们与网之易公司现有授权协议的到期,自2023年1月24日0点起,所有&l ...

  8. Jetpack Compose中的Modifier

    Modifier的基本使用 Modifier修饰符是Jetpack Compose中用来修饰组件的,提供常用的属性,写布局时几乎所有Composable组件的大部分属性都可以用Modifier 来修饰 ...

  9. Jetpack Compose中的手势操作

    点击事件 监听点击事件非常简单,使用 clickable 和 combinedClickable 修饰符即可满足需求: @OptIn(ExperimentalFoundationApi::class) ...

最新文章

  1. Best practice for JVM Tuning[转]
  2. openCV滑动条TrackBar事件实例
  3. 多终端数据同步机制设计
  4. java弹出虚拟键盘_JS实现电脑虚拟键盘的操作
  5. client 连接 host —— 虚拟机
  6. 最近30分钟合约市场爆仓702万美元 BTC爆仓281万美元
  7. gnuplot添加直线和箭头
  8. 导出WPS office文档格式的说明
  9. 美容院店务管理系统哪家好?
  10. ppt转html5原理,如何实现PPT转成H5?
  11. 2011年下半年11月份系统架构设计师上午试题答案之二
  12. c++中rand(),srand()使用
  13. UE4 Async Loading Screen真异步加载插件使用傻瓜式教程
  14. 微信红包随机算法转载
  15. 点与直线位置关系,叉乘
  16. 网络对时设备(NTP校时)如何守护安全日志
  17. 【C语言】初识指针(终篇)
  18. linux中日志服务器的搭建
  19. 对偶理论,敏感性分析(方述诚 笔记4
  20. P1443 马的遍历(洛谷)

热门文章

  1. 全世界70亿人同时起跳,地球会天崩地裂?答案可能让你难以置信
  2. python 保留数字有效位数
  3. 腾讯新研究登Nature子刊,让细胞与计算机直接“对话”,还能辅助医生精准治癌...
  4. 家里全面翻修,破旧家电全部换,电视选哪台?
  5. ChatGPT的一些有趣用法
  6. 【leetcode】帕斯卡三角形
  7. tensorflow2.0基础简介
  8. Android系统之系统签名制作
  9. C# invoke 和 begininvoke 用法
  10. cgo: could not determine kind of name for C.*