Jetpack Compose でのパフォーマンスに関する実践的な問題の解決

1. 始める前に

この Codelab では、Compose アプリの実行時のパフォーマンスを改善する方法を学びます。科学的なアプローチに基づいて、パフォーマンスの測定、デバッグ、パフォーマンスの最適化を行います。サンプルのアプリを例に、パフォーマンスに関する複数の問題を調査します。システム トレースを利用して、パフォーマンスの悪いランタイム コードを変更します。アプリには異なるタスクに対応した複数の画面があります。画面はそれぞれ異なる作りになっています。

  • 最初の画面は 2 列のリストで、画像のアイテムがあり、アイテムの上部にタグがついています。ここでは処理を重くしているコンポーザブルを最適化します。

8afabbbbbfc1d506.gif

  • 2 つ目と 3 つ目の画面には、頻繁に再コンポーズする状態が含まれます。ここでは不要な再コンポーズを削除して、パフォーマンスを最適化します。

f0ccf14d1c240032.gif 51dc23231ebd5f1a.gif

  • 最後の画面には不安定なアイテムが含まれています。ここではさまざまな手法でアイテムを安定させます。

127f2e4a2fc1a381.gif

前提条件

  • Compose アプリを構築する方法に関する知識
  • テストについての基礎知識または Macrobenchmark についての基礎知識

演習内容

必要なもの

2. セットアップする

使用を開始するには、以下のステップを実行してください。

  1. GitHub リポジトリのクローンを作成します。
$ git clone https://github.com/android/codelab-android-compose.git

または、リポジトリを ZIP ファイルとしてダウンロードすることもできます。

  1. PerformanceCodelab プロジェクトを開きます。このプロジェクトには以下のブランチが含まれます。
  • main: このプロジェクトのスターター コードが含まれています。これに変更を加えて Codelab を完了します。
  • end: この Codelab の解答コードが含まれています。

main から始めて、Codelab の手順に沿って自分のペースで進めることをおすすめします。

  1. 解答コードを確認する場合は、このコマンドを実行します。
$ git clone -b end https://github.com/android/codelab-android-compose.git

または、解答コードをダウンロードすることもできます。

オプション: この Codelab で使用されるシステム トレース

この Codelab では、システム トレースをキャプチャする複数のベンチマークを実行します。

ベンチマークを実行できない場合は、代わりに以下のシステム トレースをダウンロードできます。

3. パフォーマンスの問題を解決するためのアプローチ

アプリを使ってみて目で確認すれば、低速でパフォーマンスが悪い UI を見つけることができます。ただし、自身の想定に基づいてコードの修正を開始する前に、変更によって違いが生じたかどうか後でわかるようにするために、コードのパフォーマンスを測定する必要があります。

開発中、アプリの debuggable ビルドで、パフォーマンスが十分でないところを見つけて、その問題を解決したくなることがあるかもしれません。しかし、debuggable アプリのパフォーマンスは、ユーザーにとってのパフォーマンスを反映したものではありません。本当に問題があるのか、non-debuggable アプリで確認することが重要です。debuggable アプリでは、すべてのコードがランタイムによって解釈される必要があります。

Compose のパフォーマンスに関しては、特定の機能を実装するために守るべき厳格なルールはありません。早まって次のようなことをしないでください。

  • コードに潜む不安定なパラメータすべてを追って修正しないでください。
  • コンポーザブルの再コンポーズの原因となっているアニメーションを削除しないでください。
  • 直感のままに読みづらい最適化を行わないでください。

変更を加える際には、情報を得たうえで、利用可能なツールを使い、変更がパフォーマンスの問題に対処するものとなるようにする必要があります。

パフォーマンスの問題に対処する際には、次の科学的なアプローチに沿ってください:

  1. 最初のパフォーマンスを測定します。
  2. 観察して問題の原因を確認します。
  3. 観察結果に基づいてコードを変更します。
  4. 測定し、最初のパフォーマンスと比較します。
  5. 上記を繰り返します。

構造化された方法に沿わないと、一部の変更でパフォーマンスが改善し、別の変更でパフォーマンスが悪化して、結局同様の結果になる可能性があります。

Compose でのアプリのパフォーマンス向上について説明している、次の動画をご覧になることをおすすめします。パフォーマンスの問題の修正について触れているだけでなく、パフォーマンスを改善するためのヒントも紹介しています。

ベースライン プロファイルを生成する

パフォーマンスの問題の調査を始める前に、アプリのベースライン プロファイルを生成します。Android 6(API レベル 23)以降では、アプリのコードの実行に、実行時の解釈およびジャストインタイム(JIT)コンパイルと、インストール時の事前(AOT)コンパイルを利用します。実行時に解釈および JIT コンパイルしたコードは、AOT コンパイルしたコードよりも実行が遅くなりますが、ディスクとメモリの使用容量が少なくなります。すべてのコードを AOT コンパイルしないのはそのためです。

ベースライン プロファイルを実装すると、アプリの起動を 30% 高速化できます。また、また、実行時に JIT モードで実行するコードを 8 分の 1 に減らすことができます。次の画像では、Now in Android サンプルアプリの例を挙げています。

b51455a2ca65ea8.png

ベースライン プロファイルについて詳しくは、以下のリソースをご覧ください。

パフォーマンスを測定する

パフォーマンスを測定するために、Jetpack Macrobenchmark を使ってベンチマークをセットアップし、作成することをおすすめします。Macrobenchmark はインストルメンテーション テストです。アプリのパフォーマンスをモニタリングしながら、ユーザーがするようにアプリを操作します。テストコードを加えるためにアプリのコードを変更する必要がないため、パフォーマンスの正確な情報を入手できます。

この Codelab では、パフォーマンスの問題の修正に直接集中するために、すでにコードベースをセットアップし、ベンチマークを作成しました。プロジェクトで Macrobenchmark をセットアップして使用する方法がわからない場合は、以下のリソースをご覧ください。

Macrobenchmark では、以下のコンパイル モードのいずれかを選択できます。

  • None: コンパイル状態をリセットして、すべてを JIT モードで実行します。
  • Partial: ベースライン プロファイルを使ってアプリのプリコンパイルを行い、ウォームアップのイテレーションを行い、JIT モードで実行します。
  • Full: アプリのコード全体をプリコンパイルします。JIT モードで実行されるコードはありません。

