jetpack compse原理分析
jetpack compse
Jetpack Composeは、I/O 2019でGoogleがオープンソースとして発表したアンバンドルツールキットです。Jetpack Composeは、ネイティブAndroid UIを構築するためのモダンなツールキットです。 Jetpack Composeは、より少ないコード、強力なツール、直感的なKotlin APIを使用して、Android上でのUI開発を簡素化し、加速します。最も重要なことは、jetpack Composeはレスポンシブアーキテクチャで構築されており、レスポンシブ開発を完璧にサポートしていることです。しかし、唯一のプレビュー版があり、正式版は決定されていない、この記事も簡単な分析の原則の現在のプレビュー版に基づいています。
宣言型UI開発
この原則を理解するために、ある概念を理解する必要があります。つまり、ウィジェットを作成した後、ウィジェットのインスタンスを再取得し、関連関数を呼び出してプロパティを変更する必要があります。
var times = 0
val button:Button = findViewById(R.id.button)
val textView:TextView = findViewById(R.id.text)
button.setOnClickListener{
++times
textView.text="click times:${times}"
}
アピールコードは非常にシンプルで、関数の実装も非常に明確で、つまり、ボタンのクリック数を表示するために使用されるtextviewは、あなたがtextviewのテキストを変更するたびに、あなたはtextViewのインスタンスを取得し、テキストを変更するには、setTextコマンドを呼び出す必要があることを見つけることができ、uiの宣言が異なっている、通常、彼は現在のインターフェイスを記述するために使用される状態を持っています。次に、そのようなコードのアピール機能を達成するために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++;
});
}
@override
Widget 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),
),
);
}
}
状態が変わるたびに、インターフェイスのレイアウトはビルド関数を通じて再宣言され、再表示されることがわかります。これはdeclarative uiとimperative uiの違いです、そこからすぐにdeclarative uiの利点を見つけることができます、あなたはより多くの開発の変更とロジックの実装の状態ではなく、インターフェイスに集中させることができます、ちょうど対応するuiレイアウトの宣言の現在の状態に応じてすることができますし、インターフェイスの再構築や他の作業の状態に応じて便利なことができます、これは非常にモバイル開発に友好的です。しかし、我々はまた、その欠点を見つけることができます、つまり、すべての宣言的なフレームワークは、再利用を最大化するために、コンポーネントの下部に対応するアルゴリズムを持つことになるにもかかわらず、再構築によってもたらされるパフォーマンスの損失は、毎回、それでも、コマンドuiのパフォーマンスや損失の一定量に比べて。次のステップは、Androidのuiの構築方法の未来を探ることですjetpackは、実装の基本原理を合成しますが、より良いこの記事を理解するためには、まず、次の記事を読みに行くことができますjetpack composeアイデアこの記事は、jetpackの合成のレベルの原則の実装から、私はまた、この記事だけでなく、ソースコードに基づいて移動します!この記事では、jetpack composeの実装原理から解説しており、私もこの記事とソースコードを元にjetpack composeを探求しています。同時に、jetpack composeとflutterのいくつかのapiの共通点も見つけることができるので、この記事では2つの実装の共通点と相違点を分析するために2つを比較することもします。
原理分析
名前からjetpackの合成を見ることができる 、 フレームワークは、また、機能的な応答プログラミングによって提唱されているビルドする継承の概念よりも良いの組み合わせに基づいています 。jetpack composeにコンタクトを取った当初、私の最初の直感はAndroidの既存のビューシステムアーキテクチャに基づいてカプセル化し、可能な限り現在のコンポーネントを再利用することでしたが、理解した後、実はそうではなく、jetpack composeは元のAndroidのビューシステムを放棄し、完全にuiシステムの新しいセットを再実装していることがわかりました。ビューシステムが大きすぎると、コンセプトがあまりにも時代遅れではなく、現在の機会を壊さないように、完全に新しいアイデアに基づいて、現代のuiシステムを再実装するという説明をGoogleがしました。
全体的なフレームワーク
uiが最終的にツリーの形で記述できることは誰の目にも明らかで、composeも同様で、最終的にはLayoutNodeのツリーなのですが、宣言的なuiの特徴を実現するためには、設定と描画を分離する必要があり、また、適切なタイミングでノードを再利用する必要があるため、composeはアノテーションの助けを借りて、関連する詳細を実現するために使用されます。composeの全体的なuiフレームワークはざっとこんな感じです。
1.@Composableアノテーション関数 ここでは、uiの宣言と構成は、また、開発者に直接向けられています 2.アノテーションとComposerの実装の具体的な詳細に基づいて ここでは、LayoutNodeのキャッシュと再利用の実装は、また、リスニングの属性変更の実装です 3.LayoutNodeは、主にレイアウトや図面に使用されます。
1.widgetはuiの記述です。
2.elementはコンポーネントを取ります。 3.renderobjectはレイアウトと描画に使われます。
両者を比較すると、実は多くの共通点があることがわかります。実現方法は大きく異なりますが、考え方は似ています
compose LayoutNodeレイアウト
実際、flutterとcomposeは考え方が似ているだけでなく、レイアウトの実装も似ています。composeはflutterと同じレイアウト方法、つまりボックス制約を使用しています。レイアウト空間の制約を計算するために使用されるLayoutNodeのソースコードをここで見ることができ、レイアウト処理で渡されるパラメータも同じです。
制約はここで見ることができ、flutterのボックス制約は、flutterの学生が知っておくべき理解するために類似している、flutterのレイアウトプロセスは、レイアウトを高速化するために使用されるレイアウト境界の最適化条件がある、つまり、再レイアウトの境界が発生した場合、再レイアウト処理でビューは、ビューを変更する方法に関係なく、レイアウト要求を渡すために続行されませんので、レイアウト要求を渡すために続行されません。このビューを変更する方法に関係なく、親ビューのレイアウトに影響を与えませんので、親ビューが再レイアウトする必要はありませんので、最初は2つのレイアウトの類似性に基づいて、私はまた、構成することも、関連する最適化手法で使用されると思いますが、コードをトレースし、ないことを発見し続ける、ここではなぜ、または最適化の公式バージョンの後続のバージョンは、コードのこの部分に追加される可能性があります明確ではありません。コードのこの部分は、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 node
return 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 = layoutNode
while (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 remeasuring
consistencyChecker?.assertConsistent()
return false
}
layout = parent
}
}
layout.markRemeasureRequested()
requestRelayout(layout.parent ?: layout)
}
}
レイアウトリクエストを渡す際に、effectsParentSizeの判定がありますが、このプロパティを割り当てるコードは以下の通りです。
// The more idiomatic, `if (parentLayoutNode?.isMeasuring == true)` causes boxing
affectsParentSize = parent != null && parent.isMeasuring == true
@Composeableアノテーション実装の詳細
次に、最後に@Composeableアノテーションを見て何をするために、コードのこの部分は、彼が動的に生成するkoltinアノテーションに基づいているため、直接表示するには良いではありません、私は直接スタジオで生成されたコードを見つけませんでした、私は表示するには、この方法を使用しています、まずapkをコンパイルし、jarファイルにclasses.dexファイルをデコンパイルし、任意のAndroidプロジェクトにjarファイルを導入すると、コードを表示することができますまず、Layoutに対応するコードを見て、これはcomposeレイアウトの基本クラスであり、Columnなどは、それに基づいています。をjarファイルに保存し、そのjarファイルを任意のAndroidプロジェクトに導入すると、関連するコードを表示することができますまず、Layoutに対応するコードを見て、これはレイアウトを構成する基本クラスであり、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 method
public 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);
}
}));
}
}
あなたは、アノテーションが自動的にノードの再利用と更新の作成を処理するのに役立つことがわかりますが、これは主にノードを管理するためにComposerクラスの手段であり、@Composeableアノテーション関数は、最終的にRestartableFunction関数を生成します、つまり、上記のコードvar0 LayoutNodeの再利用に、ここでのComposeは、レイアウトノードのキャッシュのためのギャップバッファと呼ばれるメソッドを使用して、より複雑であり、再利用するかどうかを判断するために位置情報メモリに基づいています。ここでのcomposeはより複雑で、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 method
public 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 method
public 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 method
public 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 method
public 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);
}
コードは非常に簡単です、つまり、テキストテキストを変更するには、ボタンをクリックすると、追跡状態を見つけることができ、状態は最終的にMutableState変数が生成され、その値を変更すると、最終的にsetValue関数を呼び出します。
@Suppress("UNCHECKED_CAST")
override var value: T
get() = next.readable(this).value
set(value) = next.withCurrent {
if (!areEquivalent(it.value, value)) {
next.writable(this).value = value
}
}
ここで、属性が判定され、同じであれば変更されず、異なっていれば変更され、値が記録されます。ここで、next.writeableは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.id
val 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 frame
frame.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 = id
frame.modified?.add(framed)
return newData
}
そして、writeObserverコールバックは、FramesManagerによって登録されます。
private val writeObserver: (write: Any, isNew: Boolean) -> Unit = { value, isNew ->
if (!commitPending) {
commitPending = true
schedule {
commitPending = false
nextFrame()
}
}
recordWrite(value, isNew)
}
nextFrame関数では、次のフレームでデータを変更するよう通知されます。
fun nextFrame() {
if (inFrame) {
commit()
open()
}
}
コミットをコールバックして現在のフレームに変更を提出
/**
* 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://.org/pdf/..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.modified
val id = frame.id
val 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 = openFrames
val nextFrame = maxFrameId
val start = frame.invalid.set(id)
for (framed in frame.modified) {
val first = framed.firstFrameRecord
if (readable(
first,
nextFrame,
current
) != readable(first, id, start)
) {
abort(frame)
}
}
closeFrame(frame)
commitListeners.toList()
}
}
if (modified != null)
for (commitListener in listeners) {
commitListener(modified, frame)
}
}
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() }
}
}
}
}
ここでは、コンポーザーが値を取得したときに、コンポーザーごとに無効化した値を記録しておき、値が変更されたときにコンポーザーに通知して値を更新できるようにしています。
private val readObserver: (read: Any) -> Unit = { read ->
currentComposerInternal?.currentRecomposeScope?.let {
synchronized(lock) {
it.used = true
invalidations.add(read, it)
}
}
}
まとめ
この記事は、原理の簡単な分析を達成するために、jetpackのcompose uiアーキテクチャ上の単純なものですが、コードのこの部分のために私は完全に明確になっていない、ちょうど大まかにプロセスと構造の一部を理解しているので、分析が少し混沌としているかもしれない、場所の一部が完全に明確になっていない、私はさらにソースコードを分析し続けるでしょう、私は整理し、要約します。私は整理と要約を続け、関連する原理を完全に理解するよう努めます




