Jetpack Composeの`@Stable`や`@Immutable`のバイトコード上の挙動を確かめてみる


どういうときにComposable関数がキャッシュされる?されない?ってすごく重要だと思っていますがみなさん理解されていますか?

Compose 1.0.1で行っています。

これらのアノテーションについてはこちらで紹介しています。

また以下のドキュメントで言及されています

実際どうなるのみたいなところが気になったので、少し気になるパターンでまとめてみました。

キャッシュされているときはでコンパイルすると以下のようなskipのコードが入ります。
このGradle Pluginを使うと以下のようなJavaコードを簡単に見ることができます。
https://github.com/takahirom/decomposer

      if (($dirty & 11 ^ 2) == 0 && $composer.getSkipping()) {
         $composer.skipToGroupEnd();
      } else {
         TextKt.Text-fLXpl1I(Intrinsics.stringPlus("Hello ", article), (Modifier)null, 0L, 0L, (FontStyle)null, (FontWeight)null, (FontFamily)null, 0L, (TextDecoration)null, (TextAlign)null, 0L, 0, false, 0, (Function1)null, (TextStyle)null, $composer, 0, 64, 65534);
      }

チェックに用いたコード

@Composable
fun String(name: String) {
    Text(text = "Hello $name!")
}


data class Article(val title: String)

@Composable
fun DataClass(article: Article) {
    Text(text = "Hello $article")
}

data class MutableArticle(var title: String)

@Composable
fun MutableDataClass(article: MutableArticle) {
    Text(text = "Hello $article")
}


data class HasThrowableDataClass(val error:Throwable)

@Composable
fun HasThrowableDataClass(hasThrowableDataClass: HasThrowableDataClass) {
    Text(text = "Hello $hasThrowableDataClass")
}

@Immutable data class HasThrowableWithImmutableAnnotation(val error:Throwable)

@Composable
fun HasThrowableWithImmutableAnnotation(hasThrowable: HasThrowableWithImmutableAnnotation) {
    Text(text = "Hello $hasThrowable")
}

@Stable data class HasThrowableWithStableAnnotation(val error:Throwable)

@Composable
fun HasThrowableWithStableAnnotation(hasThrowable: HasThrowableWithStableAnnotation) {
    Text(text = "Hello $hasThrowable")
}

data class HasSimpleClass(val simpleClass: SimpleClass)
class SimpleClass(val simpleText:String)

@Composable
fun HasSimpleClass(simpleClass: HasSimpleClass) {
    Text(text = "Hello $simpleClass")
}

data class HasHasThrowableDataClass(val obj: HasThrowableDataClass)

@Composable
fun HasHasThrowableDataClass(obj: HasHasThrowableDataClass) {
    Text(text = "Hello $obj")
}

上記のコードは以下のリポジトリにおいてあり、ビルドすれば勝手にJavaコードがapp/build/decompiledにでコンパイルされたJavaコードが見られます。
https://github.com/takahirom/jetpack-compose-stability/blob/main/app/src/main/java/com/github/takahirom/jetpack_compose_stability_samples/StabilityChecks.kt

デコンパイル結果はこちら


package com.github.takahirom.jetpack_compose_stability_samples;

import androidx.compose.material.TextKt;
import androidx.compose.runtime.Composable;
import androidx.compose.runtime.Composer;
import androidx.compose.runtime.ComposerKt;
import androidx.compose.runtime.ScopeUpdateScope;
import androidx.compose.ui.Modifier;
import androidx.compose.ui.text.TextStyle;
import androidx.compose.ui.text.font.FontFamily;
import androidx.compose.ui.text.font.FontStyle;
import androidx.compose.ui.text.font.FontWeight;
import androidx.compose.ui.text.style.TextAlign;
import androidx.compose.ui.text.style.TextDecoration;
import kotlin.Metadata;
import kotlin.jvm.functions.Function1;
import kotlin.jvm.functions.Function2;
import kotlin.jvm.internal.Intrinsics;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