この Codelab ではベンチマークに CompilationMode.Full() モードのみを使用します。これは、アプリのコンパイル状態には関心がなく、コードに加えた変更にのみ関心があるからです。このアプローチにより、コードを JIT モードで実行することによる分散を抑えることができます。カスタムのベースライン プロファイルを実装すると、こうした分散を減らすことができます。Full モードはアプリの起動に悪影響をおよぼす可能性がある点に注意してください。このモードはアプリの起動を測定するベンチマークには使用せず、実行時のパフォーマンスの改善を測定するベンチマークにのみ使用してください。

パフォーマンスの改善を終えて、ユーザーがアプリをインストールした際のパフォーマンスを確認する場合は、ベースライン プロファイルを使用する CompilationMode.Partial() モードを使用します。

次のセクションでは、トレースを読んでパフォーマンスの問題を発見する方法を学習します。

4. システム トレースでパフォーマンスを分析する

アプリの debuggable ビルドでは、コンポーズ数を含む Layout Inspector を使用して、再コンポーズが多く行われすぎているところをすぐに確認できます。

b7edfea340674732.gif

ただし、それは全体的なパフォーマンスの検査の一部にすぎません。近似値を測定できるだけで、コンポーザブルをレンダリングするために実際にかかる時間を測定できるわけではないからです。N 回再コンポーズしているものがあって、合計で 1 ミリ秒もかかっていない場合は、あまり問題ではありません。しかし、1 回か 2 回のコンポーズで 100 ミリ秒かかっているようなら問題があります。コンポーザブルが 1 回のみコンポーズされ、それでも長い時間がかかり、画面表示に影響することがしばしばあります。

パフォーマンスの問題を確実に検査し、アプリの動作を確認して、時間がかかりすぎているところがないかを把握するために、コンポジションのトレースを含むシステム トレースを利用できます。

システム トレースからは、アプリ内で起きたことのタイミングに関する情報を得られます。アプリにオーバーヘッドが発生することはないため、パフォーマンスへの悪影響を懸念することなくアプリの製品版で利用できます。

コンポジションのトレースをセットアップする

何かが再コンポーズされるタイミングや、Lazy レイアウトがアイテムをプリフェッチするタイミングなど、実行時のフェーズについての情報が Compose によって自動的に入力されます。ただし、この情報は、どのセクションに問題があるのかを把握するために十分なものではありません。コンポジションのトレースをセットアップすれば、情報量を増やすことができます。コンポジションのトレースからは、トレース中にコンポーズされた個々のコンポーザブルの名前がわかります。そこからパフォーマンスの問題について調査を始められます。カスタムの trace("label") セクションを多数追加する必要はありません。

コンポジションのトレースを有効にする手順は次のとおりです。

  1. :app モジュールに runtime-tracing 依存関係を追加します。
implementation("androidx.compose.runtime:runtime-tracing:1.0.0-beta01")

この時点で、Android Studio のプロファイラを使ってシステム トレースを記録でき、そうすればシステム トレースにはすべての情報が含まれます。しかし以降では、パフォーマンスの測定とシステム トレースの記録に Macrobenchmark を使用します。

  1. :measure モジュールに依存関係を追加して、Macrobenchmark でのコンポジションのトレースを有効にします。
implementation("androidx.tracing:tracing-perfetto:1.0.0")
implementation("androidx.tracing:tracing-perfetto-binary:1.0.0")
  1. :measure モジュールの build.gradle ファイルにインストルメンテーション引数 androidx.benchmark.fullTracing.enable=true を追加します。
defaultConfig {
    // ...
    testInstrumentationRunnerArguments["androidx.benchmark.fullTracing.enable"] = "true"
}

ターミナルからの使用方法など、コンポジションのトレースのセットアップ方法について詳しくは、ドキュメントをご覧ください。

Macrobenchmark で最初のパフォーマンスをキャプチャする

システム トレース ファイルを取得する方法は複数あります。たとえば、 Android Studio のプロファイラを使って記録したり、デバイスでキャプチャしたり、Macrobenchmark で記録したシステム トレースを取得したりすることができます。この Codelab では、Macrobenchmark ライブラリで取得したトレースを使用します。

このプロジェクトでは、:measure モジュールにベンチマークが含まれています。これを実行してパフォーマンスを測定できます。このプロジェクトのベンチマークは、Codelab の時間を節約するために、1 回のみ実行されるようにセットアップされています。実際のアプリでは、出力の分散が大きい場合は 10 回以上は実行することをおすすめします。

最初のパフォーマンスをキャプチャするには、AccelerateHeavyScreenBenchmark テストを使用します。このテストでは、最初のタスクの画面をスクロールします。手順は次のとおりです。

  1. AccelerateHeavyScreenBenchmark.kt ファイルを開きます。
  2. ベンチマーク クラスの横にあるガター アクションを使用してベンチマークを実行します。

e93fb1dc8a9edf4b.png

このベンチマークは、タスク 1 の画面をスクロールして、フレーム時間とカスタム

トレース セクションをキャプチャします。

8afabbbbbfc1d506.gif

ベンチマークが完了すると、Android Studio の出力ペインに結果が表示されます。

AccelerateHeavyScreenBenchmark_accelerateHeavyScreenCompilationFull
ImagePlaceholderCount               min  20.0,   median  20.0,   max  20.0
ImagePlaceholderMs                  min  22.9,   median  22.9,   max  22.9
ItemTagCount                        min  80.0,   median  80.0,   max  80.0
ItemTagMs                           min   3.2,   median   3.2,   max   3.2
PublishDate.registerReceiverCount   min   1.0,   median   1.0,   max   1.0
PublishDate.registerReceiverMs      min   1.9,   median   1.9,   max   1.9
frameDurationCpuMs                  P50    5.4,   P90    9.0,   P95   10.5,   P99   57.5
frameOverrunMs                      P50   -4.2,   P90   -3.5,   P95   -3.2,   P99   74.9
Traces: Iteration 0

出力の中で重要な指標を次に示します。

  • frameDurationCpuMs: フレームのレンダリングにかかった時間を示します。短いほどよい指標です。
  • frameOverrunMs: GPU の処理を含めて、フレームの上限を超えた時間を示します。負の値になっていれば、時間に余裕があったことになるので、良好な結果といえます。

ImagePlaceholderMs などの他の指標は、カスタム トレース セクションを使用します。トレース ファイル内のすべてのカスタム トレース セクションの合計所要時間を出力します。また、ImagePlaceholderCount 指標で、発生した回数を出力します。

これらの指標は、コードベースへの変更がパフォーマンスを改善したかどうかを理解するために役立ちます。

トレース ファイルを読む

Android Studio またはウェブベースのツール Perfetto を使用して、システム トレースを読むことができます。

