本文へスキップ
すべての記事

Androidのベースラインプロファイル: 2026年、コールドスタート時間を実際に左右するもの

Android Baseline Profilesの実践ガイド — Macrobenchmarkでの生成方法、Gradleへの組み込み、実際の効果測定、そしてコールドスタートを左右する他の3つの要素。

MFKAPPS 1 分で読めます

コールドスタートは、すべてのユーザーが体感するのに、ほとんど誰も測定しないパフォーマンス指標だ。アイコンをタップすると、一瞬の白い画面か引き伸ばされたスプラッシュ画面があり、それからアプリが現れる。500ミリ秒未満なら誰も気づかない。1秒を超えると、その後の画面がすべて瞬時に表示されたとしても「遅い」と受け取られる — 第一印象は残るもので、アプリアイコンはその第一印象だ。

その数値の大部分は、レイアウトやデータベースクエリとは関係がない。最適化する対象のプロファイルが何もない状態で、JITコンパイラがアプリの最も頻繁に実行されるコードパスに対してコールドで解釈実行(インタープリット)しているのが原因だ。Baseline Profilesはまさにそのウォームアップを飛ばすために存在し、一度の午後の作業で、その後インストールされるたびに配当を払い続ける数少ないAndroidパフォーマンスツールの一つだ。

Baseline Profileの実体

Baseline Profileは、アプリが最も一般的なフロー — コールドスタート、そしてプロファイリングを選んだ他の経路 — で触れるクラスとメソッドを列挙したテキストファイル(baseline-prof.txt)だ。これをAPK/AABに同梱すると、Android Runtime(ART)はそれらのメソッドを初回使用時にインタープリットやJITコンパイルするのではなく、あらかじめ(ahead-of-time)コンパイルするために利用する。

効果は限定的だが確実だ。コードそのものを高速化するわけではなく、起動時に実際に実行される特定のコードにかかる解釈のコストを取り除くだけだ。Playでは、Googleはこれまでプロファイルを持たなかったアプリで一般的にコールドスタート時間を20〜30%短縮すると報告している — 実際の効果は、起動パスのどれだけが実際にカバーされているかに完全に依存する。

Macrobenchmarkでプロファイルを生成する

プロファイルは実際に計測された(instrumented)実行から得る必要がある — 手書きでは意味のあるものにならない。androidx.benchmark.macroライブラリがこれを行う。

// build.gradle.kts (baselineprofileモジュール)
plugins {
    id("androidx.baselineprofile")
}

dependencies {
    baselineProfile(project(":app"))
}
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {
    @get:Rule
    val rule = BaselineProfileRule()

    @Test
    fun generate() = rule.collect(
        packageName = "com.mfk.hydrame",
        includeInStartupProfile = true,
    ) {
        pressHome()
        startActivityAndWait()
        // スプラッシュだけでなく、事前コンパイルする価値のあるフローを実行する。
        device.findObject(By.text("Log a glass")).click()
        device.waitForIdle()
    }
}

:app:generateBaselineProfileというGradleタスクを、実機かそこそこ高速なエミュレータで実行する(プロファイル生成にはroot相当の計測用ビルドが必要だが、プラグインがそれを処理してくれる)。このスクリプト化された実行中に触れたすべてのクラスを記録し、baseline-prof.txtapp/src/main/に書き出す。ここに書かれた内容は、AGPのbaseline profileプラグインがリリースビルド用に自動的に取り込む。

多くの人が省略してしまう部分: スプラッシュ画面だけでなく、それ以上をスクリプト化すること。実際のボトルネックがRoomから取得したデータでの最初のリスト描画や、ウィジェットの最初のプロバイダ呼び出しなら、その操作をcollectブロックに含めるべきだ。onCreate()だけをカバーするプロファイルは、実際の効果の大部分を取り逃す。

効果を正直に測定する

ストップウォッチと親指を信用してはいけない。別のmacrobenchmarkテストでStartupTimingMetricを使い、プロファイルなしのビルドとありのビルドを比較し、ノイズが両方向で平均化されるだけの十分な回数の中央値を比較する。

@Test
fun startupCompilationModes() {
    benchmarkRule.measureRepeated(
        packageName = "com.mfk.hydrame",
        metrics = listOf(StartupTimingMetric()),
        iterations = 10,
        startupMode = StartupMode.COLD,
        compilationMode = CompilationMode.Partial(baselineProfileMode = BaselineProfileMode.Require),
    ) {
        pressHome()
        startActivityAndWait()
    }
}

基準比較用の実行ではcompilationModeCompilationMode.None()に切り替える。10回の反復が妥当な下限だ — デバイスの熱状態やバックグラウンドプロセスは、3回の実行ではどちらの方向にも嘘をつくほどのノイズを加える。

Baseline Profilesが解決しないこと

これらは起動時の作業そのものを実際に減らすことの代わりにはならない。Application.onCreate()が3つのSDKを初期化し、データベースを積極的に開き、重い最初の画面をinflateしているなら、プロファイルはその作業をより速く解釈できるようにするだけで、作業量自体を減らすわけではない。効果はこれらと組み合わさって積み上がるものであり、代替にはならない。

  • 重要でない初期化を遅延させる。 最初のフレームに必要ないもの — アナリティクスSDK、必須ではないWorkManagerのスケジューリング、リモート設定の取得 — はonCreate()ではなく、UIが表示された後にバックグラウンドディスパッチャで実行する。
  • リリースビルドでのR8フルモード。 バイトコードを縮小・最適化することは、プロファイルの有無にかかわらず、ARTが最初に走査すべきコード量を減らすことを意味する。
  • 積極的なシングルトンではなく遅延シングルトン。 最初のDAOアクセス時に初期化されるby lazyのデータベースインスタンスは、最初の画面がまだそれを必要としていなければ、起動時のコストはゼロだ。

これはHydrameのホーム画面ウィジェットを作っているときに気づいたことだ。ウィジェット自体のコールドスタート(別のプロセスコンテキストで、頼れる温まったApplicationがない)は、プロファイルにウィジェットのコードパスが一切含まれていなかったため、アプリ本体よりも遅かった。Macrobenchmarkスクリプトに、アプリのメインアクティビティだけでなくウィジェット専用のシナリオを追加したことで、そのギャップの大部分が解消された。

この午後の作業が報われるとき

アプリがすでに高速であれば、Baseline Profileはあれば嬉しいおまけ程度だ。しかし、コールドスタートがPlay ConsoleのVitalsダッシュボードで黄色く表示されている唯一の指標なら、これは通常、費やせる中で最も投資対効果の高い午後の作業になる。アーキテクチャの変更もなく、機能的なリスクもなく、生成されたファイルとGradleプラグインだけだ。リリースごとに実際のクリティカルパスから一度生成し、再設計がプロファイルのカバー範囲を静かに超えてしまわないようMacrobenchmarkテストをCIに残し、コールドスタートの数値を他の回帰と同じように扱うこと — 一度直して忘れるものではなく、継続的に監視するものとして。