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. Если утечка обнаружена, вы увидите системное уведомление:

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