LeakCanary для Kotlin: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

LeakCanary для Kotlin: полное руководство с примерами

В мире разработки под Android управление памятью — одна из самых критичных задач. Утечки памяти (memory leaks) — это ситуация, когда объект, который больше не нужен приложению, продолжает удерживаться в памяти из-за неявных ссылок. Это приводит к росту потребления памяти, частым GC (Garbage Collection) паузам, лагам и, в конечном итоге, к OutOfMemoryError и падению приложения. LeakCanary — это библиотека с открытым исходным кодом от Square, которая автоматически обнаруживает утечки памяти в Android-приложениях на Java и Kotlin.

Зачем она нужна? Отладка утечек вручную — это сложный и трудоемкий процесс, требующий глубокого понимания JVM, анализа heap dumps и использования профилировщиков (Android Studio Profiler, MAT). LeakCanary берет на себя всю эту работу: она автоматически отслеживает жизненный цикл Activity, Fragment, ViewModel и других компонентов, и, если какой-то из них не был собран сборщиком мусора после уничтожения, библиотека создает heap dump, анализирует его и выдает вам готовый отчет с указанием точной цепочки ссылок, которая привела к утечке. Это позволяет разработчикам быстро находить и исправлять проблемы, не тратя часы на ручной анализ.

LeakCanary интегрируется в приложение в виде библиотеки и работает в фоновом режиме. Она не влияет на работу пользователя, но предоставляет разработчику (в debug-сборке) уведомления и подробные логи. Начиная с версии 2.x, библиотека была полностью переписана на Kotlin, стала более производительной и простой в использовании. Она поддерживает новейшие архитектурные компоненты Android (Jetpack) и позволяет кастомизировать процесс обнаружения утечек.

Установка

LeakCanary распространяется через Maven Central. Для установки добавьте зависимость в файл build.gradle.kts (или build.gradle) вашего модуля (app).

Важно: LeakCanary предназначен только для debug-сборок. Никогда не включайте его в релизную (release) версию приложения, так как это может негативно сказаться на производительности и безопасности.

// build.gradle.kts (Module :app)
dependencies {
    // debugImplementation — библиотека будет доступна только в debug-сборке
    debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")
    
    // Для поддержки Jetpack Navigation (опционально)
    debugImplementation("com.squareup.leakcanary:leakcanary-android-navigation:2.14")
    
    // Для поддержки Process Lifecycle (опционально)
    debugImplementation("com.squareup.leakcanary:leakcanary-android-process:2.14")
}

После синхронизации Gradle LeakCanary автоматически зарегистрирует свой ContentProvider при запуске приложения. Никакой дополнительной инициализации в коде не требуется.

Быстрый старт

Минимальный рабочий пример — это просто установка библиотеки. После этого LeakCanary будет автоматически отслеживать утечки.

1. Установите библиотеку, как описано выше.

2. Запустите приложение в debug-режиме.

3. Совершите действие, которое может вызвать утечку (например, поверните экран, если есть утечка Activity).

4. Если утечка обнаружена, вы увидите системное уведомление:

LeakCanary Notification

5. Нажмите на уведомление, чтобы открыть экран с подробным отчетом (Heap Dump Info).

Пример кода, который создает утечку для тестирования:

// Пример утечки: статическая ссылка на Activity
class LeakyActivity : AppCompatActivity() {
    companion object {
        // Статическая ссылка не даст Activity собраться GC
        var leakedActivity: Activity? = null
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        // Сохраняем ссылку на себя
        leakedActivity = this
        
        // Закрываем Activity
        finish()
    }
}

После запуска этого Activity и его закрытия, LeakCanary обнаружит, что экземпляр LeakyActivity не был уничтожен, и покажет утечку.

Основные методы, функции и классы

LeakCanary 2.x построен на концепции ObjectWatcher (наблюдатель за объектами). Он "наблюдает" за объектами, которые должны быть собраны GC. Если объект не уничтожается в течение заданного времени, запускается процесс анализа. Ниже приведены ключевые классы и методы.

1. LeakCanary.config

Сигнатура: LeakCanary.config: LeakCanary.Config

Описание: Позволяет получить или изменить глобальную конфигурацию LeakCanary. Это главная точка для кастомизации поведения библиотеки.

// Пример: отключаем уведомления
LeakCanary.config = LeakCanary.config.copy(
    // Показывать ли уведомления в статус-баре
    showNotification = false
)

2. LeakCanary.showLeakDisplayActivityLauncherIcon

Сигнатура: LeakCanary.showLeakDisplayActivityLauncherIcon: Boolean

Описание: Управляет отображением иконки лаунчера для Activity, показывающего список утечек. По умолчанию true в debug-сборке.

// Скрываем иконку приложения для списка утечек
LeakCanary.showLeakDisplayActivityLauncherIcon = false

3. AppWatcher.objectWatcher

Сигнатура: AppWatcher.objectWatcher: ObjectWatcher

Описание: Возвращает экземпляр ObjectWatcher, который используется для наблюдения за объектами. Вы можете вручную добавить любой объект для отслеживания.

// Наблюдаем за кастомным объектом
val myObject = MyHeavyObject()
AppWatcher.objectWatcher.watch(
    watchedObject = myObject,
    name = "MyHeavyObject reference"
)

4. ObjectWatcher.watch()

Сигнатура: fun watch(watchedObject: Any, name: String = "")

Описание: Добавляет объект под наблюдение. Если объект не будет собран GC в течение заданного времени (по умолчанию 5 секунд), будет зафиксирована утечка.

