1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ComposeのノードがキャッシュされているときにどのようにRecompose(処理の再呼び出し)をスキップするのか?

Last updated at Posted at 2021-09-25

の続きです。

以下の中で3:の部分を読んでいきます。3の最後までになります。

1: recordComposerModificationsLocked() を呼ぶ。Recomposer.snapshotInvalidationsを見て、IdentityScopeMapを使って変更点に対しての影響を受けるスコープを取得し、Recomposer.compositionInvalidationsやComposition.invalidationsというフィールドに変更を入れる。
2: compositionInvalidationsをtoRecomposeに入れる。
3: toRecomposeに対して、performRecompose(composition, modifiedValues)、(内部でdoCompose())することでtoApplyを作成。SlotTableを見て、ここで木を見ていき、木への変更をrecordしていく。
4: recordされたtoApplyに対してapplyChanges()をすることで、SlotTableの書き換えが走る。 (予想。コード未読)

以下でstateに変更があったときにNode2()がどのように動かないのか??を追っていきます。

@Composable
fun Content() {
    var state by remember { mutableStateOf(true) }
    LaunchedEffect(Unit) {
        delay(10000)
        state = false
    }
    if (state) {
        Node1()
    }
    Node2()
}

@Composable
private fun Node2(name: String = "node2") {
    ReusableComposeNode<Node.Node2, NodeApplier>(
        factory = {
            Node.Node2()
        },
        update = {
            set(name) { this.name = it }
        },
    )
}

まずデコンパイル結果はこちら。ここでは読む必要ないです。

Content()関数
         $composer.endReplaceableGroup();
         Node2((String)null, $composer, 0, 1);
      }

      ScopeUpdateScope var18 = $composer.endRestartGroup();
      if (var18 != null) {
         var18.updateScope((Function2)(new Function2() {
            public final void invoke(@Nullable Composer $composer, int $force) {
               MainKt.Content($composer, $changed | 1);
            }
         }));
      }

   }
Node2()関数
   @Composable
   private static final void Node2(final String name, Composer $composer, final int $changed, final int var3) {
      $composer = $composer.startRestartGroup(1815931930);
      int $dirty = $changed;
      if ((var3 & 1) != 0) {
         $dirty = $changed | 6;
      } else if (($changed & 14) == 0) {
         $dirty = $changed | ($composer.changed(name) ? 4 : 2);
      }

      if (($dirty & 11 ^ 2) == 0 && $composer.getSkipping()) {
         $composer.skipToGroupEnd();
      } else {
// !!!!!今回はスキップされるので、この中は実行されない!!!!!
         if ((var3 & 1) != 0) {
            name = "node2";
         }

         Function0 factory$iv = (Function0)null.INSTANCE;
         int $changed$iv = false;
         int $i$f$ReusableComposeNode = false;
         $composer.startReplaceableGroup(1546164276);
         ComposerKt.sourceInformation($composer, "C(ReusableComposeNode):Composables.kt#9igjgp");
         if (!($composer.getApplier() instanceof NodeApplier)) {
            ComposablesKt.invalidApplier();
         }

         $composer.startReusableNode();
         if ($composer.getInserting()) {
            $composer.createNode((Function0)(new MainKt$Node2$$inlined$ReusableComposeNode$1(factory$iv)));
         } else {
            $composer.useNode();
         }

         $composer.disableReusing();
         Composer $this$Node2_u24lambda_u2d6 = Updater.constructor-impl($composer);
         int var9 = false;
         Updater.set-impl($this$Node2_u24lambda_u2d6, name, (Function2)null.INSTANCE);
         $composer.enableReusing();
         $composer.endNode();
         $composer.endReplaceableGroup();
      }

      ScopeUpdateScope var10 = $composer.endRestartGroup();
      if (var10 != null) {
         var10.updateScope((Function2)(new Function2() {
            public final void invoke(@Nullable Composer $composer, int $force) {
               MainKt.Node2(name, $composer, $changed | 1, var3);
            }
         }));
      }

   }

Node2()

まずは$composer.startRestartGroup()

これは前回の https://qiita.com/takahirom/items/11e3ed72eb2f83440b12#addrecomposescope
と同じだと思われます。

Node2()
└── *startRestartGroup()* ← Now reading

