jetpack compose原理解析
目录
- 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原理解析相关推荐
- 【koa系列】koa洋葱模型及其compose原理解析
什么是洋葱模型 先来看一个 demo const Koa = require('koa'); const app = new Koa();// 中间件1 app.use((ctx, next) =&g ...
- Jetpack Room 使用及原理解析
深入学习 Jetpack 系列的 Android Architecture Components 中的一些列组件,记录一下学习过程,本文是 Room 的使用及原理解析,通过一个实际的例子,来体验 Ro ...
- Android 开发新技术:Jetpack Compose当仁不让
前言 Jetpack Compose是用于构建原生Android 界面的新款工具包. 平时我们开发Android界面都是靠XML画出来,但是Compose 则是用代码来写界面,和Flutter写法有点 ...
- Jetpack Compose 深入探索系列四: Compose UI
通过 Compose runtime 集成 UI Compose UI 是一个 Kotlin 多平台框架.它提供了通过可组合函数发出 UI 的构建块和机制.除此之外,这个库还包括 Android 和 ...
- Jetpack Compose学习笔记
在前不久的 Android Dev Summit '19 上,Jetpack Compose 终于发布了一个可直接获得的预览版.现在的版本还是 0.1.0-dev02,处于非常早期的版本,官方也再三强 ...
- 随输入动态改变ui_深入详解 Jetpack Compose | 优化 UI 构建
人们对于 UI 开发的预期已经不同往昔.现如今,为了满足用户的需求,我们构建的应用必须包含完善的用户界面,其中必然包括动画 (animation) 和动效 (motion),这些诉求在 UI 工具包创 ...
- 在 Jetpack Compose 中安全地使用数据流
/ 今日科技快讯 / 11月17日下午,暴雪中国官方微博发布公告称,各位暴雪游戏的国服玩家,我们很遗憾地通知大家,随着我们与网之易公司现有授权协议的到期,自2023年1月24日0点起,所有&l ...
- Jetpack Compose中的Modifier
Modifier的基本使用 Modifier修饰符是Jetpack Compose中用来修饰组件的,提供常用的属性,写布局时几乎所有Composable组件的大部分属性都可以用Modifier 来修饰 ...
- Jetpack Compose中的手势操作
点击事件 监听点击事件非常简单,使用 clickable 和 combinedClickable 修饰符即可满足需求: @OptIn(ExperimentalFoundationApi::class) ...
最新文章
- Best practice for JVM Tuning[转]
- openCV滑动条TrackBar事件实例
- 多终端数据同步机制设计
- java弹出虚拟键盘_JS实现电脑虚拟键盘的操作
- client 连接 host —— 虚拟机
- 最近30分钟合约市场爆仓702万美元 BTC爆仓281万美元
- gnuplot添加直线和箭头
- 导出WPS office文档格式的说明
- 美容院店务管理系统哪家好?
- ppt转html5原理,如何实现PPT转成H5?
- 2011年下半年11月份系统架构设计师上午试题答案之二
- c++中rand(),srand()使用
- UE4 Async Loading Screen真异步加载插件使用傻瓜式教程
- 微信红包随机算法转载
- 点与直线位置关系,叉乘
- 网络对时设备(NTP校时)如何守护安全日志
- 【C语言】初识指针(终篇)
- linux中日志服务器的搭建
- 对偶理论,敏感性分析(方述诚 笔记4
- P1443 马的遍历(洛谷)
热门文章
- 全世界70亿人同时起跳,地球会天崩地裂?答案可能让你难以置信
- python 保留数字有效位数
- 腾讯新研究登Nature子刊,让细胞与计算机直接“对话”,还能辅助医生精准治癌...
- 家里全面翻修,破旧家电全部换,电视选哪台?
- ChatGPT的一些有趣用法
- 【leetcode】帕斯卡三角形
- tensorflow2.0基础简介
- Android系统之系统签名制作
- C# invoke 和 begininvoke 用法
- cgo: could not determine kind of name for C.*