Android Studio のプロファイラは、トレースを簡単に開いて、アプリのプロセスを表示できます。Perfetto ではより詳細な調査が可能で、強力な SQL クエリなどを利用して、システムで実行されるすべてのプロセスを調査できます。この Codelab では、Perfetto を使用してシステム トレースを分析します。

  1. Perfetto のウェブサイトにアクセスします。ダッシュボードが開きます。
  2. ホスト ファイル システムで Macrobenchmark によってキャプチャしたシステム トレースを選択します。これは [module]/outputs/connected_android_test_additional_output/benchmarkRelease/connected/[device]/ フォルダにあります。ベンチマークは、繰り返し実行されるたびに別のトレース ファイルを記録します。それぞれのファイルに、アプリでの同じ操作が含まれます。

51589f24d9da28be.png

  1. AccelerateHeavyScreenBenchmark_...iter000...perfetto-trace ファイルを Perfetto の UI にドラッグして、トレース ファイルが読み込まれるのを待ちます。
  2. オプション: ベンチマークを実行してトレース ファイルを生成できない場合は、こちらで準備したトレース ファイルをダウンロードして Perfetto にドラッグしてください。

547507cdf63ae73.gif

  1. アプリのプロセスである com.compose.performance を見つけます。通常は、フォアグラウンド アプリは、ハードウェア情報のレーンと、システムの数個のレーンの下にあります。
  2. アプリのプロセス名のプルダウン メニューを開きます。アプリで実行されているスレッドのリストが表示されます。次のステップでも使うので、トレース ファイルは開いたままにしておいてください。

582b71388fa7e8b.gif

アプリのパフォーマンスの問題を見つけるために、アプリのスレッドリストの上部にある、想定のタイムラインと実際のタイムラインを利用します。

1bd6170d6642427e.png

想定のタイムラインは、パフォーマンスの高い滑らかな UI を表示するために、どのくらいの時間でアプリがフレームを作成することをシステムが想定しているかを示します。この場合は 16 ミリ秒と 600 マイクロ秒(1,000 ミリ秒 / 60)です。実際のタイムラインは、GPU の処理を含めて、アプリによるフレームの作成に実際にかかった時間を示します。

異なる色が使われている場合、それぞれの意味は次のとおりです。

  • 緑のフレーム: フレームは時間どおりに作成されました。
  • 赤のフレーム: フレームの作成に想定以上の時間がかかり、ジャンクが発生しました。これらのフレームでの処理について調査し、パフォーマンスの問題を防ぐ必要があります。
  • 明るい緑のフレーム: フレームは時間制限内に作成されましたが、遅れがあったため、入力の遅延が発生しました。
  • 黄色のフレーム: フレームでジャンクが発生しましたが、原因はアプリではありませんでした。

UI を画面にレンダリングするところでは、デバイスが想定するフレームの作成時間よりも所要時間が短くなるような変更を加える必要があります。想定される時間は、従来は 16.6 ミリ秒でした。これは、ディスプレイのリフレッシュ レートが 60Hz だったためです。しかし、最新の Android デバイスでは、ディスプレイのリフレッシュ レートが 90Hz 以上であるため、この時間が 11 ミリ秒かそれ未満になる可能性があります。また、可変リフレッシュ レートが使用されている場合、フレームごとに数値が異なる可能性もあります。

たとえば、UI に 16 個のアイテムがある場合、フレームがスキップされないようにするには、1 個のアイテムを約 1 ミリ秒で作成する必要があります。一方で、動画プレーヤーなど、アイテムが 1 個しかない場合、ジャンクなしでコンポーズするために 16 ミリ秒まで使うことができます。

システム トレースの Call Chart について理解する

以下の画像は、再コンポーズについて示しているシステム トレースを簡略化した例です。

8f16db803ca19a7d.png

上から順に、それぞれのバーは、その下にあるバーの時間を合計したものです。バーは呼び出された関数のコードのセクションにも対応しています。Compose の呼び出しにより、コンポジション階層上で再コンポーズが行われます。最初のコンポーザブルは MaterialTheme です。MaterialTheme 内にはコンポジション ローカルがあり、テーマの情報を提供します。そこから HomeScreen コンポーザブルが呼び出されます。ホーム画面のコンポーザブルは、コンポジションの一部として MyImage コンポーザブルと MyButton コンポーザブルを呼び出します。

システム トレース内のギャップは、トレースされなかったコードが実行されたことによるものです。システム トレースは、トレースの対象としてマークされたコードのみを表示します。そのコードは、MyImage が呼び出された後、MyButton が呼び出される前に実行されました。ギャップの大きさの分だけ実行に時間がかかりました。

次のステップでは、前のステップで取得したトレースを分析します。

5. 処理に時間がかかっているコンポーザブルを高速化する

アプリのパフォーマンスを最適化する最初の作業として、処理に時間がかかっているコンポーザブル、またはメインスレッドで時間がかかっているタスクを見つけます。処理に時間がかかる要因はさまざまで、UI の複雑さや、UI をコンポーズするためにどのくらいの時間があるかなどによります。

フレームがドロップした場合、どのコンポーザブルに時間がかかりすぎているかを明らかにして、メインスレッドからオフロードするか、メインスレッドで実行する処理の一部をスキップして、処理を高速化します。

AccelerateHeavyScreenBenchmark テストで取得したトレースを分析する手順は次のとおりです。

  1. 前のステップで取得したシステム トレースを開きます。
  2. 最初の時間がかかっているフレームにズームインします。このフレームには、データが読み込まれた後の UI の初期化が含まれています。フレームの内容は、次の画像に似ています。

838787b87b14bbaf.png

トレースでは、1 つのフレーム内でさまざまなことが起きていることがわかります。Choreographer#doFrame セクションの下でその様子を確認できます。画像からわかるとおり、最も大きな処理が生じているのは、大きな画像を読み込む ImagePlaceholder セクションを含むコンポーザブルです。

メインスレッドで大きな画像を読み込まないようにする

ネットワークにある画像を非同期で読み込むには、CoilGlide などの便利なライブラリを使えばよいということは明らかかもしれませんが、アプリのローカルに大きな画像があり、それを表示する必要がある場合はどうすればいいでしょうか。

よく使われるリソースから画像を読み込むコンポーズ可能な関数 painterResource は、コンポジション中にメインスレッド上で画像を読み込みます。画像が大きいと、処理がメインスレッドをブロックする可能性があります。

ここでは、非同期の画像のプレースホルダに問題があることがわかります。painterResource コンポーザブルがプレースホルダ画像を読み込んでいて、読み込みに 23 ミリ秒かかっています。

c83d22c3870655a7.jpeg

