メモリリーク — モバイル開発において最も厄介な問題の一つです。アプリのメモリ使用量はOSが設定した制限に達するまで着実に増加し、その後OutOfMemoryErrorまたは強制終了が発生します。Square Engineeringによると、約40%のAndroidアプリに少なくとも1つのメモリリークが存在し、プロファイリングによってのみ検出可能です。ここでは原因とメモリ増大の防止策を解説します。
重要ポイント
メモリリーク — アプリに不要になったオブジェクトが、GCルートセットからのアクティブな参照がまだ存在するためにヒープに保持され続ける状態。ガベージコレクタはそのようなオブジェクトを生存していると見なし、削除しません。
メモリ肥大化 — アプリが現在のタスクを実行するために必要な以上にメモリを消費する、より広範な問題。原因:過剰なキャッシュ、オブジェクトの重複、非効率なデータ構造、ヒープの断片化。
Androidでは、各アプリに制限付きのヒープが割り当てられます(通常、デバイスとOSバージョンに応じて64–512MB)。iOSでは制限はそれほど厳しくありませんが、上限に近づくとシステムがメモリ警告を送信します。
| 特性 | Android | iOS |
|---|---|---|
| ヒープ制限 | 64–512MB(デバイスによる) | 暗黙的(システム) |
| ガベージコレクション | ART(同時、コンパクト) | ARC(自動参照カウント) |
| リークの仕組み | GCルート参照 | リテインサイクル(強参照サイクル) |
| 結果 | OutOfMemoryError | メモリ警告 → 終了 |
Facebook Engineering Blogによると、メモリリークはモバイルアプリのクラッシュレポートの約15%を占めています。Androidでは、メモリ不足時の頻繁なGCポーズによるANRも加わります。
Activityへの静的参照 — 古典的なAndroidリーク。静的フィールドやシングルトンがActivityへの参照を保持している場合、シングルトンが生存している限り、finish()後もGCによって収集されません。Activityはビュー階層、リソース、Contextを含む重いオブジェクトです。
object LeakHolder {
var activityRef: Activity ?= null // leak: static reference to Activity
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle ?= null) {
super.onCreate(savedInstanceState)
LeakHolder.activityRef = this // ❌ MainActivity will never be GC'd
}
}
匿名クラスとラムダ — 外部クラスへの参照を暗黙的に保持します。RunnableやCallbackが外部サービスに渡され、Activityが破棄された場合でも、匿名クラスのオブジェクトはキューに残り、Activityのガベージコレクションを妨げます。
iOSでは、主な問題はリテインサイクルです:2つのオブジェクトが互いに強参照を保持し、ARCがいずれの参照カウントもゼロにできません。典型的なケース:selfを強くキャプチャするクロージャと、クロージャへの参照を保持するself。
LeakCanary — Androidでの自動リーク検出のためのSquareのライブラリ。ActivityやFragmentが破棄された後、オブジェクトがGCによって収集されたかどうかを確認します。収集されていない場合、ヒープダンプを取得し、リークトレースを表示します。
// LeakCanary 2.x — auto-integration via Application
class ExampleApplication : Application() {
override fun onCreate() {
super.onCreate()
// LeakCanary auto-installs in debug build
// via ContentProvider — zero code setup
}
}
// Force check invocation
AppWatcher.objectWatcher.watch(watchedObject, "leak description")
Android Studio Profiler — リアルタイムメモリ監視のための組み込みツール。ヒープダンプの記録、疑わしいオブジェクト(Retained Size > 1MB)の発見、各オブジェクトへのGCルートパスの追跡が可能です。
iOSにはXcode Memory Graph Debuggerを使用します。メモリ内のオブジェクトグラフを可視化し、リテインサイクルを表示し、循環参照を即座に検出できます。Instruments > Allocationsも長期監視に利用可能です。
WeakReference — ガベージコレクションを妨げるべきでない参照のための基本メカニズム。GCがオブジェクトを収集することを決定した場合、WeakReferenceはnullを返します。コールバック、リスナー、バックグラウンドスレッドからのUIコンポーネントへの参照に使用されます。
Lifecycle-awareコンポーネント — Android Jetpack(Lifecycle、LiveData、Flow、coroutines)で実装されたアーキテクチャアプローチ。サブスクリプションはonDestroyで自動的にキャンセルされ、主要なリーククラスを排除します。
class MyViewModel : ViewModel() {
private val _data = MutableLiveData<List<User>>()
val data: LiveData<List<User>> get() = _data
fun loadData() {
viewModelScope.launch {
val result = repository.fetchData()
_data.postValue(result)
// coroutine auto-cancels on onCleared()
}
}
}
viewModelScopeとlifecycleScope — Androidに組み込まれたCoroutineScopeで、対応するライフサイクルイベントでキャンセルされます。これにより、現代のAndroid開発で最も一般的なシナリオであるコルーチンを介したリークを排除します。
Android StudioのMemory Profiler — ヒープ監視のための主要ツール。ライブアロケーション、ヒープスナップショット、タイプ別のオブジェクト数を表示します。ダンプを記録し、MAT(Memory Analyzer Tool)で分析して疑わしいオブジェクトを見つけることができます。
Eclipse MAT — デスクトップヒープダンプアナライザ。Android StudioからHPROFファイルをロード後、MATは支配ツリーを構築し、各オブジェクトのretainサイズを表示し、Leak Suspects Reportを通じて自動リーク分析を提供します。
Xcode Memory Graph — ビジュアルリテインサイクルデバッガ。Memory Graph Debuggerボタンをクリックすると、Xcodeはアプリを停止し、メモリ内の完全なオブジェクトグラフを構築し、リテインサイクルを赤色で強調表示します。
| ツール | プラットフォーム | 特徴 |
|---|---|---|
| LeakCanary | Android | 破棄後のリーク自動検出 |
| Memory Profiler | Android Studio | ヒープダンプ + ライブアロケーション |
| Eclipse MAT | Android | 支配ツリー、Leak Suspects Report |
| Memory Graph | iOS(Xcode) | リテインサイクル可視化 |
Google I/O 2023によると、デバッグビルドでLeakCanaryを使用するアプリは、導入後最初の2ヶ月でメモリ関連のクラッシュが30–50%減少します。プロジェクトのオンボーディング段階でLeakCanaryを追加することを推奨します。
よくある質問
リーク — コードからアクセスできないが、アクティブな参照のためにGCによって収集されないオブジェクト。肥大化 — アプリが論理的には必要だが過剰な量のオブジェクトを保持する状態(例:80MBの実行中アプリで50MBのキャッシュ)。肥大化はアーキテクチャ的に解決し、リークは正しい参照管理によって解決します。
LeakCanaryはObjectWatcherを使用します — ActivityのonDestroy()後、ActivityへのWeakReferenceを作成しGCを実行します。5秒経ってもWeakReferenceがクリアされない場合、ヒープダンプを取得し、GC Rootからオブジェクトへの最短参照チェーンを分析し、ファイルとコード行を含む正確なリークスタックを表示します。
BitmapはJavaヒープ外のネイティブメモリ(ネイティブヒープ)を占有します。1つのBitmapのサイズ = 幅 × 高さ × 4バイト(ARGB_8888)。12MPの写真(4000×3000)は48MBを占有します。Androidは常にタイムリーにネイティブメモリを解放できるとは限らず、複数のBitmapが蓄積すると、十分なJavaヒープがあってもOOMが発生します。
リテインサイクル — ARCにおいて、2つのオブジェクトが互いに強参照を保持し、参照カウントが決してゼロにならない状態。典型的な例:クロージャへの強参照を持つViewControllerと、selfを強くキャプチャするクロージャ。解決策:クロージャで[weak self]または[unowned self]を使用します。
ヒープサイズはデバイスとAndroidバージョンによって異なります。古いデバイス(API 15–24)は64–128MB、現代のデバイス(API 25+)は256–512MBです。正確な値はActivityManager.getMemoryClass()で取得できます。大きなアプリ(ゲーム、エディタ)の場合、マニフェストでlargeHeap=trueを設定すると最大1GBまで提供されます。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。