0Pricing
Android Academy · レッスン

メモリリークと修正方法

リークを見つけて止めます。

「メモリリークと修正方法」はCoddyKit上の無料Android Academyレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはAndroid Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Android Academyコースには全4レッスンが含まれています。

メモリリークとは何か

メモリリークとは、不要になったオブジェクトが、何かに参照を保持され続けているためにガベージコレクションで回収できない状態です。Androidで典型的な対象となるのは、画面が破棄された後も残り続けるActivityやContextです。

リークが時間とともに増えると、ヒープが大きくなり、ガベージコレクション(UIを一時停止させ、jankの原因になります)の頻度が上がります。そして最終的にはOutOfMemoryErrorでアプリがクラッシュします。このレッスンでは、リークの見つけ方と修正方法を学びます。

GCが保持するものを決める仕組み

Androidのガベージコレクターは、GCルート(生きているスレッドやstaticフィールドなど)から到達可能なすべてのオブジェクトを保持します。到達できないオブジェクトは解放されます。

リークとは、単純に言えば望ましくない到達可能性の経路です。つまり、長く生存するオブジェクトが短命なオブジェクトへの参照を保持している状態です。下の純粋なKotlinのデモでは、到達可能性によってオブジェクトが生存し続ける様子を示しています。

object GlobalCache {
    val items = mutableListOf<ByteArray>()
}

fun cacheSomething() {
    // This 1MB array stays alive forever because GlobalCache
    // (a static singleton, a GC root) keeps referencing it.
    GlobalCache.items.add(ByteArray(1_000_000))
}

fun main() {
    repeat(3) { cacheSomething() }
    println("Held arrays: ${GlobalCache.items.size}") // never freed
}

定番:Activityのリーク

Androidで最もよくあるリークは、staticまたは長期間存続するオブジェクトがContextを保持することです。Activityが破棄されると(画面の回転や移動など)、システムは解放しようとしますが、staticな参照が保持し続けるため、Activityはいつまでも残り続けます。

以下のコードはリークを起こします。シングルトンがActivityのコンテキストをキャッシュしているためです。

// LEAK: static field holds an Activity Context
object Analytics {
    var context: Context? = null   // <- keeps Activity alive
    fun init(ctx: Context) { context = ctx }
}

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        Analytics.init(this) // passing the Activity -> leak on rotation
    }
}

修正:Application Contextを使う

長期間存続するものにContextを保持する必要がある場合は、プロセス全体の存続期間にわたって存在し、安全に保持できるapplication contextを保存してください。

Activity/Viewのコンテキストは、その画面が存在する期間だけ保持してください。この1つのルールで、Androidのメモリリークの大半を防げます。

object Analytics {
    private var appContext: Context? = null
    fun init(ctx: Context) {
        // applicationContext is process-scoped and safe to keep
        appContext = ctx.applicationContext
    }
}

// Call site is now leak-free:
Analytics.init(this) // stores applicationContext, not the Activity

内部クラスとHandler

非staticの内部クラス(多くのリスナー、Runnable、Handlerのコールバックを含みます)は、外側のクラスへの暗黙的な参照を保持します。その処理が画面より長く存続すると、画面がリークします。

遅延実行するHandler.postDelayedは、よくある原因です。保留中のメッセージが、実行されるまでActivityを保持するためです。

// LEAK: the posted Runnable holds the Activity for 60 seconds
handler.postDelayed({ updateUi() }, 60_000)

// FIX: cancel pending work when the screen goes away
override fun onDestroy() {
    super.onDestroy()
    handler.removeCallbacksAndMessages(null)
}

コルーチン:処理のスコープを設定する

画面より長く存続するコルーチンは、キャプチャしたすべてのものをリークさせます。修正方法はstructured concurrencyです。ライフサイクルに紐づいたスコープで処理を起動すれば、自動的にキャンセルされます。

ViewModelではviewModelScopeを、UIコントローラーではlifecycleScopeを使用してください。所有者が破棄されるとスコープがキャンセルされ、参照が解放されます。

class FeedViewModel : ViewModel() {
    fun load() {
        // Cancelled automatically when the ViewModel is cleared
        viewModelScope.launch {
            val feed = repository.fetchFeed()
            _state.value = feed
        }
    }
}

// In Compose, collect tied to the lifecycle:
val state by viewModel.state.collectAsStateWithLifecycle()

ComposeのDisposableEffect

Composeでは、登録したものはコンポーザブルが画面から離れるときに必ず登録解除してください。DisposableEffectは、そのためのonDisposeブロックを提供します。リスナー、オブザーバー、センサー、ブロードキャストレシーバーなどに使用できます。