この問題の解決方法はいくつかあります。一部を次に示します。

  • 画像を非同期で読み込む。
  • 画像を小さくして速く読み込めるようにする。
  • 必要なサイズに基づいてスケーリングする、ベクター型ドローアブルを使用する。

パフォーマンスの問題を解決するために、次の手順を実行します。

  1. AccelerateHeavyScreen.kt ファイルに移動します。
  2. 画像を読み込む imagePlaceholder() コンポーザブルを見つけます。プレースホルダ画像のサイズは 1,600 x 1,600 ピクセルとなっています。これは表示内容のわりに大きすぎます。

53b34f358f2ff74.jpeg

  1. ドローアブルを R.drawable.placeholder_vector に変更します。
@Composable
fun imagePlaceholder() =
    trace("ImagePlaceholder") { painterResource(R.drawable.placeholder_vector) }
  1. AccelerateHeavyScreenBenchmark テストを再実行します。アプリが再ビルドされ、システム トレースが再度取得されます。
  2. システム トレースを Perfetto のダッシュボードにドラッグします。

または、トレースをダウンロードします。

  1. ImagePlaceholder トレース セクションを検索します。ここでは、改善された部分が直接表示されます。

abac4ae93d599864.png

  1. ImagePlaceholder 関数がメインスレッドをあまりブロックしなくなったことを確認します。

8e76941fca0ae63c.jpeg

実際のアプリでは、プレースホルダ画像ではなく、アートワークが問題を引き起こしている可能性があります。その場合は別の解決策があります。その場合は、CoilrememberAsyncImage コンポーザブルを使用して、コンポーザブルを非同期で読み込むことができます。この解決策をとると、プレースホルダが読み込まれるまで、空のスペースが表示されます。アートワークのプレースホルダを用意する必要があることに注意してください。

パフォーマンスの問題は他にもあります。次のステップでそれらに対処します。

6. 重い処理をバックグラウンド スレッドにオフロードする

同じアイテムで他の問題を探すと、binder transaction というセクションが見つかります。ここではそれぞれに 1 ミリ秒ほどかかっています。

5c08376b3824f33a.png

binder transaction セクションでは、アプリのプロセスとシステムのプロセスの間で、プロセス間通信(IPC)が行われていたことがわかります。これはシステムから情報を取得する通常の手段の一つです。システム サービスの取得などに使われます。

こうしたトランザクションは、システムと通信する API の多くに含まれています。たとえば、getSystemService でシステム サービスを取得したり、ブロードキャスト レシーバを登録したり、ConnectivityManager をリクエストしたりする場合がこれに該当します。

これらのトランザクションは、何をリクエストしているのかについて、多くの情報を提供しません。コードを分析して、言及されている API の利用について調べる必要があります。それからカスタムの trace セクションを追加して、その部分に問題があることを確認してください。

バインダー トランザクションを改善する手順は次のとおりです。

  1. AccelerateHeavyScreen.kt ファイルを開きます。
  2. PublishedText コンポーザブルを見つけます。このコンポーザブルは現在のタイムゾーンで日時をフォーマットし、タイムゾーンの変更を追跡する BroadcastReceiver オブジェクトを登録します。currentTimeZone 状態変数があり、システムのデフォルトのタイムゾーンが初期値となっています。また、DisposableEffect は、タイムゾーンの変更のためのブロードキャスト レシーバを登録します。最後に、このコンポーザブルは、Text でフォーマットされた日時を示します。ブロードキャスト レシーバの登録を解除する方法が必要なので、DisposableEffect はこのシナリオには適しています。登録の解除は onDispose のラムダで行います。しかし、問題があるのは、DisposableEffect 内のコードがメインスレッドをブロックしていることです。
@Composable
fun PublishedText(published: Instant, modifier: Modifier = Modifier) {
    val context = LocalContext.current
    var currentTimeZone: TimeZone by remember { mutableStateOf(TimeZone.currentSystemDefault()) }

    DisposableEffect(Unit) {
        val receiver = object : BroadcastReceiver() {
            override fun onReceive(context: Context?, intent: Intent?) {
                currentTimeZone = TimeZone.currentSystemDefault()
            }
        }

        // TODO Codelab task: Wrap with a custom trace section
        context.registerReceiver(receiver, IntentFilter(Intent.ACTION_TIMEZONE_CHANGED))

        onDispose { context.unregisterReceiver(receiver) }
    }

    Text(
        text = published.format(currentTimeZone),
        style = MaterialTheme.typography.labelMedium,
        modifier = modifier
    )
}
  1. context.registerReceivertrace の呼び出しでラップして、これが本当にすべての binder transactions を引き起こしていることを確認します。
trace("PublishDate.registerReceiver") {
    context.registerReceiver(receiver, IntentFilter(Intent.ACTION_TIMEZONE_CHANGED))
}

一般的には、メインスレッドで長く実行されるコードが多くの問題を起こすのではなく、画面上で目に見えるアイテムごとに実行されるバインダー トランザクションが問題を引き起こします。画面上で目に見えるアイテムが 6 つあるとしたら、それらは最初のフレームでコンポーズされる必要があります。この呼び出しだけでも 12 ミリ秒かかります。これは 1 フレームの期限にほぼ等しい長さです。

この問題を解決するには、ブロードキャスト レシーバの登録を別のスレッドにオフロードする必要があります。コルーチンでそれが可能です。

  1. コンポーザブル ライフサイクルに結び付けられたスコープを取得します: val scope = rememberCoroutineScope()
  2. 作用内で、Dispatchers.Main ではないディスパッチャーでコルーチンを開始します。たとえば、この場合は Dispatchers.IO です。こうすれば、実際の状態 currentTimeZone がメインスレッドに保たれつつ、ブロードキャスト レシーバの登録がメインスレッドをブロックすることがなくなります。
val scope = rememberCoroutineScope()

DisposableEffect(Unit) {
    val receiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            currentTimeZone = TimeZone.currentSystemDefault()
        }
    }

    // launch the coroutine on Dispatchers.IO
    scope.launch(Dispatchers.IO) {
        trace("PublishDate.registerReceiver") {
            context.registerReceiver(receiver, IntentFilter(Intent.ACTION_TIMEZONE_CHANGED))
        }
    }

    onDispose { context.unregisterReceiver(receiver) }
}

この最適化にはもう 1 つステップがあります。ブロードキャスト レシーバは、リスト内の各アイテムに必要ではなく、1 つだけ必要です。ブロードキャスト レシーバをホイスティングすべきです。

