【Kotlin/Android】LeakCanaryでメモリリークを検知する方法!

この記事からわかること

  • Android Studio/Kotlinメモリリーク検知する方法
  • Memory Profiler使い方
  • LeakCanary導入と確認方法

index

[open]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

環境

メモリリークとは?

Androidアプリを開発しているとメモリが解放されず、確保されたままになってしまうことがあり、これをメモリリークと呼びます。基本的にActivityやFragmentなどはライフサイクルを保持しているためAndroidシステムがライフサイクルに応じたタイミングでメモリを解放してくれます。

しかしリソースやオブジェクトを不適切に管理するとメモリリークが発生する可能性があります。

[Android]LeakCanaryに怒られない実装をする

メモリリークの発生や原因を調査する

メモリリーク自体が発生していることを検知するためには以下2つのツールが役に立ちます。

Memory Profiler

公式リファレンス:Memory Profiler を使用してアプリのメモリ使用量を調べる

Memory Profiler」はAndroid Studioの機能の「Android Profiler」から提供されている1つで、アプリのメモリ消費量などを確認することができます。これは実機へビルドしてテストしている場合に利用することができます。

Android Profiler

Android Profilerを起動させるには上部メニュー「View」 > 「Tool Windows」 > 「Profiler」をクリックします(Android Studioボトムバーの「Profiler」からでも表示できます)。起動させると以下のように上からCPU/Memory/Energyの使用量が可視化されて表示されるようになります。

【Kotlin/Android】LeakCanaryでメモリリークを検知する方法!

メモリリークが発生している場合はこのMemoryの使用量が減らずにどんどん増えていくような挙動になります。

LeakCanary

公式リファレンス:LeakCanary

LeakCanary」はAndroid Studio標準装備の機能ではなくオープンソースのライブラリです。メモリの使用量などを確認するツールではなく実際にアプリにメモリリークが発生した際に通知を行い、詳細なレポートを提供してくれるものになります。

検出できるメモリリークは以下の通りです。

導入するには以下を「build.gradle(Module:App)」に追加します。デバッグ環境でのみ動作させたいのでdebugImplementationを使用します。動作させたい環境がstagingならstagingImplementationと指定します。


dependencies {
    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.14'
}

導入はこれだけであとは普通にアプリをビルドするだけです。こちらは実機でなくエミュレーターでも動作するようです。すると端末に黄色い鳥のアイコンのアプリが自動で一緒にインストールされます。

【Kotlin/Android】LeakCanaryでメモリリークを検知する方法!

また正常に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」アプリ内で以下のようにメモリリークが発生しているクラスが表示されます。

【Kotlin/Android】LeakCanaryでメモリリークを検知する方法!

クリックすると詳細なスタックトレースを参照することができます。

【Kotlin/Android】LeakCanaryでメモリリークを検知する方法!

また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でのメモリリークがらみ

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

searchbox

スポンサー

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑
今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index