@Metadata(
   mv = {1, 5, 1},
   k = 2,
   xi = 48,
   d1 = {"\u0000H\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0003\n\u0002\u0018\u0002\n\u0002\b\u0003\n\u0002\u0018\u0002\n\u0002\b\u0003\n\u0002\u0018\u0002\n\u0002\b\u0003\n\u0002\u0018\u0002\n\u0002\b\u0002\n\u0002\u0018\u0002\n\u0002\b\u0002\n\u0002\u0018\u0002\n\u0002\b\u0003\n\u0002\u0010\u000e\n\u0002\b\u0002\u001a\u0015\u0010\u0000\u001a\u00020\u00012\u0006\u0010\u0002\u001a\u00020\u0003H\u0007¢\u0006\u0002\u0010\u0004\u001a\u0015\u0010\u0005\u001a\u00020\u00012\u0006\u0010\u0006\u001a\u00020\u0007H\u0007¢\u0006\u0002\u0010\b\u001a\u0015\u0010\t\u001a\u00020\u00012\u0006\u0010\n\u001a\u00020\u000bH\u0007¢\u0006\u0002\u0010\f\u001a\u0015\u0010\r\u001a\u00020\u00012\u0006\u0010\u000e\u001a\u00020\u000fH\u0007¢\u0006\u0002\u0010\u0010\u001a\u0015\u0010\u0011\u001a\u00020\u00012\u0006\u0010\u0012\u001a\u00020\u0013H\u0007¢\u0006\u0002\u0010\u0014\u001a\u0015\u0010\u0015\u001a\u00020\u00012\u0006\u0010\u0012\u001a\u00020\u0016H\u0007¢\u0006\u0002\u0010\u0017\u001a\u0015\u0010\u0018\u001a\u00020\u00012\u0006\u0010\u0002\u001a\u00020\u0019H\u0007¢\u0006\u0002\u0010\u001a\u001a\u0015\u0010\u001b\u001a\u00020\u00012\u0006\u0010\u001c\u001a\u00020\u001dH\u0007¢\u0006\u0002\u0010\u001e¨\u0006\u001f"},
   d2 = {"DataClass", "", "article", "Lcom/github/takahirom/jetpack_compose_stability_samples/Article;", "(Lcom/github/takahirom/jetpack_compose_stability_samples/Article;Landroidx/compose/runtime/Composer;I)V", "HasHasThrowableDataClass", "obj", "Lcom/github/takahirom/jetpack_compose_stability_samples/HasHasThrowableDataClass;", "(Lcom/github/takahirom/jetpack_compose_stability_samples/HasHasThrowableDataClass;Landroidx/compose/runtime/Composer;I)V", "HasSimpleClass", "simpleClass", "Lcom/github/takahirom/jetpack_compose_stability_samples/HasSimpleClass;", "(Lcom/github/takahirom/jetpack_compose_stability_samples/HasSimpleClass;Landroidx/compose/runtime/Composer;I)V", "HasThrowableDataClass", "hasThrowableDataClass", "Lcom/github/takahirom/jetpack_compose_stability_samples/HasThrowableDataClass;", "(Lcom/github/takahirom/jetpack_compose_stability_samples/HasThrowableDataClass;Landroidx/compose/runtime/Composer;I)V", "HasThrowableWithImmutableAnnotation", "hasThrowable", "Lcom/github/takahirom/jetpack_compose_stability_samples/HasThrowableWithImmutableAnnotation;", "(Lcom/github/takahirom/jetpack_compose_stability_samples/HasThrowableWithImmutableAnnotation;Landroidx/compose/runtime/Composer;I)V", "HasThrowableWithStableAnnotation", "Lcom/github/takahirom/jetpack_compose_stability_samples/HasThrowableWithStableAnnotation;", "(Lcom/github/takahirom/jetpack_compose_stability_samples/HasThrowableWithStableAnnotation;Landroidx/compose/runtime/Composer;I)V", "MutableDataClass", "Lcom/github/takahirom/jetpack_compose_stability_samples/MutableArticle;", "(Lcom/github/takahirom/jetpack_compose_stability_samples/MutableArticle;Landroidx/compose/runtime/Composer;I)V", "String", "name", "", "(Ljava/lang/String;Landroidx/compose/runtime/Composer;I)V", "app_release"}
)
public final class StabilityChecksKt {
   @Composable
   public static final void String(@NotNull final String name, @Nullable Composer $composer, final int $changed) {
      Intrinsics.checkNotNullParameter(name, "name");
      $composer = $composer.startRestartGroup(-1464156855);
      ComposerKt.sourceInformation($composer, "C(String)");
      int $dirty = $changed;
      if (($changed & 14) == 0) {
         $dirty = $changed | ($composer.changed(name) ? 4 : 2);
      }

      if (($dirty & 11 ^ 2) == 0 && $composer.getSkipping()) {
         $composer.skipToGroupEnd();
      } else {
         TextKt.Text-fLXpl1I("Hello " + name + '!', (Modifier)null, 0L, 0L, (FontStyle)null, (FontWeight)null, (FontFamily)null, 0L, (TextDecoration)null, (TextAlign)null, 0L, 0, false, 0, (Function1)null, (TextStyle)null, $composer, 0, 64, 65534);
      }

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

   }