ブロードキャスト レシーバをホイスティングして、タイムゾーンのパラメータをコンポーザブルのツリーの下流に渡すことができます。あるいは、ブロードキャスト レシーバは UI の多くの場所で使われるものではないので、コンポジション ローカルを使用できます。

この Codelab では、ブロードキャスト レシーバはコンポーザブルのツリーの一部として維持します。ただし、実際のアプリでは、ブロードキャスト レシーバをデータレイヤに分離して、UI コードに害をおよぼさないようにするとよい可能性があります。

  1. システムのデフォルトのタイムゾーンでコンポジション ローカルを定義します。
val LocalTimeZone = compositionLocalOf { TimeZone.currentSystemDefault() }
  1. content ラムダを受け取って現在のタイムゾーンを提供する ProvideCurrentTimeZone コンポーザブルを更新します。
@Composable
fun ProvideCurrentTimeZone(content: @Composable () -> Unit) {
    var currentTimeZone = TODO()

    CompositionLocalProvider(
        value = LocalTimeZone provides currentTimeZone,
        content = content,
    )
}
  1. DisposableEffectPublishedText コンポーザブルから新しいコンポーザブルに移してホイスティングし、currentTimeZone を状態と副作用で置き換えます。
@Composable
fun ProvideCurrentTimeZone(content: @Composable () -> Unit) {
    val context = LocalContext.current
    val scope = rememberCoroutineScope()
    var currentTimeZone: TimeZone by remember { mutableStateOf(TimeZone.currentSystemDefault()) }

    DisposableEffect(Unit) {
        val receiver = object : BroadcastReceiver() {
            override fun onReceive(context: Context?, intent: Intent?) {
                currentTimeZone = TimeZone.currentSystemDefault()
            }
        }

        scope.launch(Dispatchers.IO) {
            trace("PublishDate.registerReceiver") {
                context.registerReceiver(receiver, IntentFilter(Intent.ACTION_TIMEZONE_CHANGED))
            }
        }

        onDispose { context.unregisterReceiver(receiver) }
    }

    CompositionLocalProvider(
        value = LocalTimeZone provides currentTimeZone,
        content = content,
    )
}
  1. コンポジション ローカルを有効にするコンポーザブルを ProvideCurrentTimeZone でラップします。次のスニペットに示すように、AccelerateHeavyScreen 全体をラップできます。
@Composable
fun AccelerateHeavyScreen(items: List<HeavyItem>, modifier: Modifier = Modifier) {
    // TODO: Codelab task: Wrap this with timezone provider
    ProvideCurrentTimeZone {
        Box(
            modifier = modifier
                .fillMaxSize()
                .padding(24.dp)
        ) {
            ScreenContent(items = items)

            if (items.isEmpty()) {
                CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
            }
        }
    }
}
  1. PublishedText コンポーザブルを変更して、基本的なフォーマット機能だけを含むようにして、LocalTimeZone.current を通じてコンポジション ローカルの現在の値を読み取ります。
@Composable
fun PublishedText(published: Instant, modifier: Modifier = Modifier) {
    Text(
        text = published.format(LocalTimeZone.current),
        style = MaterialTheme.typography.labelMedium,
        modifier = modifier
    )
}
  1. ベンチマークを再実行します。アプリがビルドされます。

または、コードが修正されたシステム トレースをダウンロードすることもできます。

  1. トレース ファイルを Perfetto のダッシュボードにドラッグします。すべての binder transactions セクションがメインスレッドから移動しました。
  2. セクション名を検索します。前のステップと似た名前になっています。これは、コルーチン(DefaultDispatch)によって作成された他のスレッドのいずれかにあります:

87feee260f900a76.png

7. 不要なサブコンポジションを削除する

処理に時間のかかるコードをメインスレッドから移動させたので、そのコードによってコンポジションがブロックされることはなくなりました。まだ改善の余地があります。各アイテムの LazyRow コンポーザブルという形で、不要なオーバーヘッドを削除できます。

例では、次の画像でハイライト表示しているように、各アイテムにタグの行があります。

e821c86604d3e670.png

この行は LazyRow コンポーザブルで実装されています。そうすると記述しやすくなるからです。アイテムを LazyRow コンポーザブルに渡して、後の処理をそちらで行います。

@Composable
fun ItemTags(tags: List<String>, modifier: Modifier = Modifier) {
    // TODO: remove unnecessary lazy layout
    LazyRow(
        modifier = modifier
            .padding(4.dp)
            .fillMaxWidth(),
        horizontalArrangement = Arrangement.spacedBy(2.dp)
    ) {
        items(tags) { ItemTag(it) }
    }
}

問題は、Lazy レイアウトは、制限されたサイズ以上のアイテムがある場合には優れているものの、コストがかかり、遅延コンポジションが必要ない場合にはそのコストは余計なものになるという点です。

SubcomposeLayout コンポーザブルを使用する Lazy コンポーザブルは、その性質上、複数のまとまった処理を行います。最初はコンテナで、次のまとまった処理となるのは、画面に表示されているアイテムです。また、システム トレースには compose:lazylist:prefetch トレースがあります。ここで、追加のアイテムがビューポートに入っており、そのためプリフェッチされて用意されていることがわかります。

b3dc3662b5885a2e.jpeg

おおよそどのくらいの時間がかかるのかを判断するために、同じトレース ファイルを開きます。親アイテムから切り離されたセクションがあることがわかります。各アイテムは、コンポーズされている実際のアイテムと、タグアイテムで構成されています。こうすることで、各アイテムのコンポジション時間は約 2.5 ミリ秒となります。これに目に見えるアイテムの数をかけると、まとまった処理になります。

a204721c80497e0f.jpeg

この問題を解決する手順は次のとおりです。

  1. AccelerateHeavyScreen.kt ファイルに移動して、ItemTags コンポーザブルを見つけます。
  2. LazyRow の実装を Row コンポーザブルに変更して、次のスニペットに示すように、tags のリストに対して反復処理を行います。
@Composable
fun ItemTags(tags: List<String>, modifier: Modifier = Modifier) {
    Row(
        modifier = modifier
            .padding(4.dp)
            .fillMaxWidth()
        horizontalArrangement = Arrangement.spacedBy(2.dp)
    ) {
        tags.forEach { ItemTag(it) }
    }
}
  1. ベンチマークを再実行します。アプリもビルドされます。
  2. オプション: コードを修正したシステム トレースをダウンロードします。

  1. ItemTag セクションを見つけて、かかる時間が短くなったこと、同じ Compose:recompose ルート セクションを使用していることを確認します。

219cd2e961defd1.jpeg

