はじめに
こんにちは、ブランドソリューション開発本部フロントエンド部WEAR Androidブロックの安土琢朗です。普段はファッションコーディネートアプリWEARのAndroidアプリを開発しています。
WEARではすでにXMLで書かれたレイアウトをJetpack Composeにリファクタリングする作業を進めています。作業を進める中で、Jetpack ComposeのLazyColumn利用箇所でスクロールが以前よりスムーズに動かない、初回起動時にスクロールが遅いなどのパフォーマンス問題に直面しました。
本投稿では、最適なパフォーマンスを実現する方法の1つであるベースラインプロファイルの導入の仕方について説明します。
ベースラインプロファイルとは
ベースラインプロファイルはAndroid Runtime(ART)がプリコンパイルする時に使うクラスやメソッドをリスト化してあるものです。ベースラインプロファイルを使うことで起動時間とジャンクの削減、全体的なランタイムパフォーマンスの向上ができます。
それでは実際に導入をみていきましょう。
導入方法
benchmarkモジュールをアプリに追加する
まずAndroid Studioの"Create New Module" でベンチマーク用のモジュールを追加するテンプレートが用意されているのでモジュールを追加します。
ベースライン プロファイルの難読化を無効にする
ベンチマークに対して難読化を無効にする必要があります。appモジュール内にbenchmark-rules.proというファイルを作成します。
- benchmark-rules.pro
# Disables obfuscation for benchmark builds. -dontobfuscate
次に、appモジュールのbuild.gradle.ktsでbenchmark buildTypeを変更し、作成したファイルを追加します。
- app/build.gradle.kts
buildTypes { create("benchmark") { signingConfig = signingConfigs.getByName("debug") matchingFallbacks += listOf("release") proguardFiles("benchmark-rules.pro") } }
ベースラインプロファイルのジェネレータを作成する
ベースラインプロファイルを生成するために、benchmarkモジュールにテストクラスBaselineProfileGeneratorを作成します。
- BaselineProfileGenerator.kt
@ExperimentalBaselineProfilesApi class BaselineProfileGenerator { companion object { const val PACKAGE_NAME = "com.example.myapplication" } @get:Rule val baselineProfileRule = BaselineProfileRule() @Test fun generate() = baselineProfileRule.collectBaselineProfile(PACKAGE_NAME) { startActivityAndWait() startApplication() scrollScreen() } // アプリを起動する関数 private fun MacrobenchmarkScope.startApplication() { pressHome() startActivityAndWait() device.wait(Until.hasObject(By.pkg(device.launcherPackageName).depth(0)), 5_000) val suggestions = device.findObject(By.res("suggestions")) val searchCondition = Until.hasObject(By.res("coordinateTop")) suggestions.wait(searchCondition, 5_000) } // リストをスクロールする関数 private fun MacrobenchmarkScope.scrollScreen() { val suggestions = device.findObject(By.res("suggestions")) suggestions.setGestureMargin(device.displayWidth / 5) suggestions.fling(Direction.DOWN) device.waitForIdle() } }
startApplication() 関数では次の3点を行います。
アプリの状態が再起動になったことを確認。
デフォルトのアクティビティを開始し、最初のフレームがレンダリングされるのを待つ。
コンテンツが読み込まれてレンダリングされ、ユーザー操作が可能になるまで待つ。
scrollScreen()関数では次の2点を行います。
LazyColumnのmodifierにtestTagを追加してtagを元にスクロールできるUI要素を見つける。
リストをスクロールする。
ベースラインプロファイルのジェネレータを実行する
ベースラインプロファイルを生成するには、root権限のあるAndroid9(API 28)以上のデバイスを使用する必要があります。benchmarkモジュールのbuild.gradle.ktsファイルで、Gradleで管理されているデバイスを定義します。
- benchmark/build.gradle.kts
testOptions { managedDevices { devices { create<ManagedVirtualDevice>("pixel2Api31") { device = "Pixel 2" apiLevel = 31 systemImageSource = "aosp" } } } }
生成されたベースラインプロファイルをアプリに適用する
テストが正常に終了したら、アプリにベースラインプロファイルを適用します。生成されたファイルは/benchmark/build/outputs/
の中のmanaged_device_android_test_additional_output/
フォルダ内にあります。そのファイルをappモジュールにbaseline-prof.txtでコピーします。
続いて、appモジュールにprofileinstallerの依存関係を追加します。
- app/build.gradle.kts
dependencies { implementation("androidx.profileinstaller:profileinstaller:1.2.0") }
ここまでがベースラインプロファイルを生成してアプリに適用するまでの手順です。
次に実際にアプリのパフォーマンスについて測定してみます。使うライブラリはMacrobenchmarkです。
Macrobenchmarkとは
「Jetpack Macrobenchmark」は、パフォーマンスを測定するために導入されます。起動やUIの操作、アニメーションなどのパフォーマンスを測定できます。このライブラリを使用すると、以下のことができます。
アプリを複数回測定し、起動パターンやスクロール速度で測定できます。
複数のテスト実行結果を平均化し、パフォーマンスのばらつきを抑えることができます。
アプリのコンパイル状態を制御することで、パフォーマンスの安定性に影響を与える要因を制御できます。
Google Playストアで行われるインストール時の最適化をローカルで再現して、実際のパフォーマンスを確認できます。
Macrobenchmark導入方法
Macrobenchmarkのライブラリを追加
まずbenchmarkモジュールにMacrobenchmarkのライブラリを追加します。
- benchmark/build.gradle.kts
dependencies { implementation "androidx.benchmark:benchmark-macro-junit4:1.1.1" }
アプリのパフォーマンスを測定する
アプリのパフォーマンスを測定するために以下のテストクラスを作成します。
- StartupBenchmark.kt
class StartupBenchmark { companion object { const val PACKAGE_NAME = "com.example.myapplication" } @get:Rule val benchmarkRule = MacrobenchmarkRule() @Test fun startupCompilationNone() = startup(CompilationMode.None()) @Test fun startupCompilationPartial() = startup(CompilationMode.Partial()) @Test fun startupCompilationFull() = startup(CompilationMode.Full()) private fun startup(compilationMode: CompilationMode) = benchmarkRule.measureRepeated( packageName = PACKAGE_NAME, metrics = listOf(StartupTimingMetric()), iterations = 5, compilationMode = compilationMode, startupMode = StartupMode.COLD ) { pressHome() startActivityAndWait() device.wait(Until.hasObject(By.pkg(device.launcherPackageName).depth(0)), 5_000) val suggestions = device.findObject(By.res("suggestions")) val searchCondition = Until.hasObject(By.res("coordinateTop")) suggestions.wait(searchCondition, 5_000) suggestions.setGestureMargin(device.displayWidth / 5) suggestions.fling(Direction.DOWN) device.waitForIdle() } }
benchmarkRule.measureRepeated関数に以下パラメータを指定します。
packageNameは測定するアプリのパッケージ名を指定します。
metricsは測定する情報の種類を指定します。
iterationsはベンチマークを繰り返す回数を指定します。回数が多いほど結果は正確になります。
startupModeはアプリの起動方法を指定します。指定可能な値はCOLD、WARM、HOTです。
measureBlockは測定するアクション(例えば、アクティビティの開始、ボタンのクリック、スクロール、スワイプなど)を定義します。
CompilationModeを使って異なる3つのテスト関数を追加します。それぞれのテストの役割は以下です。
CompolationMode.Noneはアプリのコードをプリコンパイルしません。
CompolationMode.Partialはベンチマークを実行する前にベースラインプロファイルを読み込みプロファイルで指定されたクラス、関数をプリコンパイルします。
CompolationMode.Fullはアプリ全体をプリコンパイルします。
測定結果
次の表に中央値を示します。
timeToInitialDisplay(ms) | |
---|---|
None | 394.5 |
Full | 454.1 |
Partial | 348.7 |
Noneモードの場合は全くプリコンパイルしないのでパフォーマンスが悪くなりPartialより数値は大きくなります。
Fullモードの場合はコード全体をプリコンパイルします。なので起動時にディスクの読み込みにかかるコストと命令キャッシュの負荷が増加するため、パフォーマンスは1番低くなります。
Partialモードの場合はベースラインプロファイルを使用していてユーザーがよく使うコードを優先的にプリコンパイルし、あまり重要でないコードは一時的に読み込まないようにしています。結果としてベースラインプロファイルを使用しているPartialはパフォーマンスが高い結果となりました。
まとめ
今回ベースラインプロファイルを導入することによってパフォーマンスを改善できました。ただライブラリ更新や実装が変わった時にベースラインプロファイルを都度作成する必要があるので、ワークフローを作成するようにした方が良いです。またMacrobenchmarkを使って実際にパフォーマンスを測定でき、数値で比較できました。今後その数値を元にパフォーマンスをより向上させたいと思います。
最後に
ZOZOではAndroidエンジニアを募集しています。ご興味のある方は下記リンクからぜひご応募ください。