   @Composable
   public static final void DataClass(@NotNull final Article article, @Nullable Composer $composer, final int $changed) {
      Intrinsics.checkNotNullParameter(article, "article");
      $composer = $composer.startRestartGroup(-924370680);
      ComposerKt.sourceInformation($composer, "C(DataClass)");
      int $dirty = $changed;
      if (($changed & 14) == 0) {
         $dirty = $changed | ($composer.changed(article) ? 4 : 2);
      }

      if (($dirty & 11 ^ 2) == 0 && $composer.getSkipping()) {
         $composer.skipToGroupEnd();
      } else {
         TextKt.Text-fLXpl1I(Intrinsics.stringPlus("Hello ", article), (Modifier)null, 0L, 0L, (FontStyle)null, (FontWeight)null, (FontFamily)null, 0L, (TextDecoration)null, (TextAlign)null, 0L, 0, false, 0, (Function1)null, (TextStyle)null, $composer, 0, 64, 65534);
      }

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

   }

   @Composable
   public static final void MutableDataClass(@NotNull final MutableArticle article, @Nullable Composer $composer, final int $changed) {
      Intrinsics.checkNotNullParameter(article, "article");
      $composer = $composer.startRestartGroup(-738357243);
      ComposerKt.sourceInformation($composer, "C(MutableDataClass)");
      TextKt.Text-fLXpl1I(Intrinsics.stringPlus("Hello ", article), (Modifier)null, 0L, 0L, (FontStyle)null, (FontWeight)null, (FontFamily)null, 0L, (TextDecoration)null, (TextAlign)null, 0L, 0, false, 0, (Function1)null, (TextStyle)null, $composer, 0, 64, 65534);
      ScopeUpdateScope var3 = $composer.endRestartGroup();
      if (var3 != null) {
         var3.updateScope((Function2)(new Function2() {
            public final void invoke(@Nullable Composer $composer, int $force) {
               StabilityChecksKt.MutableDataClass(article, $composer, $changed | 1);
            }
         }));
      }

   }

   @Composable
   public static final void HasThrowableDataClass(@NotNull final HasThrowableDataClass hasThrowableDataClass, @Nullable Composer $composer, final int $changed) {
      Intrinsics.checkNotNullParameter(hasThrowableDataClass, "hasThrowableDataClass");
      $composer = $composer.startRestartGroup(1081258760);
      ComposerKt.sourceInformation($composer, "C(HasThrowableDataClass)");
      TextKt.Text-fLXpl1I(Intrinsics.stringPlus("Hello ", hasThrowableDataClass), (Modifier)null, 0L, 0L, (FontStyle)null, (FontWeight)null, (FontFamily)null, 0L, (TextDecoration)null, (TextAlign)null, 0L, 0, false, 0, (Function1)null, (TextStyle)null, $composer, 0, 64, 65534);
      ScopeUpdateScope var3 = $composer.endRestartGroup();
      if (var3 != null) {
         var3.updateScope((Function2)(new Function2() {
            public final void invoke(@Nullable Composer $composer, int $force) {
               StabilityChecksKt.HasThrowableDataClass(hasThrowableDataClass, $composer, $changed | 1);
            }
         }));
      }

   }

   @Composable
   public static final void HasThrowableWithImmutableAnnotation(@NotNull final HasThrowableWithImmutableAnnotation hasThrowable, @Nullable Composer $composer, final int $changed) {
      Intrinsics.checkNotNullParameter(hasThrowable, "hasThrowable");
      $composer = $composer.startRestartGroup(-1872658300);
      ComposerKt.sourceInformation($composer, "C(HasThrowableWithImmutableAnnotation)");
      int $dirty = $changed;
      if (($changed & 14) == 0) {
         $dirty = $changed | ($composer.changed(hasThrowable) ? 4 : 2);
      }

      if (($dirty & 11 ^ 2) == 0 && $composer.getSkipping()) {
         $composer.skipToGroupEnd();
      } else {
         TextKt.Text-fLXpl1I(Intrinsics.stringPlus("Hello ", hasThrowable), (Modifier)null, 0L, 0L, (FontStyle)null, (FontWeight)null, (FontFamily)null, 0L, (TextDecoration)null, (TextAlign)null, 0L, 0, false, 0, (Function1)null, (TextStyle)null, $composer, 0, 64, 65534);
      }

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

   }