SubcomposeLayout コンポーザブルを使用する他のコンテナ、たとえば BoxWithConstraints コンポーザブルで、同様の状況が生じることがあります。Compose:recompose セクション全体を通じてアイテムを作成できます。これはフレームのジャンクとして直接確認することはできないかもしれませんが、ユーザーが目で見てわかる可能性があります。可能であれば、各アイテムで BoxWithConstraints コンポーザブルを避けてください。このコンポーザブルは、利用できるスペースに基づいてさまざまな UI をコンポーズする場合以外では必要ない可能性があります。

このセクションでは、時間がかかりすぎているコンポジションを修正する方法を学びました。

8. 結果を最初のベンチマークと比較する

画面のパフォーマンスの最適化が完了しました。ベンチマークの結果を最初の結果と比べてみましょう。

  1. Android Studio の実行ペイン 667294bf641c8fc2.png で [Test History] を開きます。
  2. 変更をしていない段階の最初のベンチマークに関連する、最も古い実行を選択して、frameDurationCpuMsframeOverrunMs の指標を比較します。次の表のような結果が表示されます。

変更前

AccelerateHeavyScreenBenchmark_accelerateHeavyScreenCompilationFull
ImagePlaceholderCount               min  20.0,   median  20.0,   max  20.0
ImagePlaceholderMs                  min  22.9,   median  22.9,   max  22.9
ItemTagCount                        min  80.0,   median  80.0,   max  80.0
ItemTagMs                           min   3.2,   median   3.2,   max   3.2
PublishDate.registerReceiverCount   min   1.0,   median   1.0,   max   1.0
PublishDate.registerReceiverMs      min   1.9,   median   1.9,   max   1.9
frameDurationCpuMs                  P50    5.4,   P90    9.0,   P95   10.5,   P99   57.5
frameOverrunMs                      P50   -4.2,   P90   -3.5,   P95   -3.2,   P99   74.9
Traces: Iteration 0
  1. すべての最適化を含むベンチマークに関連する、最新の実行を選択します。次の表のような結果が表示されます。

変更後

AccelerateHeavyScreenBenchmark_accelerateHeavyScreenCompilationFull
ImagePlaceholderCount               min  20.0,   median  20.0,   max  20.0
ImagePlaceholderMs                  min   2.9,   median   2.9,   max   2.9
ItemTagCount                        min  80.0,   median  80.0,   max  80.0
ItemTagMs                           min   3.4,   median   3.4,   max   3.4
PublishDate.registerReceiverCount   min   1.0,   median   1.0,   max   1.0
PublishDate.registerReceiverMs      min   1.1,   median   1.1,   max   1.1
frameDurationCpuMs                  P50    4.3,   P90    7.7,   P95    8.8,   P99   33.1
frameOverrunMs                      P50  -11.4,   P90   -8.3,   P95   -7.3,   P99   41.8
Traces: Iteration 0

frameOverrunMs 行を選択すると、改善したすべてのパーセンタイルが表示されます。

P50

P90

P95

P99

変更前

-4.2

-3.5

-3.2

74.9

変更後

-11.4

-8.3

-7.3

41.8

改善

171%

137%

128%

44%

次のセクションでは、発生頻度が多すぎるコンポジションを修正する方法を学習します。

9. 不要な再コンポーズを防止する

Compose には 3 つのフェーズがあります。

  • コンポジション: コンポーザブルのツリーを構築して、何を表示するかを決定します。
  • レイアウト: ツリーを受け取って、画面のどこでコンポーザブルを表示するかを決定します。
  • 描画: コンポーザブルを画面に描画します。

通常、これらのフェーズの順序は変わりません。コンポジション、レイアウト、描画の順にデータが一方向に流れて、UI のフレームを作成します。

2147ae29192a1556.png

BoxWithConstraints、Lazy レイアウト(LazyColumnLazyVerticalGrid など)、SubcomposeLayout コンポーザブルに基づくすべてのレイアウトは例外で、子のコンポジションは親のレイアウトのフェーズに左右されます。

一般に、実行の負荷が最も高いのはコンポジションのフェーズです。処理量が最も多く、関係ない他のコンポーザブルの再コンポーズを引き起こす可能性もあるためです。

ほとんどのフレームには 3 つのフェーズすべてが含まれますが、やることがなければ、Compose はいずれかのフェーズをスキップすることもあります。この機能を利用して、アプリのパフォーマンスを向上させることができます。

ラムダ修飾子でコンポジション フェーズを遅延させる

コンポーズ可能な関数は、コンポジション フェーズで実行されます。コードを別のタイミングで実行できるようにするには、ラムダ関数として提供します。

手順は次のとおりです。

  1. PhasesComposeLogo.kt ファイルを開きます。
  2. アプリ内でタスク 2 の画面に移動します。画面の端でロゴが跳ね返る様子が表示されます。
  3. Layout Inspector を開いて、再コンポーズ数を調べます。再コンポーズ数が急激に増加していることがわかります。

a9e52e8ccf0d31c1.png

  1. オプション: PhasesComposeLogoBenchmark.kt ファイルを見つけて実行し、システム トレースを取得します。フレームごとに発生する、PhasesComposeLogo トレース セクションのコンポジションを確認します。再コンポーズは、トレースに同じ名前のセクションとして繰り返し表示されます。

4b6e72578c89b2c1.jpeg 7036a895a31138d3.png

  1. 必要があればプロファイラと Layout Inspector を閉じ、コードに戻ります。PhaseComposeLogo コンポーザブルが次のようになっています。
@Composable
fun PhasesComposeLogo() = trace("PhasesComposeLogo") {
    val logo = painterResource(id = R.drawable.compose_logo)
    var size by remember { mutableStateOf(IntSize.Zero) }
    val logoPosition by logoPosition(size = size, logoSize = logo.intrinsicSize)

    Box(
        modifier = Modifier
            .fillMaxSize()
            .onPlaced {
                size = it.size
            }
    ) {
        with(LocalDensity.current) {
            Image(
                painter = logo,
                contentDescription = "logo",
                modifier = Modifier.offset(logoPosition.x.toDp(), logoPosition.y.toDp())
            )
        }
    }
}

logoPosition コンポーザブルには、フレームごとに状態を変えるロジックが含まれます。内容は次のとおりです。