// Наблюдаем за ViewModel после ее очистки
class MyViewModel : ViewModel() {
    override fun onCleared() {
        super.onCleared()
        // Добавляем ViewModel под наблюдение после очистки
        AppWatcher.objectWatcher.watch(this, "MyViewModel was not destroyed")
    }
}

5. LeakCanary.Config.retentionDelay

Сигнатура: retentionDelay: Duration

Описание: Задержка (в миллисекундах) между обнаружением потенциальной утечки и запуском heap dump. По умолчанию 5 секунд. Это дает время GC для сбора объекта.

// Увеличиваем задержку до 10 секунд
LeakCanary.config = LeakCanary.config.copy(
    retentionDelay = Duration.ofSeconds(10)
)

6. LeakCanary.Config.maxStoredHeapDumps

Сигнатура: maxStoredHeapDumps: Int

Описание: Максимальное количество хранимых heap dump файлов. По умолчанию 7. Старые файлы автоматически удаляются.

// Храним не более 3 последних дампов
LeakCanary.config = LeakCanary.config.copy(
    maxStoredHeapDumps = 3
)

7. LeakCanary.Config.onHeapAnalyzedListener

Сигнатура: onHeapAnalyzedListener: (HeapAnalysis) -> Unit

Описание: Callback, который вызывается после завершения анализа heap dump. Позволяет выполнить кастомные действия (например, отправить отчет на сервер).

// Логируем результат анализа
LeakCanary.config = LeakCanary.config.copy(
    onHeapAnalyzedListener = { heapAnalysis ->
        if (heapAnalysis is HeapAnalysisFailure) {
            Log.e("LeakCanary", "Analysis failed: ${heapAnalysis.exception}")
        } else if (heapAnalysis is HeapAnalysisSuccess) {
            Log.d("LeakCanary", "Found ${heapAnalysis.allLeaks.size} leaks")
        }
    }
)

8. LeakCanary.Config.computeRetainedHeapSize

Сигнатура: computeRetainedHeapSize: Boolean

Описание: Включает или отключает вычисление размера удерживаемой памяти (Retained Heap). По умолчанию true. Отключение ускоряет анализ, но вы не увидите размер утечки.

// Отключаем вычисление размера для ускорения
LeakCanary.config = LeakCanary.config.copy(
    computeRetainedHeapSize = false
)

9. LeakCanary.Config.expectActivityLeaks

Сигнатура: expectActivityLeaks: Boolean

Описание: Включает или отключает автоматическое наблюдение за Activity. По умолчанию true. Если отключить, LeakCanary не будет автоматически отслеживать Activity.

// Отключаем автоматическое отслеживание Activity
LeakCanary.config = LeakCanary.config.copy(
    expectActivityLeaks = false
)

10. LeakCanary.Config.expectFragmentLeaks

Сигнатура: expectFragmentLeaks: Boolean

Описание: Аналогично expectActivityLeaks, но для Fragment (поддержка AndroidX).

// Отключаем автоматическое отслеживание Fragment
LeakCanary.config = LeakCanary.config.copy(
    expectFragmentLeaks = false
)

Реальный практический пример

Рассмотрим типичную ситуацию: утечка через анонимный внутренний класс (callback) в Activity. Activity запускает фоновую задачу, которая держит ссылку на Activity, из-за чего Activity не может быть собран GC после поворота экрана.

// Пример: Activity с утечкой через callback
class MainActivity : AppCompatActivity() {

    private lateinit var textView: TextView

    // Пример синглтона, который держит колбэки
    object EventBus {
        private val listeners = mutableListOf<() -> Unit>()

        fun register(listener: () -> Unit) {
            listeners.add(listener)
        }

        fun unregister(listener: () -> Unit) {
            listeners.remove(listener)
        }

        fun postEvent() {
            listeners.forEach { it.invoke() }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        textView = findViewById(R.id.textView)

        // Анонимный класс захватывает ссылку на `this` (Activity)
        val callback = {
            // Используем Activity внутри callback
            runOnUiThread {
                textView.text = "Event received!"
            }
        }

        // Регистрируем callback, но НЕ отписываемся
        EventBus.register(callback)

        // Симулируем поворот экрана
        findViewById<Button>(R.id.buttonRotate).setOnClickListener {
            // recreate() вызовет onDestroy() и onCreate()
            recreate()
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        // Проблема: мы не отписали callback в onDestroy()!
        // EventBus.unregister(callback) // Этого вызова нет
    }
}

Как LeakCanary поможет:

После поворота экрана старый экземпляр MainActivity должен быть уничтожен. Однако, EventBus (синглтон) все еще держит ссылку на callback, который, в свою очередь, держит ссылку на старую Activity. LeakCanary обнаружит это и покажет отчет:

┬─ MainActivity (утечка)
   ├─ LeakCanary Demo.EventBus$listeners (статическая коллекция)
   │    └─ lambda (анонимный класс callback)
   │         └─ this$0 (ссылка на MainActivity)
   ╰─ GC Root: System Class

Исправленный код:

class MainActivity : AppCompatActivity() {

    private var callback: (() -> Unit)? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        callback = {
            runOnUiThread {
                findViewById<TextView>(R.id.textView).text = "Event received!"
            }
        }

        callback?.let { EventBus.register(it) }
    }

    override fun onDestroy() {
        // Обязательно отписываемся!
        callback?.let { EventBus.unregister(it) }
        callback = null // Помогаем GC
        super.onDestroy()
    }
}

Советы по производительности и типичные ошибки

Советы по производительности

  • Используйте только в debug

Рекомендаций