Skip to content

LeakCanary は、Android アプリのメモリリーク検出に広く使われるライブラリである。

導入は依存関係の追加だけで足りる。

kotlin
dependencies {
  // debugImplementation because LeakCanary should only run in debug builds.
  debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")
}

デバッグビルドを端末へ入れると、アプリと並んでカナリアのランチャが現れ、リークを検知するとトーストや通知で知らせる。

LeakCanary dumping toast

アプリケーション側に初期化コードを書かなくても動作に介入できる理由が、本記事の主題である。

メモリリークと監視の起点

メモリリークとは、本来回収可能になるべきオブジェクトが、参照チェーンによって GC 対象から外れたまま残ることである。View ベースの Android アプリでは、画面単位のライフサイクルを持つ Activity が代表的な監視対象になる。

リーク検出には、コンポーネントの破棄を知る手段が要る。Activity は公開 API としてライフサイクル監視を提供する。したがってソース上には Application.registerActivityLifecycleCallbacks のような登録箇所が見えるはずである。

LeakCanary のリポジトリripgrep で辿ると、その期待どおりの経路が現れる。

ripgrep による ActivityWatcher 周辺の検索結果

ActivityWatcherFragmentAndViewModelWatcher が中心的な監視実装である。呼び出し関係を追うと、おおよそ次の流れに整理できる。

LeakCanary の監視インストールから検出までの流れ

監視そのものは AppWatcher と各 Watcher が担う。問題は、それらのインストールがどの時点で、誰の手によって走るのかである。

自動インストールの古典形:ContentProvider

Android では、プロセス起動時に ApplicationonCreate より前に ContentProvideronCreate が呼ばれる。ライブラリ作者は、自前の ContentProvider をライブラリの AndroidManifest.xml に宣言し、マージマニフェスト経由でホストアプリへ持ち込むことで、アプリ側の一行も借りずに初期化できる。

LeakCanary の標準アーティファクト leakcanary-android が依存する object-watcher 側では、次のようなインストーラがマニフェストに載る(現行ソースでは MainProcessAppWatcherInstaller)。

xml
<provider
    android:name="leakcanary.internal.MainProcessAppWatcherInstaller"
    android:authorities="${applicationId}.leakcanary-installer"
    android:enabled="@bool/leak_canary_watcher_auto_install"
    android:exported="false" />

実装は CRUD を持たない「初期化専用」Provider である。onCreateApplication を取り出し、AppWatcher.manualInstall を呼ぶ。

kotlin
internal class MainProcessAppWatcherInstaller : ContentProvider() {

  override fun onCreate(): Boolean {
    val application = context!!.applicationContext as Application
    AppWatcher.manualInstall(application)
    return true
  }

  // query / insert / update / delete は未使用
}

manualInstall の先で既定の Watcher 群(Activity や Fragment など)が登録され、リーク監視が始まる。依存を debugImplementation しただけでカナリアが動く、という体験の正体は、このマニフェストマージと ContentProvider 起動順序である。

ContentProvider による自動初期化は Firebase や古い WorkManager などでも使われてきた一方、ライブラリごとに Provider が増えると起動コストが積み上がる、という問題が知られている。

AndroidX App Startup と LeakCanary の対応

Google は AndroidX App Startup を導入した。各ライブラリが個別の ContentProvider を持つ代わりに、共通の InitializationProvider 一つを共有し、その下の <meta-data>Initializer 実装を列挙する。起動時に App Startup が依存関係を解決しながら Initializer.create を呼ぶ。

LeakCanary は 2.8 系から、この経路をオプションとして用意している。通常の leakcanary-android を、leakcanary-android-startup に差し替える。

kotlin
dependencies {
  // debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")
  debugImplementation("com.squareup.leakcanary:leakcanary-android-startup:2.14")
}

startup 用モジュールは、自前の ContentProvider ではなく App Startup のマニフェストエントリをマージする。object-watcher 側の例は次のとおりである。

xml
<provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    android:exported="false"
    tools:node="merge">

  <meta-data
      android:name="leakcanary.AppWatcherStartupInitializer"
      android:value="androidx.startup" />
</provider>

対応する Initializer は、やはり AppWatcher.manualInstall を呼ぶ薄いラッパである。

kotlin
class AppWatcherStartupInitializer : Initializer<AppWatcherStartupInitializer> {
  override fun create(context: Context) = apply {
    val application = context.applicationContext as Application
    AppWatcher.manualInstall(application)
  }

  override fun dependencies() = emptyList<Class<out Initializer<*>>>()
}

Changelog の整理どおり、leakcanary-android は自動インストール用コードを乗せる薄い入口であり、コアは leakcanary-android-core にある。手動で AppWatcher.manualInstall する場合は core を直接依存し、自動インストールを避ける構成も取れる。object-watcher と plumber にも、それぞれ *-core / 既定(ContentProvider)/ *-startup の対応関係がある。

経路依存の目安起動フック
既定の自動インストールleakcanary-androidライブラリ専用 ContentProvider
App Startupleakcanary-android-startup共有 InitializationProvider + Initializer
手動leakcanary-android-core などアプリの Application.onCreate などで manualInstall

アプリが既に App Startup で他ライブラリを束ねている場合、LeakCanary も startup アーティファクトに揃えると、ContentProvider の個数を増やさずに自動インストールを維持できる。初期化のタイミングや順序を厳密に制御したい場合は、自動インストールを切って manualInstall に寄せる。

調査の進め方についてのメモ

今回の経路は、公開 API(Activity ライフサイクル)とマニフェスト/起動順という二つの「入口」からソースを辿る形で見えた。検索ツール(grep / ripgrep)で名前の手がかりを拾い、IDE で参照を追う、という手順は、AI 支援の有無にかかわらず有効である。

ライブラリの「依存を足すだけで動く」体験の多くは、ランタイムの魔法ではなく、マニフェストマージとコンポーネント起動順序というプラットフォームの規約に乗っている。ContentProvider から App Startup への移行も、同じ規約の上で初期化の集約方法を変えたものとして読める。