@Composable
fun logoPosition(size: IntSize, logoSize: Size): State<IntOffset> =
    produceState(initialValue = IntOffset.Zero, size, logoSize) {
        if (size == IntSize.Zero) {
            this.value = IntOffset.Zero
            return@produceState
        }

        var xDirection = 1
        var yDirection = 1

        while (true) {
            withFrameMillis {
                value += IntOffset(x = MOVE_SPEED * xDirection, y = MOVE_SPEED * yDirection)

                if (value.x <= 0 || value.x >= size.width - logoSize.width) {
                    xDirection *= -1
                }

                if (value.y <= 0 || value.y >= size.height - logoSize.height) {
                    yDirection *= -1
                }
            }
        }
    }

状態は、Modifier.offset(x.dp, y.dp) 修飾子によって PhasesComposeLogo コンポーザブル内で読み取られています。つまり、コンポジション内で読み取るということです。

アプリがこのアニメーションのすべてのフレームで再コンポーズを行っている理由は、この修飾子です。この場合は他の方法としてシンプルなものがあります。それはラムダベースの Offset 修飾子です。

  1. Image コンポーザブルを更新して、Modifier.offset 修飾子を使用するようにします。次のスニペットに示すとおり、この修飾子は IntOffset オブジェクトを返すラムダを受け入れます。
Image(
  painter = logo,
  contentDescription = "logo",
  modifier = Modifier.offset { IntOffset(logoPosition.x,  logoPosition.y) }
)
  1. アプリに戻り、Layout Inspector を確認します。アニメーションが再コンポーズを生成しなくなりました。

画面のレイアウトを調整するためだけに再コンポーズするべきではありません。これはフレームでのジャンクの発生につながります。スクロール中に発生する再コンポーズは、ほとんどが不要で、回避する必要があります。

その他のラムダ修飾子

ラムダ バージョンがあるのは Modifier.offset 修飾子だけではありません。以下の表には、毎回再コンポーズする一般的な修飾子を挙げています。頻繁に変化する状態の値を渡す際には、これらを遅延する代替手段に置き換えることができます。

一般的な修飾子

遅延する代替手段

.background(color)

.drawBehind { drawRect(color) }

.offset(0.dp, y)

.offset { IntOffset(0, y.roundToPx()) }

.alpha(a).rotate(r).scale(s)

.graphicsLayer { alpha = a; rotationZ = r; scaleX = s; scaleY = s}

10. カスタム レイアウトで Compose のフェーズを遅延させる

多くの場合、コンポジションの無効化を避けるには、ラムダベースの修飾子を使うのが最も簡単な方法です。しかし、必要な処理を行うラムダベースの修飾子がないこともあります。そのような場合は、カスタム レイアウトまたは Canvas コンポーザブルを直接実装して、描画フェーズに進むことができます。カスタム レイアウト内で行われる Compose の状態の読み取りによってレイアウトが無効になり、再コンポーズがスキップされます。一般的なガイドラインとしては、レイアウトまたはサイズのみを調整し、コンポーザブルを追加または削除しないのであれば、多くの場合でコンポジションを無効化せずにそれを行うことができます。

手順は次のとおりです。

  1. PhasesAnimatedShape.kt ファイルを開いてアプリを実行します。
  2. タスク 3 の画面に移動します。この画面には、ボタンをクリックするとサイズが変わる図形があります。サイズの値は animateDpAsState Compose アニメーション API で操作されます。

51dc23231ebd5f1a.gif

  1. Layout Inspector を開きます。
  2. [Toggle size] をクリックします。
  3. アニメーションのフレームごとに図形が再コンポーズすることを確認します。

63d597a98fca1133.png

MyShape コンポーザブルは size オブジェクトをパラメータとして受け取ります。これは読み取った状態です。つまり、size オブジェクトに変化があると、PhasesAnimatedShape コンポーザブル(最も近い再コンポーズ スコープ)が再コンポーズされ、続いて入力が変化した MyShape コンポーザブルが再コンポーズされます。

再コンポーズをスキップする手順は次のとおりです。

  1. size パラメータをラムダ関数に変更して、サイズの変更が MyShape コンポーザブルを直接再コンポーズしないようにします。
