【Kotlin/Android】LeakCanaryでメモリリークを検知する方法!
この記事からわかること
- Android Studio/Kotlinでメモリリークの検知する方法
- Memory Profilerの使い方
- LeakCanaryの導入と確認方法
index
[open]
\ アプリをリリースしました /
友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-
posted withアプリーチ
環境
- Android Studio:Flamingo
- Kotlin:1.8.20
メモリリークとは?
Androidアプリを開発しているとメモリが解放されず、確保されたままになってしまうことがあり、これをメモリリークと呼びます。基本的にActivityやFragmentなどはライフサイクルを保持しているためAndroidシステムがライフサイクルに応じたタイミングでメモリを解放してくれます。
しかしリソースやオブジェクトを不適切に管理するとメモリリークが発生する可能性があります。
[Android]LeakCanaryに怒られない実装をする
メモリリークの発生や原因を調査する
メモリリーク自体が発生していることを検知するためには以下2つのツールが役に立ちます。
- Memory Profiler・・・CPUやメモリの使用量を把握する
- LeakCanary・・・実際にメモリリークが発生している箇所を特定する
Memory Profiler
公式リファレンス:Memory Profiler を使用してアプリのメモリ使用量を調べる
「Memory Profiler」はAndroid Studioの機能の「Android Profiler」から提供されている1つで、アプリのメモリ消費量などを確認することができます。これは実機へビルドしてテストしている場合に利用することができます。
Android Profiler
- CPU Profiler・・・CPUの使用量(パフォーマンス)
- Memory Profiler・・・メモリの消費量(割り当て量)
- Energy Profiler・・・バッテリーの消耗につながる可能性があるエネルギー使用量
Android Profilerを起動させるには上部メニュー「View」 > 「Tool Windows」 > 「Profiler」をクリックします(Android Studioボトムバーの「Profiler」からでも表示できます)。起動させると以下のように上からCPU/Memory/Energyの使用量が可視化されて表示されるようになります。
メモリリークが発生している場合はこのMemoryの使用量が減らずにどんどん増えていくような挙動になります。
LeakCanary
「LeakCanary」はAndroid Studio標準装備の機能ではなくオープンソースのライブラリです。メモリの使用量などを確認するツールではなく実際にアプリにメモリリークが発生した際に通知を行い、詳細なレポートを提供してくれるものになります。
検出できるメモリリークは以下の通りです。
- Activity
- Fragment
- fragment View
- ViewModel
- Service
導入するには以下を「build.gradle(Module:App)」に追加します。デバッグ環境でのみ動作させたいのでdebugImplementation
を使用します。動作させたい環境がstaging
ならstagingImplementation
と指定します。
dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.14'
}
導入はこれだけであとは普通にアプリをビルドするだけです。こちらは実機でなくエミュレーターでも動作するようです。すると端末に黄色い鳥のアイコンのアプリが自動で一緒にインストールされます。
また正常にLeakCanaryが動作している場合はデバッグログに以下のように出力されているはずなので確認してみてください。もしなければ実行環境がdebugかどうかなどを確かめてみてください。
LeakCanary is running and ready to detect memory leaks.
メモリリークが発生した場合
まずは意図的にメモリリークを発生させてみます。Activity
を静的な参照として保持させておき、Activity
を終了させればメモリリークが発生します。
object ActivityHolder {
var activity: Activity? = null
}
class SecondActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Staticな参照を保持
ActivityHolder.activity = this
val button: Button = findViewById(R.id.transfer_button)
button.setOnClickListener {
// Activityを終了させる
finish()
}
}
}
メモリリークが発生すると「Leaks」アプリ内で以下のようにメモリリークが発生しているクラスが表示されます。
クリックすると詳細なスタックトレースを参照することができます。
またAndroid Studioのデバッグログにも以下のように同じものが出力されます。
====================================
HEAP ANALYSIS RESULT
====================================
1 APPLICATION LEAKS
References underlined with "~~~" are likely causes.
Learn more at https://squ.re/leaks.
121845 bytes retained by leaking objects
Signature: fe615b9787218124377ec23b28893a6368a999af
┬───
│ GC Root: Thread object
│
├─ android.os.HandlerThread instance
│ Leaking: NO (PathClassLoader↓ is not leaking)
│ Thread name: 'LeakCanary-Heap-Dump'
│ ↓ Thread.contextClassLoader
├─ dalvik.system.PathClassLoader instance
│ Leaking: NO (ActivityHolder↓ is not leaking and A ClassLoader is never leaking)
│ ↓ ClassLoader.runtimeInternalObjects
├─ java.lang.Object[] array
│ Leaking: NO (ActivityHolder↓ is not leaking)
│ ↓ Object[1508]
├─ com.XXXXXX.プロジェクト名.ActivityHolder class
│ Leaking: NO (a class is never leaking)
│ ↓ static ActivityHolder.activity
│ ~~~~~~~~
╰→ com.XXXXXX.プロジェクト名.SecondActivity instance
Leaking: YES (ObjectWatcher was watching this because com.XXXXXX.プロジェクト名.SecondActivity received
Activity#onDestroy() callback and Activity#mDestroyed is true)
Retaining 121.8 kB in 1835 objects
key = 49a0ff15-2382-44d9-987d-bf0afe1d01a2
watchDurationMillis = 47881
retainedDurationMillis = 42875
mApplication instance of android.app.Application
mBase instance of android.app.ContextImpl
====================================
0 LIBRARY LEAKS
A Library Leak is a leak caused by a known bug in 3rd party code that you do not have control over.
See https://square.github.io/leakcanary/fundamentals-how-leakcanary-works/#4-categorizing-leaks
====================================
0 UNREACHABLE OBJECTS
An unreachable object is still in memory but LeakCanary could not find a strong reference path
from GC roots.
====================================
METADATA
Please include this in bug reports and Stack Overflow questions.
Build.VERSION.SDK_INT: 34
Build.MANUFACTURER: Google
LeakCanary version: 2.14
App process name: com.XXXXXX.プロジェクト名
Class count: 27039
Instance count: 208203
Primitive array count: 148728
Object array count: 30080
Thread count: 22
Heap total bytes: 27529604
Bitmap count: 8
Bitmap total bytes: 158814
Large bitmap count: 0
Large bitmap total bytes: 0
Db 1: open /data/user/0/com.XXXXXX.プロジェクト名/no_backup/androidx.work.workdb
Db 2: open /data/user/0/com.XXXXXX.プロジェクト名/databases/leaks.db
Stats: LruCache[maxSize=3000,hits=114582,misses=198449,hitRate=36%]
RandomAccess[bytes=9822648,reads=198449,travel=83388825113,range=33535611,size=41882066]
Heap dump reason: user request
Analysis duration: 16420 ms
Heap dump file path: /storage/emulated/0/Download/leakcanary-com.XXXXXX.プロジェクト名/2024-07-11_21-52-22_854.hprof
Heap dump timestamp: 1720702364725
Heap dump duration: 2004 ms
====================================
Swiftでのメモリリークがらみ
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。