   @Composable
   public static final void HasThrowableWithStableAnnotation(@NotNull final HasThrowableWithStableAnnotation hasThrowable, @Nullable Composer $composer, final int $changed) {
      Intrinsics.checkNotNullParameter(hasThrowable, "hasThrowable");
      $composer = $composer.startRestartGroup(340271846);
      ComposerKt.sourceInformation($composer, "C(HasThrowableWithStableAnnotation)");
      int $dirty = $changed;
      if (($changed & 14) == 0) {
         $dirty = $changed | ($composer.changed(hasThrowable) ? 4 : 2);
      }

      if (($dirty & 11 ^ 2) == 0 && $composer.getSkipping()) {
         $composer.skipToGroupEnd();
      } else {
         TextKt.Text-fLXpl1I(Intrinsics.stringPlus("Hello ", hasThrowable), (Modifier)null, 0L, 0L, (FontStyle)null, (FontWeight)null, (FontFamily)null, 0L, (TextDecoration)null, (TextAlign)null, 0L, 0, false, 0, (Function1)null, (TextStyle)null, $composer, 0, 64, 65534);
      }

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

   }

   @Composable
   public static final void HasSimpleClass(@NotNull final HasSimpleClass simpleClass, @Nullable Composer $composer, final int $changed) {
      Intrinsics.checkNotNullParameter(simpleClass, "simpleClass");
      $composer = $composer.startRestartGroup(790858239);
      ComposerKt.sourceInformation($composer, "C(HasSimpleClass)");
      int $dirty = $changed;
      if (($changed & 14) == 0) {
         $dirty = $changed | ($composer.changed(simpleClass) ? 4 : 2);
      }

      if (($dirty & 11 ^ 2) == 0 && $composer.getSkipping()) {
         $composer.skipToGroupEnd();
      } else {
         TextKt.Text-fLXpl1I(Intrinsics.stringPlus("Hello ", simpleClass), (Modifier)null, 0L, 0L, (FontStyle)null, (FontWeight)null, (FontFamily)null, 0L, (TextDecoration)null, (TextAlign)null, 0L, 0, false, 0, (Function1)null, (TextStyle)null, $composer, 0, 64, 65534);
      }

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

   }

   @Composable
   public static final void HasHasThrowableDataClass(@NotNull final HasHasThrowableDataClass obj, @Nullable Composer $composer, final int $changed) {
      Intrinsics.checkNotNullParameter(obj, "obj");
      $composer = $composer.startRestartGroup(1108688073);
      ComposerKt.sourceInformation($composer, "C(HasHasThrowableDataClass)");
      TextKt.Text-fLXpl1I(Intrinsics.stringPlus("Hello ", obj), (Modifier)null, 0L, 0L, (FontStyle)null, (FontWeight)null, (FontFamily)null, 0L, (TextDecoration)null, (TextAlign)null, 0L, 0, false, 0, (Function1)null, (TextStyle)null, $composer, 0, 64, 65534);
      ScopeUpdateScope var3 = $composer.endRestartGroup();
      if (var3 != null) {
         var3.updateScope((Function2)(new Function2() {
            public final void invoke(@Nullable Composer $composer, int $force) {
               StabilityChecksKt.HasHasThrowableDataClass(obj, $composer, $changed | 1);
            }
         }));
      }

   }
}

キャッシュされたかの結果

試したもの キャッシュされた?
String
data class Article(val title: String)
data class MutableArticle(var title: String)
data class HasThrowableDataClass(val error:Throwable)
@Immutable data class HasThrowableWithImmutableAnnotation(val error:Throwable)
@Stable data class HasThrowableWithStableAnnotation(val error:Throwable)
data class HasSimpleClass(val simpleClass: SimpleClass)
class SimpleClass(val simpleText:String)
data class HasHasThrowableDataClass(val obj: HasThrowableDataClass)

valにしていて、変わらない想定でも、なにかinterfaceとかthrowableとかが入り込んじゃう場合は @Stableなどをつけたほうが良さそうに見えます。
StableImmutableの違いをこのバイトコード上では見つけることができませんでした。
changed()は以下のようになっていたので、両方equalsが使われていそうに見えました。

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