Photoruction工事中!

Photoructionの開発ブログです!

StrictModeでリークを検出する

はじめに

Androidチームでは現在、クラッシュフリー率の改善に取り組んでいます。 詳細は過去の記事をご覧ください。 https://kojichu.photoruction.com/entry/2025/04/02/090000 改善の結果、クラッシュフリー率は95%まで向上することはできましたが、そこから先97〜99%の間を行き来する状態が続いており、伸び悩みを感じていました。

目についたのはOOM(OutOfMemoryError)

Crashlyticsを確認していく中で、気になる傾向がありました。 それが OOM(OutOfMemoryError) です。 特定の重い処理ではなく、「一見なんてことのない画面」でOOMが発生しているケースが複数見つかりました。 実際のログは以下のようなものです。

Fatal Exception: java.lang.OutOfMemoryError:
Failed to allocate a 56 byte allocation with 2777520 free bytes and 2712KB until OOM,
target footprint 536870912, growth limit 536870912;
giving up on allocation because <1% of heap free after GC.

一見すると「たった56バイトの確保に失敗している」だけですが、GC後でもヒープの空きがほとんど残っていない状態になっていることが分かります。 この状況からまず疑ったのは、メモリリークでした。ActivityやContextが解放されていない or 不要なオブジェクトが保持され続けていると。 こういった典型的なリークが積み重なり、ヒープを圧迫しているのではないかと考えました。

StrictMode(VMポリシー)を使って調査する

メモリリークの調査といえばLeakCanaryが定番ですが、 今回はまず軽量に試せる方法として StrictModeのVMポリシー を使ってみました。 StrictModeというとスレッド周りの検知が有名ですが、VMポリシーを使うと「リソースやオブジェクトの扱いの問題」を検知できるのがポイントです。 設定は以下の通りです。

if (BuildConfig.DEBUG) {
    StrictMode.setVmPolicy(
        StrictMode.VmPolicy.Builder()
            .detectLeakedClosableObjects()
            .detectActivityLeaks()
            .detectLeakedSqlLiteObjects()
            .penaltyLog()
            .build()
    )
}

Activityリークは検出されなかった

まず想定していたのは、ActivityやContextのリークでした。しかし実際に画面を操作してみても、 Activity leaks や Context保持に関する違反を示すログは確認できませんでした。 ここだけ見ると、仮説は外れたように見えます。

代わりに出てきたのはClosable / SQLiteリソースのリーク

その代わりに出力されたのが、以下のログです。

StrictMode policy violation: android.os.strictmode.LeakedClosableViolation:
A resource was acquired at attached stack trace but never released.
...
Caused by: java.lang.Throwable: Explicit termination method 'close' not called
...
at android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:336)
at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper.getWritableDatabase(FrameworkSQLiteOpenHelper.java:112)
at androidx.room.RoomDatabase.inTransaction(RoomDatabase.java:706)

LeakedClosableViolation という名前の通り、Closeableなリソースが取得されたまま解放されていないことを示しています。 さらに、Explicit termination method 'close' not calledというメッセージから、明示的な close() 呼び出し漏れが疑われます。 スタックトレースを追うと、 * SQLiteDatabase * Room * Repository層 へとつながっており、少なくとも SQLite関連リソースの管理に問題がある可能性 が見えてきました。

今回分かったこと

今回の調査では、当初想定していたようなActivityリークは確認できませんでした。 一方で、StrictModeのVMポリシーによって、Closable / SQLiteリソースの解放漏れが可視化されたという結果になりました。 これがOOMの直接的な原因かどうかは、現時点では断定できません。 ただし、リソースが適切に解放されていない&それが積み重なることでメモリやシステムリソースを圧迫する可能性があるという点は無視できないと感じています。

現時点の対応と今後

現時点では、該当箇所の修正方針までは整理できています。 * SQLite関連リソースのライフサイクル見直し * close漏れの洗い出し * Repository層の責務の整理 今後はこれらを段階的に修正しつつ、 * OOMの発生状況が改善するか * 他にも同様のリークがないか を引き続き検証していく予定です。

まとめ

OOMの原因調査としてStrictModeを導入しましたが、想定していたメモリリークではなく、Closable / SQLiteリソースの問題が先に見つかるという結果になりました。 StrictModeは「特定の問題をピンポイントで見つけるツール」というより、仮説と違っても、潜んでいる問題を可視化してくれるツールだと感じています。 まだ調査は道半ばですが、少なくとも「見えていなかった問題」が一つ明らかになったのは大きな収穫でした。 同じようにOOMや原因不明の不具合に悩んでいる場合は、 StrictModeのVMポリシーを一度有効にしてみるとヒントが得られるかもしれません。