16
6

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-08-27

どういうときに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
        }
    }
16
6
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
16
6