今がSlotTableのどこなのかを明示していきます。
今のcurrentは13になりました。
image.png

androidx.compose.runtime.SlotTable@5935541
Group(0) key=100, nodes=2, size=16, slots=[0: {}]
 Group(1) key=1000, nodes=2, size=15
  Group(2) key=200, nodes=2, size=14 objectKey=OpaqueKey(key=provider)
   Group(3) key=-985533281, nodes=2, size=13, slots=[2: androidx.compose.runtime.RecomposeScopeImpl@4fb4ae6, androidx.compose.runtime.internal.ComposableLambdaImpl@3b52827]
    Group(4) key=-337788286, nodes=2, size=12 aux=C(Content), slots=[5: androidx.compose.runtime.RecomposeScopeImpl@b882ad4]
     Group(5) key=-3687241, nodes=0, size=1 aux=C(remember):Composables.kt#9igjgp, slots=[7: MutableState(value=true)@167707773]
     Group(6) key=-3686930, nodes=0, size=1 aux=C(remember)P(1):Composables.kt#9igjgp, slots=[9: MutableState(value=true)@167707773, Function2<kotlinx.coroutines.CoroutineScope, kotlin.coroutines.Continuation<? super kotlin.Unit>, java.lang.Object>]
     Group(7) key=1036442245, nodes=0, size=2 aux=C(LaunchedEffect)P(1)336@14101L58:Effects.kt#9igjgp
      Group(8) key=-3686930, nodes=0, size=1 aux=C(remember)P(1):Composables.kt#9igjgp, slots=[13: kotlin.Unit, androidx.compose.runtime.LaunchedEffectImpl@8d3f428]
     Group(9) key=-337788139, nodes=1, size=4
      Group(10) key=1815931752, nodes=1, size=3, slots=[15: androidx.compose.runtime.RecomposeScopeImpl@7421fc3]
       Group(11) key=1546164276, nodes=1, size=2 aux=C(ReusableComposeNode):Composables.kt#9igjgp
        Group(12) key=125, nodes=0, size=1 node=Node1(name=node1), slots=[18: node1]
↓ ここと実装を比較していく。
     Group(13) key=1815932025, nodes=1, size=3, slots=[19: androidx.compose.runtime.RecomposeScopeImpl@a72040, 600123930]
      Group(14) key=1546164276, nodes=1, size=2 aux=C(ReusableComposeNode):Composables.kt#9igjgp
       Group(15) key=125, nodes=0, size=1 node=Node2(name=600123930), slots=[23: 600123930]

Node2の計算は何をしているのか?

Node2の呼び出し元では以下のようになっています。

Node2((String)null, $composer, changed = 0, var3 = 1);
これはデフォルト引数に関するなにかのようです
Compose Compilerをデバッグすると以下のようにパラメーターを以下する処理を見ることができます。