@Composable
fun MyShape(
    size: () -> Dp,
    modifier: Modifier = Modifier
) {
  // ...
  1. PhasesAnimatedShape コンポーザブルのコールサイトを更新して、ラムダ関数を使用するようにします。
MyShape(size = { size }, modifier = Modifier.align(Alignment.Center))

size パラメータをラムダに変更すると、状態の読み取りが遅延されます。状態の読み取りはラムダが呼び出されたときに行われるようになります。

  1. MyShape コンポーザブルの本体を次のように変更します。
Box(
    modifier = modifier
        .background(color = Purple80, shape = CircleShape)
        .layout { measurable, _ ->
            val sizePx = size()
                .roundToPx()
                .coerceAtLeast(0)

            val constraints = Constraints.fixed(
                width = sizePx,
                height = sizePx,
            )

            val placeable = measurable.measure(constraints)
            layout(sizePx, sizePx) {
                placeable.place(0, 0)
            }
        }
)

layout 修飾子の最初の行でラムダを評価します。size ラムダが呼び出されたことがわかります。これは layout 修飾子内にあるので、レイアウトのみが無効になり、コンポジションは無効になりません。

  1. アプリを再実行して タスク 3 の画面に移動し、Layout Inspector を開きます。
  2. [Toggle Size] をクリックして、図形のサイズが以前と同じように変化し、MyShape コンポーザブルが再コンポーズしないことを確認します。

11. 安定したクラスで再コンポーズを防止する

Compose は、すべての入力パラメータが安定していて、前回のコンポジションから変化していない場合は、コンポーザブルの実行をスキップできるコードを生成します。タイプが安定しているということは、不変であるか、再コンポーズの間に値が変化したかどうかを Compose エンジンが把握できるということです。

Compose エンジンでコンポーザブルが安定しているかどうかがわからない場合は、Compose エンジンはコンポーザブルを不安定なものとして扱い、再コンポーズをスキップするコードロジックを生成しません。この場合、コンポーザブルは毎回再コンポーズします。これが発生するのは、クラスがプリミティブ型でなく、以下のいずれかに当てはまる場合です。

  • クラスが可変の場合。たとえば、可変のプロパティがある場合です。
  • Compose を使用しない Gradle モジュール内でクラスが定義されている場合。Compose コンパイラに依存関係がありません。
  • クラスが不安定なプロパティを含む場合。

この動作はパフォーマンスの問題を引き起こすこともあり、望ましくない場合もあります。動作を変更する方法は次のとおりです。

  • 強いスキップモードを有効にします。
  • パラメータに @Immutable アノテーションまたは @Stable アノテーションを加えます。
  • クラスを安定性の構成ファイルに追加します。

安定性の詳細については、ドキュメントをご覧ください。

このタスクにはアイテムのリストがあり、アイテムの追加、削除、チェックが可能です。再コンポーズが不要なところでアイテムが再コンポーズしないようにする必要があります。毎回再作成されるアイテムと、そうでないアイテムの 2 種類があります。

ここでは、毎回再作成されるアイテムは、データをローカルのデータベース(RoomsqlDelight など)やリモートのデータソース(API リクエストや Firestore エンティティなど)から取得して、変更があるたびにオブジェクトの新しいインスタンスを返す、実際のユースケースをシミュレートしたものです。

複数のコンポーザブルに Modifier.recomposeHighlighter() 修飾子が付けられています。これは GitHub リポジトリで確認できます。この修飾子は、コンポーザブルが再コンポーズされる際には常に枠線を表示します。Layout Inspector の一時的な代替策として利用できます。

127f2e4a2fc1a381.gif

強いスキップモードを有効にする

Jetpack Compose コンパイラ 1.5.4 以降には、強いスキップモードを有効にするオプションがあります。このモードを有効にすると、不安定なパラメータがあるコンポーザブルでも、スキップを行うコードを生成できます。このモードを使用すると、プロジェクト内のスキップできないコンポーザブルの量を大幅に減らすことができます。これはコードを変更せずにパフォーマンスを改善することにつながります。

不安定なパラメータについては、スキップを行うロジックでは、インスタンスの等価性が比較されます。つまり、以前に同じインスタンスがコンポーザブルに渡されたことがあれば、そのパラメータはスキップされます。対照的に、安定したパラメータについては、構造の等価性を使用(Object.equals() メソッドを呼び出し)して、スキップを行うロジックを決定します。

強いスキップモードでは、ロジックをスキップするだけでなく、コンポーズ可能な関数内にあるラムダを自動的に記憶します。つまり、ViewModel メソッドを呼び出すラムダ関数などについて、remember を呼び出してラップする必要はありません。

強いスキップモードは Gradle モジュール ベースで有効にできます。

有効にする手順は次のとおりです。

  1. アプリの build.gradle.kts ファイルを開きます。
  2. composeCompiler ブロックを更新して、以下のスニペットにします。
composeCompiler {
    // Not required in Kotlin 2.0 final release
    suppressKotlinVersionCompatibilityCheck = "2.0.0-RC1"

    // This settings enables strong-skipping mode for all module in this project.
    // As an effect, Compose can skip a composable even if it's unstable by comparing it's instance equality (===).
    enableExperimentalStrongSkippingMode = true
}

これにより、Gradle モジュールに experimentalStrongSkipping コンパイラ引数が追加されます。

  1. b8a9619d159a7d8e.png [Sync Project with Gradle Files] をクリックします。
  2. プロジェクトを再ビルドします。
  3. タスク 5 の画面を開いて、構造の等価性を使うアイテムに EQU アイコンがマークされ、アイテムのリストを操作しても再コンポーズしないことを確認します。

1de2fd2c42a1f04f.gif

ただし、他のタイプのアイテムは依然として再コンポーズします。この問題は次のステップで修正します。

安定性をアノテーションで修正する

前述のとおり、強いスキップモードを有効にすると、パラメータが以前のコンポジションと同じインスタンスを持っている場合、コンポーザブルが実行をスキップします。ただし、不安定なクラスの新しいインスタンスに対するすべての変更が与えられる場合は、これに該当しません。

この例では、不安定な LocalDateTime プロパティを含む StabilityItem クラスは不安定です。

このクラスの安定性を修正する手順は次のとおりです。

  1. StabilityViewModel.kt ファイルに移動します。
  2. StabilityItem クラスを見つけて、@Immutable アノテーションを付けます。
// TODO Codelab task: make this class Stable
@Immutable
data class StabilityItem(
    val id: Int,
    val type: StabilityItemType,
    val name: String,
    val checked: Boolean,
    val created: LocalDateTime
)
  1. アプリを再ビルドします。
  2. タスク 5 の画面に移動して、再コンポーズされるリストアイテムがないことを確認します。

938aad77b78f7590.gif

このクラスでは、前回のコンポジションから変更があったか、構造の等価性を使ってチェックするようになりました。そのため、再コンポーズは行いません。

これまでの修正を行っても、最新の変更の日付を参照するコンポーザブルがまだあるので、それらは再コンポーズします。

安定性を構成ファイルで修正する

以前のアプローチは、自分のコードベースの一部であるクラスにはうまく機能します。しかし、サードパーティのライブラリのクラスや、標準ライブラリのクラスなど、自分のものではないクラスは編集できません。

安定性構成ファイルを有効にできます。これはクラスを受け取り(ワイルドカードを使用可能)、それを安定したものとして扱います。

有効にする手順は次のとおりです。

  1. アプリの build.gradle.kts ファイルに移動します。
  2. composeCompiler ブロックに stabilityConfigurationFile オプションを追加します。
composeCompiler {
    ...

    stabilityConfigurationFile = project.rootDir.resolve("stability_config.conf")
}
  1. プロジェクトを Gradle ファイルと同期します。
  2. このプロジェクトのルートフォルダで、README.md ファイルのそばにある stability_config.conf ファイルを開きます。
  3. 次を追加します。
// TODO Codelab task: Make a java.time.LocalDate class stable.
java.time.LocalDate
  1. アプリを再ビルドします。日付が変わらない場合、LocalDateTime クラスが Latest change was YYYY-MM-DD コンポーザブルを再コンポーズさせることはありません。

332ab0b2c91617f2.gif

アプリではパターンを含むようにファイルを拡張できます。こうすれば、安定したものとして扱う必要があるすべてのクラスを記述する必要はありません。この場合は java.time.* ワイルドカードを使用できます。これにより、InstantLocalDateTimeZoneId など、パッケージ内のすべてのクラスと、java time のその他のクラスを安定したものとして扱います。

これまでのステップを通じて、画面上で再コンポーズするものは、追加されたアイテムや操作されたアイテムだけになりました。こうした再コンポーズは期待される動作です。

12. 完了

これで、Compose アプリのパフォーマンスの最適化が完了しました。ここで紹介したのは、アプリで発生する可能性があるパフォーマンスの問題のごく一部ですが、他の問題を探す方法や、それらを修正する方法についても学習しました。

次のステップ

アプリのベースライン プロファイルをまだ生成していない場合は、生成することを強くおすすめします。

ベースライン プロファイルを使用してアプリのパフォーマンスを改善するの Codelab をご利用ください。ベンチマークのセットアップについて詳しくは、Macrobenchmark でアプリのパフォーマンスを検査するの Codelab をご覧ください。

詳細