はじめに
本記事では、アプリ起動時間の短縮に絞ってパフォーマンスチューニングを行った内容をまとめています。
MacroBenchmarkを用いたパフォーマンスの計測、Profilerを用いたボトルネックの特定、それを受けての改善というプロセスを以下で紹介しています。
対象のアプリ
今回パフォーマンスチューニングを行うアプリは個人開発で制作した積読解消アプリ(ReadTrack)となっています。こちらのアプリはユーザーが持っている本を検索して登録し、その状態(未読/読書中/読了)や読了ページ数などを管理するアプリとなっています。
パフォーマンス改善前の状態
まずは、Macrobenchmarkというパフォーマンス測定用のライブラリを用いてパフォーマンス改善前のアプリ起動時間を計測します。
Macrobenchmarkを用いてアプリを実際に起動させ、起動時間を測定するテストコードの例を以下に示します。
@RunWith(AndroidJUnit4::class)
class StartupBenchmark {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@Test
fun startupColdCompilationNone() = startup(StartupMode.COLD, CompilationMode.None())
@Test
fun startupColdCompilationDefault() = startup(StartupMode.COLD, CompilationMode.DEFAULT)
private fun startup(startupMode: StartupMode, compilationMode: CompilationMode) =
benchmarkRule.measureRepeated(
packageName = TARGET_PACKAGE,
metrics = listOf(StartupTimingMetric()),
iterations = 5, // 5回計測して平均を取る
startupMode = startupMode,
compilationMode = compilationMode,
) {
pressHome() // ホーム画面に戻る
startActivityAndWait() // アプリを起動し、初期描画が完了するまで待機
}
private companion object {
const val TARGET_PACKAGE = "com.belltree.readtrack"
}
}
このテストクラスにおいては以下の2つのテストケースが定義されています。
| テスト | 想定 |
|---|---|
| startupColdCompilationNone | インストール直後のまっさらな状態 |
| startupColdCompilationDefault | ユーザーが普段使っている標準的な状態 |
実際に起動時間を計測した結果は以下の通りです。
| テスト | min | median | max |
|---|---|---|---|
| startupColdCompilationNone | 650.1ms | 722.6ms | 780.9ms |
| startupColdCompilationDefault | 548.5ms | 657.9ms | 787.6ms |
Profilerによる分析
次は、Profilerを用いてアプリ起動の中でどんな処理がボトルネックになっているのかを調べます。
Profilerを用いたTraceの記録は以下のような手順で行えます。
- Android Studio で app を Profile 実行 → 「profileable (low overhead)」を選ぶ
- Profiler パネルでタスク Find CPU Hotspots (Callstack Sample) を選ぶ
- 開始位置を Process start にして Start → 自動で再起動され、起動がトレースに入る。起動が終わったら Stop
今回は、Application.onCreate() の前に何が動いているかに着目してTraceを分析します。
TraceファイルをClaudeに解析してもらったところ、以下の画像のような結果が得られました。
onCreateが呼ばれるより前の区間の内訳を見ると、WorkManagerのContentProvider自動初期化(Roomデータベースの構築)が約50msを占めていることが分かりました。
修正内容
Profilerによる解析を通して、WorkManagerの初期化がボトルネックになっていることがわかりました。
そこで、WorkManagerをWorkManager.getInstance()が初めて呼ばれた時点で遅延初期化される仕組みに修正しました。具体的には、Configuration.Provider実装による遅延初期化の仕組みが公式に用意されており、これに切り替えることでこのコストを起動経路から外すことにしました。
修正1:AndroidManifest.xml で自動初期化を無効化
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<!-- WorkManagerのデフォルト自動初期化を削除 -->
<remove android:name="androidx.work.WorkManagerInitializer" />
</provider>
修正2:Applicationクラスで Configuration.Provider を実装
@HiltAndroidApp
class ReadTrackApplication : Application(), Configuration.Provider {
lateinit var appContainer: AppContainer
// 初回 WorkManager.getInstance() 呼び出し時にこの設定で遅延初期化される
override val workManagerConfiguration: Configuration
get() = Configuration.Builder().build()
override fun onCreate() {
super.onCreate()
appContainer = AppDataContainer(this)
createNotificationChannel(this)
// WorkManagerのDB構築等をメインスレッドから逃がすためバックグラウンドで初期化
CoroutineScope(Dispatchers.IO).launch {
scheduleBookUpdateCheck(this@ReadTrackApplication)
}
}
}
修正後の起動時間
修正後の起動時間は以下の通りです。
| テスト | min | median | max |
|---|---|---|---|
| startupColdCompilationNone | 558.7ms | 603.9ms | 742.9ms |
| startupColdCompilationDefault | 484.8ms | 555.5ms | 630.9ms |
中央値(median)ベースで比較したところ、以下のような成果が得られました。
- CompilationNone (事前コンパイルなし / 初回起動時の挙動に近い状態)
- 722.6 ms ➔ 603.9 ms (約 16.4% の高速化)
- CompilationDefault (通常のプロファイル適用状態)
- 657.9 ms ➔ 555.5 ms (約 15.6% の高速化)
まとめ
本記事では、自作アプリの起動時間を短縮するために行ったパフォーマンスチューニングのプロセスをご紹介しました。
この記事が皆さんのAndroidアプリ開発のお役に立てば幸いです。