image.png

   @Composable
   private static final void Node2(final String name, Composer $composer, final int $changed, final int var3) {
      $composer = $composer.startRestartGroup(1815931952);
      int $dirty = $changed;
      if ((var3 & 1) != 0) {
         $dirty = $changed | 6;
      } else if (($changed & 14) == 0) {
         $dirty = $changed | ($composer.changed(name) ? 4 : 2);
      }

      if (($dirty & 11 ^ 2) == 0 && $composer.getSkipping()) {
         $composer.skipToGroupEnd();
      } else {
         if ((var3 & 1) != 0) {
            name = "node2";
         }
@Composable
fun Content() {
    var state by remember { mutableStateOf(true) }
    LaunchedEffect(Unit) {
        delay(10000)
        state = false
    }
    if (state) {
        Node1()
    }
    Node2()
    Node2("argument1")
    Node2("argument2")
}

上記のようにデフォルト引数を追加してみて動作を見てみると、 final int $changed, final int var3 という引数が増えているのですが、var3として引数を入れている場合は0、入れていない場合は1になり、また $changed として6が渡ってきます。
おそらくvar3の1がデフォルト引数、別のものが0になっていると思われますが、一旦ここではデフォルト引数関連の処理は本筋からずれるため、省略させてください。

呼び元のバイトコード

         Node2((String)null, $composer, 0, 1);
         Node2("argument1", $composer, 6, 0);
         Node2("argument2", $composer, 6, 0);

まず最初の判定からデフォルト引数 true、そうでないとfalseになります。

>>> 1 and 1 != 0
res2: kotlin.Boolean = true
>>> 0 and 1 != 0
res3: kotlin.Boolean = false

以下がデフォルト引数を使う場合のバイトコードです。

   @Composable
   private static final void Node2(final String name, Composer $composer, final int $changed, final int var3) {
      $composer = $composer.startRestartGroup(1815931952);
      int $dirty = $changed;
      if ((var3 & 1) != 0) {
         $dirty = $changed | 6;
      } else if (($changed & 14) == 0) {
         $dirty = $changed | ($composer.changed(name) ? 4 : 2);
      }

      if (($dirty & 11 ^ 2) == 0 && $composer.getSkipping()) {
         $composer.skipToGroupEnd();
      } else {
         if ((var3 & 1) != 0) {
            name = "node2";
         }

デフォルト引数を使わないと以下のようになります。
if文がデフォルト引数用に増えている事がわかります。

   @Composable
   private static final void Node2(final String name, Composer $composer, final int $changed) {
      $composer = $composer.startRestartGroup(1815931960);
      int $dirty = $changed;
      if (($changed & 14) == 0) {
         $dirty = $changed | ($composer.changed(name) ? 4 : 2);
      }

      if (($dirty & 11 ^ 2) == 0 && $composer.getSkipping()) {
         $composer.skipToGroupEnd();
      } else {
    ...

ここで以下を見ながら見ていきます。

$changedのフォーマットは以下

ParamStateの1ビット目 ParamState2ビット目 ParamState3ビット目 強制実行用ビット
0 1 0 0

ParamState
3桁は不明=000、同じ=001、違う=010、Static(変更されない)=011、不明=100、マスク=111

例えば0 1 0 0だと010なので、変更があり、最後のbitは0なので、強制実行ではないということみたいです。

ここで以下ではわかりやすいようにビットに直して記述してみます

   @Composable
   private static final void Node2(final String name, Composer $composer, final int $changed, final int var3) {
      $composer = $composer.startRestartGroup(1815931952);
      int $dirty = $changed;
      if ((var3 & 1) != 0) {
// デフォルト引数の場合
// $dirtyをParamStateの"変更なし"に変更!
         $dirty = $changed | 0110;
      } else if (($changed & 1110) == 0) {
// デフォルト引数でない & $changedにすでに内容が入っていないならば以下を実行する

// 引数のnameがSlotReaderの内容と比較し、変化しているか?していれば0100(違う場合)に、していなければ0010(同じ場合)にする。
         $dirty = $changed | ($composer.changed(name) ? 0100 : 0010);
      }

ここまでで$dirtyに変化が入りました。

もう一度$changedのフォーマットを思い出しましょう。

ParamStateの1ビット目 ParamState2ビット目 ParamState3ビット目 強制実行用ビット
0 1 0 0

ParamState
3桁は不明=000、同じ=001、違う=010、Static(変更されない)=011、不明=100、マスク=111

つまり**3つ目のビットが1ならば変更がない。**ということになります。
そして2つ目のビットは無視する必要があります。
そのため、 $dirty & 1011 xor 0010 のようになるのではと思いました。

      if (($dirty & 1011 xor 0010) == 0 && $composer.getSkipping()) {
         $composer.skipoGroupEnd();
      } else {
    ...
// 実際の中身
      }

      ScopeUpdateScope var10 = $composer.endRestartGroup();
      if (var10 != null) {
         var10.updateScope((Function2)(new Function2() {
            public final void invoke(@Nullable Composer $composer, int $force) {
               MainKt.Node2(name, $composer, $changed | 1, var3);
            }
         }));
      }

さて、ここまでで $composer.changed() はおそらくCompose内で引数が変更があったかの判定
$composer.getSkipping() は引数の変更があったが、無視できるパターンか?という判定
を返してくるようです。

今回の場合はどうなのか

以下のような呼び元なので、上のデフォルト引数パターンだと $dirty = $changed | 0110; で変更なしになり、デフォルト引数でないパターンだと } else if (($changed & 1110) == 0) {で判定に入らないため、もはやchangedの判定さえいりません。
またgetSkipping()も呼ばれません。

         Node2((String)null, $composer, 0, 1);
         Node2("argument1", $composer, 6, 0);

ではどのようにすると $composer.changed() が呼ばれるでしょうか?
それは変更される可能性があれば、呼ばれそうなのでシードが決まっているので、固定値を返すランダム関数でも噛ませてみましょう。

>>> kotlin.random.Random(1).nextInt().toString()
res2: kotlin.String = 600123930
>>> kotlin.random.Random(1).nextInt().toString()
res3: kotlin.String = 600123930
>>> kotlin.random.Random(1).nextInt().toString()
res4: kotlin.String = 600123930

ここから何回か600123930が出てくるので覚えておいてください。

    val notRandom = Random(1).nextInt().toString()
    Node2(notRandom)

以下のように変換されます。これならデフォルト引数でもchangedにデフォルトで6が入れられることもありません。
この状態で $composer.chagned()のコードを読んでいきましょう。

         String notRandom = String.valueOf(RandomKt.Random(1).nextInt());
         Node2(notRandom, $composer, 0, 0);

composer.changed()

Node2()
├── composer.startRestartGroup()
└── *composer.changed()* ← Now reading
    └── composer.nextSlot()

changed()にはNode2で渡ってきた引数を渡しています。
ここではRandomで作られた固定の数字の文字列が渡ってきます。
一行目で次のslotを取得し、比較しています。違ったらtrueを返し、一緒ならfalseを返すようです。
ここがこの記事で一番大切な部分だと思います。SlotTableと引数が変わっているのかを確認し、差分更新を可能にしています

    @ComposeCompilerApi
    override fun changed(value: Any?): Boolean {
        return if (nextSlot() != value) {
            updateValue(value)
            true
        } else {
            false
        }
    }

image.png

composer.nextSlot()

Node2()
├── composer.startRestartGroup()
└── composer.changed()
    └── *composer.nextSlot()*  Now reading
    @PublishedApi
    @OptIn(InternalComposeApi::class)
    internal fun nextSlot(): Any? = if (inserting) { //falseになる
        validateNodeNotExpected()
        Composer.Empty // ← Composableを挿入中ならComposer.Emptyを返して強制的に違うものにするみたい
    } else reader.next().let { if (reusing) Composer.Empty else it }
//            ↑ slotReaderからnext()を取り出す。

SlotTableに入っている値を取り出す!そしてそれが composer.changed() で比較されます。
image.png

androidx.compose.runtime.SlotTable@5935541
Group(0) key=100, nodes=2, size=16, slots=[0: {}]
 Group(1) key=1000, nodes=2, size=15
  Group(2) key=200, nodes=2, size=14 objectKey=OpaqueKey(key=provider)
   Group(3) key=-985533281, nodes=2, size=13, slots=[2: androidx.compose.runtime.RecomposeScopeImpl@4fb4ae6, androidx.compose.runtime.internal.ComposableLambdaImpl@3b52827]
    Group(4) key=-337788286, nodes=2, size=12 aux=C(Content), slots=[5: androidx.compose.runtime.RecomposeScopeImpl@b882ad4]
     Group(5) key=-3687241, nodes=0, size=1 aux=C(remember):Composables.kt#9igjgp, slots=[7: MutableState(value=true)@167707773]
     Group(6) key=-3686930, nodes=0, size=1 aux=C(remember)P(1):Composables.kt#9igjgp, slots=[9: MutableState(value=true)@167707773, Function2<kotlinx.coroutines.CoroutineScope, kotlin.coroutines.Continuation<? super kotlin.Unit>, java.lang.Object>]
     Group(7) key=1036442245, nodes=0, size=2 aux=C(LaunchedEffect)P(1)336@14101L58:Effects.kt#9igjgp
      Group(8) key=-3686930, nodes=0, size=1 aux=C(remember)P(1):Composables.kt#9igjgp, slots=[13: kotlin.Unit, androidx.compose.runtime.LaunchedEffectImpl@8d3f428]
     Group(9) key=-337788139, nodes=1, size=4
      Group(10) key=1815931752, nodes=1, size=3, slots=[15: androidx.compose.runtime.RecomposeScopeImpl@7421fc3]
       Group(11) key=1546164276, nodes=1, size=2 aux=C(ReusableComposeNode):Composables.kt#9igjgp
        Group(12) key=125, nodes=0, size=1 node=Node1(name=node1), slots=[18: node1]
↓ ここから600123930を取り出す。
     Group(13) key=1815932025, nodes=1, size=3, slots=[19: androidx.compose.runtime.RecomposeScopeImpl@a72040, 600123930]
      Group(14) key=1546164276, nodes=1, size=2 aux=C(ReusableComposeNode):Composables.kt#9igjgp
       Group(15) key=125, nodes=0, size=1 node=Node2(name=600123930), slots=[23: 600123930]

reusingはfalseが入っているようです。

このreusingについては今年の5月ぐらいに導入された新しい仕組みみたいです。layout()で利用され、レイアウトのキャッシュとかがうまくできるというのがよいみたいです。一旦飛ばします。

Introduces ReusableComposeNode that will reuse the node
emitted instead of replacing it as is done for ComposeNode.

これで $composer.changed(name) がfalseになって、2が入り、0010になるのでこれは同じという意味になります。
つまり次は$composer.skipToGroupEnd()が呼ばれます

   @Composable
   private static final void Node2(final String name, Composer $composer, final int $changed, final int var3) {
      $composer = $composer.startRestartGroup(1815931952);
      int $dirty = $changed;
      if ((var3 & 1) != 0) {
         $dirty = $changed | 6;
      } else if (($changed & 14) == 0) {
         $dirty = $changed | ($composer.changed(name) ? 4 : 2);
      }

      if (($dirty & 11 ^ 2) == 0 && $composer.getSkipping()) {
         $composer.skipToGroupEnd();
      } else {
// 今回スキップされる実装
...
      }
      ScopeUpdateScope var10 = $composer.endRestartGroup();
      if (var10 != null) {
         var10.updateScope((Function2)(new Function2() {
            public final void invoke(@Nullable Composer $composer, int $force) {
               MainKt.Node2(name, $composer, $changed | 1, var3);
            }
         }));
      }
   }

composer.skipping

Composeのskippingは以下のようになっています。
inserting = false
reusing = false
providersInvalid = false
currentRecomposeScope?.requiresRecompose = false
そのため、skippingはtrueを返します。

composer
    override val skipping: Boolean get() {
        return !inserting && !reusing &&
            !providersInvalid &&
            currentRecomposeScope?.requiresRecompose == false
    }

composer.skipToGroupEnd()

Node2()
├── composer.startRestartGroup()
├── composer.changed()
   └── composer.nextSlot()
├── *composer.skipToGroupEnd()*  Now reading
└── composer.endRestartGroup()
    /**
     * Skip to the end of the group opened by [startGroup].
     * startGroupによって開かれたグループの最後までスキップする。
     */
    @ComposeCompilerApi
    override fun skipToGroupEnd() {
// スキップするので、nodeがemitされていたらおかしい。
        runtimeCheck(groupNodeCount == 0) {
            "No nodes can be emitted before calling skipAndEndGroup"
        }
// skipped = trueを書き込むのみ
        currentRecomposeScope?.scopeSkipped()
        if (invalidations.isEmpty()) {
// invalidationsはemptyなので、こちらを呼び出す。
            skipReaderToGroupEnd()
        } else {
            recomposeToGroupEnd()
        }
    }
RecomposeScopeImpl
    fun scopeSkipped() {
        skipped = true
    }
Node2()
├── composer.startRestartGroup()
├── composer.changed()
│   └── composer.nextSlot()
├── composer.skipToGroupEnd()
│   ├── RecomposeScopeImpl.scopeSkipped()
│   └── *composer.skipReaderToGroupEnd()* ← Now reading
└── composer.endRestartGroup()

readerが見てるcurrentGroupを移動させる

composer.skipReaderToGroupEnd
    private fun skipReaderToGroupEnd() {
        groupNodeCount = reader.parentNodes
        reader.skipToGroupEnd()
    }
Node2()
├── composer.startRestartGroup()
├── composer.changed()
│   └── composer.nextSlot()
├── composer.skipToGroupEnd()
│   ├── RecomposeScopeImpl.scopeSkipped()
│   └── composer.skipReaderToGroupEnd()
│       └── *SlotReader.skipToGroupEnd()* ← Now reading
└── composer.endRestartGroup()

skipToGroupEnd()ではcurrentEndをcurrentGroupに代入するのみ。

SlotReader
    fun skipToGroupEnd() {
        require(emptyCount == 0) { "Cannot skip the enclosing group while in an empty region" }
        currentGroup = currentEnd
    }

今のcurrentEndは16になっていて、完全にSlotTableでは終わっている。

androidx.compose.runtime.SlotTable@5935541
Group(0) key=100, nodes=2, size=16, slots=[0: {}]
 Group(1) key=1000, nodes=2, size=15
  Group(2) key=200, nodes=2, size=14 objectKey=OpaqueKey(key=provider)
   Group(3) key=-985533281, nodes=2, size=13, slots=[2: androidx.compose.runtime.RecomposeScopeImpl@4fb4ae6, androidx.compose.runtime.internal.ComposableLambdaImpl@3b52827]
    Group(4) key=-337788286, nodes=2, size=12 aux=C(Content), slots=[5: androidx.compose.runtime.RecomposeScopeImpl@b882ad4]
     Group(5) key=-3687241, nodes=0, size=1 aux=C(remember):Composables.kt#9igjgp, slots=[7: MutableState(value=true)@167707773]
     Group(6) key=-3686930, nodes=0, size=1 aux=C(remember)P(1):Composables.kt#9igjgp, slots=[9: MutableState(value=true)@167707773, Function2<kotlinx.coroutines.CoroutineScope, kotlin.coroutines.Continuation<? super kotlin.Unit>, java.lang.Object>]
     Group(7) key=1036442245, nodes=0, size=2 aux=C(LaunchedEffect)P(1)336@14101L58:Effects.kt#9igjgp
      Group(8) key=-3686930, nodes=0, size=1 aux=C(remember)P(1):Composables.kt#9igjgp, slots=[13: kotlin.Unit, androidx.compose.runtime.LaunchedEffectImpl@8d3f428]
     Group(9) key=-337788139, nodes=1, size=4
      Group(10) key=1815931752, nodes=1, size=3, slots=[15: androidx.compose.runtime.RecomposeScopeImpl@7421fc3]
       Group(11) key=1546164276, nodes=1, size=2 aux=C(ReusableComposeNode):Composables.kt#9igjgp
        Group(12) key=125, nodes=0, size=1 node=Node1(name=node1), slots=[18: node1]
     Group(13) key=1815932025, nodes=1, size=3, slots=[19: androidx.compose.runtime.RecomposeScopeImpl@a72040, 600123930]
      Group(14) key=1546164276, nodes=1, size=2 aux=C(ReusableComposeNode):Composables.kt#9igjgp
       Group(15) key=125, nodes=0, size=1 node=Node2(name=600123930), slots=[23: 600123930]

composer.endRestartGroup()

Node2()
├── composer.startRestartGroup()
├── composer.changed()
   └── composer.nextSlot()
├── composer.skipToGroupEnd()
   ├── RecomposeScopeImpl.scopeSkipped()
   └── composer.skipReaderToGroupEnd()
       └── SlotReader.skipToGroupEnd()
└── *composer.endRestartGroup()*  Now reading
``

ここでは結構重要そうな処理が多いようにみえます。

```kotlin
    /**
     * End a restart group. If the recompose scope was marked used during composition then a
     * [ScopeUpdateScope] is returned that allows attaching a lambda that will produce the same
     * composition as was produced by this group (including calling [startRestartGroup] and
     * [endRestartGroup]).
     * restart groupを終了する。
     * 以下返り値の使いみちの話みたいです。
     * もしrecmpose scopeはcompositionの間に使われたとマークされているのであれば、
     * ScopeUpdateScopeが返され、それはラムダがアタッチできる。
     * そのラムダは同じCompositionを生産する。そのCompositionはこのグループから生産されたものとして生産される。
     * (startRestartGroupとendRestartGroupの呼び出しを含む)
     */
    @ComposeCompilerApi
    override fun endRestartGroup(): ScopeUpdateScope? {
// エラー処理っぽいので飛ばす。
        // This allows for the invalidate stack to be out of sync since this might be called during exception stack
        // unwinding that might have not called the doneJoin/endRestartGroup in the wrong order.
        val scope = if (invalidateStack.isNotEmpty()) invalidateStack.pop()
        else null

// スコープにrequiresRecomposeをfalseを入れる
        scope?.requiresRecompose = false
// scopeのendを呼ぶ。snapshot.idを呼ぶ。
// startRestartGroup()の中で ` scope.start(snapshot.id)`で開始しており、それを終了する。
// endで帰ってくる処理を呼び出してrecord(次のフェーズで処理)するみたい。
        scope?.end(snapshot.id)?.let {
            record { _, _, _ -> it(composition) }
        }
        val result = if (scope != null &&
            !scope.skipped &&
            (scope.used || collectParameterInformation)
        ) {
            if (scope.anchor == null) {
                scope.anchor = if (inserting) {
                    writer.anchor(writer.parent)
                } else {
                    reader.anchor(reader.parent)
                }
            }
            scope.defaultsInvalid = false
            scope
        } else {
            null
        }
        end(isNode = false)
        return result
    }
Node2()
├── composer.startRestartGroup()
├── composer.changed()
   └── composer.nextSlot()
├── composer.skipToGroupEnd()
   ├── RecomposeScopeImpl.scopeSkipped()
   └── composer.skipReaderToGroupEnd()
       └── SlotReader.skipToGroupEnd()
└── composer.endRestartGroup()
    └── *RecomposeScopeImpl.end(id = 13)*  Now reading
    /**
     * Called when composition is completed for this scope. The [token] is the same token passed
     * in the previous call to [start]. If [end] returns a non-null value the lambda returned
     * will be called during [ControlledComposition.applyChanges].
     */
    fun end(token: Int): ((Composition) -> Unit)? {
// trackedInstancesがnullなので、何もせずにnullを返す
// trackedInstancesは何かというと、readすると追加されるもの、つまりstateとかを使っていれば入っていると思われる。
// そのためNode2の呼び元のContent()ではなにかが起こるかも??
        return trackedInstances?.let { instances ->
            // If any value previous observed was not read in this current composition
            // schedule the value to be removed from the observe scope and removed from the
            // observations tracked by the composition.
            // [skipped] is true if the scope was skipped. If the scope was skipped we should
            // leave the observations unmodified.
            if (
                !skipped && instances.any { _, instanceToken -> instanceToken != token }
            ) { composition ->
                if (
                    currentToken == token && instances == trackedInstances &&
                    composition is CompositionImpl
                ) {
                    instances.removeValueIf { instance, instanceToken ->
                        (instanceToken != token).also { remove ->
                            if (remove) {
                                composition.removeObservation(instance, this)
                                (instance as? DerivedState<*>)?.let {
                                    trackedDependencies?.let { dependencies ->
                                        dependencies.remove(it)
                                        if (dependencies.size == 0) {
                                            trackedDependencies = null
                                        }
                                    }
                                }
                            }
                        }
                    }
                    if (instances.size == 0) trackedInstances = null
                }
            } else null
        }
    }
}

戻ります。

Node2()
├── composer.startRestartGroup()
├── composer.changed()
   └── composer.nextSlot()
├── composer.skipToGroupEnd()
   ├── RecomposeScopeImpl.scopeSkipped()
   └── composer.skipReaderToGroupEnd()
       └── SlotReader.skipToGroupEnd()
└── *composer.endRestartGroup()*  Now reading
    └── RecomposeScopeImpl.end(id = 13)
    /**
     * End a restart group. If the recompose scope was marked used during composition then a
     * [ScopeUpdateScope] is returned that allows attaching a lambda that will produce the same
     * composition as was produced by this group (including calling [startRestartGroup] and
     * [endRestartGroup]).
     * restart groupを終了する。
     * 以下返り値の使いみちの話みたいです。
     * もしrecmpose scopeはcompositionの間に使われたとマークされているのであれば、
     * ScopeUpdateScopeが返され、それはラムダがアタッチできる。
     * そのラムダは同じCompositionを生産する。そのCompositionはこのグループから生産されたものとして生産される。
     * (startRestartGroupとendRestartGroupの呼び出しを含む)
     */
    @ComposeCompilerApi
    override fun endRestartGroup(): ScopeUpdateScope? {
// エラー処理っぽいので飛ばす。
        // This allows for the invalidate stack to be out of sync since this might be called during exception stack
        // unwinding that might have not called the doneJoin/endRestartGroup in the wrong order.
        val scope = if (invalidateStack.isNotEmpty()) invalidateStack.pop()
        else null

// スコープにrequiresRecomposeをfalseを入れる
        scope?.requiresRecompose = false
// scopeのendを呼ぶ。snapshot.idを呼ぶ。
// startRestartGroup()の中で ` scope.start(snapshot.id)`で開始しており、それを終了する。
// endで帰ってくる処理を呼び出してrecord(次のフェーズで処理)するみたい。
        scope?.end(snapshot.id)?.let {
            record { _, _, _ -> it(composition) }
        }
        val result = if (scope != null &&
// 以下がtrueになるので、elseになり、resultはnullになる。
// そしてend()が呼ばれる。
            !scope.skipped &&
            (scope.used || collectParameterInformation)
        ) {
            if (scope.anchor == null) {
                scope.anchor = if (inserting) {
                    writer.anchor(writer.parent)
                } else {
                    reader.anchor(reader.parent)
                }
            }
            scope.defaultsInvalid = false
            scope
        } else {
            null
        }
        end(isNode = false)
        return result
    }

end()

前回end()がendしない(終わらないぐらい長かった)んですが変更がないからほとんど処理を行いません。

おまけ: Content()でのendRestartGroup()

ほぼ何もなかったので、読まなくていいです。

RecomposeScopeImpl.end()
    /**
     * End a restart group. If the recompose scope was marked used during composition then a
     * [ScopeUpdateScope] is returned that allows attaching a lambda that will produce the same
     * composition as was produced by this group (including calling [startRestartGroup] and
     * [endRestartGroup]).
     */
    @ComposeCompilerApi
    override fun endRestartGroup(): ScopeUpdateScope? {
        // This allows for the invalidate stack to be out of sync since this might be called during exception stack
        // unwinding that might have not called the doneJoin/endRestartGroup in the wrong order.
        val scope = if (invalidateStack.isNotEmpty()) invalidateStack.pop()
        else null
        scope?.requiresRecompose = false
        scope?.end(snapshot.id)?.let {
            record { _, _, _ -> it(composition) }
        }
        val result = if (scope != null &&
            !scope.skipped &&
            (scope.used || collectParameterInformation)
        ) {
            if (scope.anchor == null) {
                scope.anchor = if (inserting) {
                    writer.anchor(writer.parent)
                } else {
                    reader.anchor(reader.parent)
                }
            }
            scope.defaultsInvalid = false
            scope
        } else {
            null
        }
        end(isNode = false)
        return result
    }

以下では見ていたstateクラスなどのインスタンスを見なくなったなどのときに、消せるようにするもの。今回は同じものを見ていたので、消さない。

    /**
     * Called when composition is completed for this scope. The [token] is the same token passed
     * in the previous call to [start]. If [end] returns a non-null value the lambda returned
     * will be called during [ControlledComposition.applyChanges].
     */
    fun end(token: Int): ((Composition) -> Unit)? {
        return trackedInstances?.let { instances ->
            // If any value previous observed was not read in this current composition
            // schedule the value to be removed from the observe scope and removed from the
            // observations tracked by the composition.
            // [skipped] is true if the scope was skipped. If the scope was skipped we should
            // leave the observations unmodified.
            if (
                !skipped && instances.any { _, instanceToken -> instanceToken != token }
            ) { composition ->
                if (
                    currentToken == token && instances == trackedInstances &&
                    composition is CompositionImpl
                ) {
                    instances.removeValueIf { instance, instanceToken ->
                        (instanceToken != token).also { remove ->
                            if (remove) {
                                composition.removeObservation(instance, this)
                                (instance as? DerivedState<*>)?.let {
                                    trackedDependencies?.let { dependencies ->
                                        dependencies.remove(it)
                                        if (dependencies.size == 0) {
                                            trackedDependencies = null
                                        }
                                    }
                                }
                            }
                        }
                    }
                    if (instances.size == 0) trackedInstances = null
                }
            } else null
        }
    }

まとめ

現在のSlotTableと渡される引数比較することでComposableの処理が実際にスキップされる様子を観察することができました。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?