ここでクリーンアップを忘れると、コールバックと、それがキャプチャしているすべてのものがリークします。

@Composable
fun LocationDisplay(manager: LocationManager) {
    DisposableEffect(manager) {
        val listener = LocationListener { /* update */ }
        manager.register(listener)
        onDispose { manager.unregister(listener) } // prevents the leak
    }
}

LeakCanaryでリークを見つける

LeakCanaryは、デバッグビルドでリークを自動的に検出する標準ツールです。依存関係を追加すると、破棄されたオブジェクトを監視します。オブジェクトがガベージコレクションされない場合はヒープをダンプし、そのオブジェクトを保持している正確な参照チェーンを表示します。

そのチェーンが修正箇所を示します。問題のあるフィールドやリスナーを直接特定できます。

// build.gradle.kts (app module)
dependencies {
    debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")
}
// No code needed: on a debug build, navigate away from a screen and
// LeakCanary posts a notification with the leak trace, e.g.:
//   Analytics.context -> MainActivity (leaked)

Profilerでヒープダンプを確認する

LeakCanaryが検出しないリーク(徐々に増加するメモリ、nativeメモリ、大きなキャッシュなど)には、Memory Profilerを使用します。ヒープダンプを取得してGCを強制実行し、インスタンス数が異常に多いクラスを調べてください。

画面を移動した後もActivity、Fragment、またはViewModelのインスタンス数が増え続けているなら、明らかなリークの兆候です。ProfilerにはGCルートまでの経路も表示されるため、参照を追跡できます。

Bitmapと大きなメモリ割り当て

メモリに関する問題がすべて参照リークとは限りません。大きなbitmapや上限のないキャッシュだけでも、ヒープを圧迫します。フル解像度の写真は、メモリ上で数十MBになることがあります。

画像ライブラリ(Coil/Glide)に表示サイズまでダウンサンプリングさせ、作成するキャッシュには明示的な最大サイズを設定してください。以下のスニペットは、サイズに上限を設けたLRUキャッシュを示しています。

// Bound memory: keep at most ~1/8 of available app memory
val maxKb = (Runtime.getRuntime().maxMemory() / 1024 / 8).toInt()
val bitmapCache = object : LruCache<String, Bitmap>(maxKb) {
    override fun sizeOf(key: String, value: Bitmap) = value.byteCount / 1024
}
// An unbounded HashMap<String, Bitmap> would grow until OutOfMemoryError.

リーク防止チェックリスト

次の習慣を身につければ、ほとんどのリークは未然に防げます。

  • 長期間存続するオブジェクトにはapplicationContextを保存し、Activity/Viewは保存しない。
  • 非同期処理はライフサイクルに紐づいたコルーチン(viewModelScope、lifecycleScope)で実行する。
  • リスナーやレシーバーは必ず登録解除する(onDispose、onDestroy)。
  • 遅延Handlerとタイマーをキャンセルする。
  • キャッシュに上限を設け、bitmapをダウンサンプリングする。
  • デバッグビルドにLeakCanaryを組み込み、そのトレースに対処する。

クイックチェック

シングルトンオブジェクトで、ライフサイクル全体にわたってContextを保持する必要があります。画面をリークさせないためには、どのコンテキストを保持すべきでしょうか?

まとめ:メモリをクリーンに保つ

リークとは何か、そしてリークを防ぐ方法を学びました。

  • リークとは、不要になったオブジェクトを生存させ続ける、意図しない到達可能性の経路です。
  • 典型的なリークは、Activity/Contextへのstaticまたは長期間存続する参照です。代わりにapplicationContextを保存してください。
  • 内部クラスのリスナー、遅延実行するHandler、スコープのないコルーチンは所有者をリークさせます。スコープを設定し、キャンセルしてください。
  • ComposeではDisposableEffectを使ってコールバックの登録を解除します。
  • LeakCanaryとMemory Profilerで参照チェーンを確認できます。
  • キャッシュに上限を設け、bitmapをダウンサンプリングします。

次は、起動最適化とベースラインプロファイルを使って、アプリを素早く起動できるようにします。

よくある質問

「メモリリークと修正方法」レッスンは無料ですか?

はい。「メモリリークと修正方法」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Android Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Android Academyコースには全4レッスンが含まれています。

「メモリリークと修正方法」で何を学びますか?

リークを見つけて止めます。 ブラウザで直接実行するハンズオンコードでAndroid Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Android Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのAndroid Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。

「メモリリークと修正方法」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このAndroid Academyレッスンでコードを書いて実行できますか?

はい。すべてのAndroid Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. パフォーマンスを測定する
  2. 再コンポジションを制御する
  3. メモリリークと修正方法
  4. 起動処理とBaseline Profiles
← Android Academyに戻る