LeakCanary — це бібліотека з відкритим кодом від Square для автоматичного виявлення витоків пам'яті в Android-додатках. Вона інтегрується в процес розробки та в реальному часі відстежує життєвий цикл Activity, Fragment, ViewModel та інших компонентів, сигналізуючи про витоки одразу після їх виникнення. За даними Square Open Source, бібліотека використовується в тисячах проектів і вважається стандартом де-факто для діагностики пам'яті на Android.
Головне
LeakCanary — це бібліотека для автоматичного виявлення витоків пам'яті в Android-додатках, розроблена компанією Square. Вона вбудовується в процес збірки додатка та автоматично відстежує, чи об'єкти, які повинні бути знищені (Activity, Fragment, View), залишаються в пам'яті. При виявленні витоку LeakCanary формує heap dump і аналізує ланцюжок посилань, що утримує об'єкт.
Бібліотека стала стандартом в Android-спільноті: за даними GitHub, проект зібрав понад 28 тисяч зірок і використовується в додатках Google, Uber, Airbnb та Facebook. LeakCanary доступний у двох основних версіях: класична 1.x (з ручним налаштуванням) і сучасна 2.x (автоматична інтеграція через ContentProvider). Версія 2.x не вимагає зміни Application-класу — залежності достатньо для повноцінної роботи.
Основне завдання LeakCanary — виявити ситуацію, коли об'єкт продовжує існувати в пам'яті після того, як його життєвий цикл завершено. Це типово для витоків через статичні поля, синглтони, незареєстровані колбеки, анонімні класи та замикання, що захоплюють зовнішні об'єкти.
Витоки пам'яті на Android критичніші, ніж на десктопі, через обмежений обсяг RAM на мобільних пристроях. Навіть витік у 5–10 МБ на кожному переході між екранами може призвести до OutOfMemoryError через 30–40 хвилин використання додатка. LeakCanary виявляє такі проблеми на стадії розробки, не чекаючи падіння на продакшені.
LeakCanary використовує слабкі посилання (WeakReference) у комбінації з примусовим викликом збирача сміття. Коли Activity або Fragment викликають onDestroy, LeakCanary створює WeakReference на цей об'єкт і запускає GC через коротку затримку (за замовчуванням 5 секунд). Якщо після GC об'єкт все ще доступний через WeakReference, значить, він утримується сильним посиланням — фіксується витік.
Після виявлення витоку LeakCanary робить heap dump (дамп купи) — повний знімок пам'яті додатка у форматі HPROF. Далі вбудований аналізатор (Shark для версії 2.x) будує граф досяжності від GC Roots до об'єкта, що витік, і знаходить найкоротший шлях — ланцюжок посилань, що утримує об'єкт у пам'яті.
// Спрощена логіка детекції LeakCanary
class ObjectWatcher {
private val watchedReferences = CopyOnWriteArrayList<KeyedWeakReference>()
fun watch(watchedObject: Any, description: String) {
val reference = KeyedWeakReference(watchedObject, description)
watchedReferences.add(reference)
BackgroundHandler.postDelayed({
checkForLeaks()
}, 5000)
}
private fun checkForLeaks() {
GcTrigger.runGc() // примусовий GC
for (ref in watchedReferences) {
if (ref.get() != null) {
onLeakFound(ref) // об'єкт пережив GC — це витік
}
}
}
}
Ключовий момент — примусовий виклик GcTrigger.runGc(). Без нього неможливо відрізнити об'єкт, який реально витік, від об'єкта, який GC ще не встиг зібрати. LeakCanary робить це до трьох разів: якщо після трьох циклів GC об'єкт все ще в пам'яті — витік підтверджено.
Shark — це вбудований в LeakCanary 2.x аналізатор heap dump, написаний на Kotlin. На відміну від попереднього аналізатора HAHA, Shark не завантажує весь HPROF-файл у пам'ять, а обходить його граф об'єктів з мінімальними алокаціями. Це знижує споживання RAM при аналізі з 50 MB до 2–5 MB і скорочує час аналізу з 30 секунд до 1–3 секунд.
Встановлення LeakCanary 2.x в сучасний Android-проект займає один рядок у build.gradle. Бібліотека використовує ContentProvider для автоматичної ініціалізації — не потрібно правити Application-клас або додавати якийсь код у MainActivity. Підключення виконується тільки для debug-збірки, щоб у релізному APK не було зайвого коду.
// build.gradle (app/module)
dependencies {
// debugImplementation — бібліотека тільки для debug-збірки
debugImplementation "com.squareup.leakcanary:leakcanary-android:2.14"
}
Після додавання залежності та перезбірки проекту LeakCanary автоматично з'являється в додатку. При першому запуску бібліотека показує системне сповіщення про активацію. Всі виявлені витоки відображаються у вигляді сповіщень — натискання на сповіщення відкриває екран з детальним звітом (LeakTrace).
Для кастомізації можна створити власний AppWatcherInstaller і перевизначити параметри: таймаут очікування GC, список відстежуваних типів об'єктів, включення збереження heap dump на диск. Однак для 90% проектів конфігурація за замовчуванням є оптимальною.
Починаючи з версії 2.12, LeakCanary підтримує автоматичне відстеження ViewModel, корутинних скопів та State-об'єктів Compose. Додаткових залежностей не потрібно — бібліотека сама визначає, які компоненти Jetpack використовуються в проекті, та активує відповідні детектори.
Звіт LeakCanary (LeakTrace) — це багаторядковий ланцюжок посилань від GC Root до об'єкта, що витік. Кожен рядок показує клас і поле, через яке проходить сильне посилання. Розробнику потрібно читати ланцюжок знизу вгору: нижній рядок — об'єкт, що витік, верхній — точка входу (GC Root).
Типовий LeakTrace виглядає так: GC Root → статичне поле Application → синглтон → колбек → Activity. Якщо розробник бачить такий ланцюжок, проблема ясна: синглтон тримає колбек, який захопив посилання на Activity. Рішення — замінити сильне посилання на слабке в синглтоні.
┬
├─ android.app.Application
│ Leaking: NO (Application — singleton)
│ ↓ Application.leakedActivities
├─ java.util.ArrayList
│ Leaking: NO (ArrayList — normal)
│ ↓ ArrayList[0]
├─ com.example.MainActivity
│ Leaking: YES (Activity destroyed but still in memory)
│ ↓ MainActivity.mCallback
├─ com.example.CallbackWrapper
│ Leaking: UNKNOWN
│ ↓ CallbackWrapper.mListener
│ ~~~~~~~~~~
├─ com.example.MyCallback (anonymous)
│ Leaking: UNKNOWN
│ ↓ MyCallback.this$0
├─ com.example.MainActivity
│ Leaking: YES (MainActivity is the leak)
╰
У цьому прикладі LeakCanary показує, що MainActivity утримується через ланцюжок: Application → ArrayList → MainActivity → CallbackWrapper → MyCallback → знову MainActivity. Стрілка this$0 вказує, що анонімний клас MyCallback захопив зовнішнє посилання на Activity. Рішення — зробити колбек слабким посиланням або скасовувати його в onDestroy.
LeakCanary також показує статус витоку для кожного елемента ланцюжка: NO (немає витоку — це кореневий елемент), YES (об'єкт повинен бути знищений), UNKNOWN (не вдалося визначити статус). Статус UNKNOWN не означає проблеми — це проміжний об'єкт, який LeakCanary не може однозначно класифікувати.
Перехід з версії 1.x на 2.x був кардинальним: розробники переписали бібліотеку з нуля, замінивши застарілий HAHA-аналізатор на власний рушій Shark, написаний на Kotlin. Shark працює на порядок швидше, потребує менше пам'яті для аналізу та точніше визначає корінні причини витоків.
| Параметр | LeakCanary 1.x | LeakCanary 2.x |
|---|---|---|
| Мова аналізатора | Java (HAHA — fork of Android SDK) | Kotlin (Shark — власний рушій) |
| Встановлення | Ручне налаштування AppWatcher в Application | Автоматичне через ContentProvider |
| Швидкість | 10–30 секунд на аналіз heap dump | 1–5 секунд на аналіз heap dump |
| Продуктивність | Займає 10–50 MB RAM при аналізі | Займає 2–10 MB RAM при аналізі |
Ключова перевага Shark — він не завантажує весь heap dump у пам'ять, а обходить його граф посилань з мінімальними алокаціями. Це робить LeakCanary 2.x придатним для використання на пристроях з низьким обсягом RAM без ризику OutOfMemoryError при аналізі.
У версії 2.x також з'явилася можливість експорту heap dump у файл для подальшого аналізу в Android Studio Memory Profiler. Для цього потрібно включити налаштування dumpHeapWhenLeakFound в конфігурації AppWatcher.
LeakCanary ефективно виявляє кілька класів витоків, характерних для Android. Найчастіше зустрічається витік через статичні посилання на Activity — розробники зберігають посилання на контекст Activity у синглтоні, і Activity не може бути зібрана GC після завершення її життєвого циклу.
Друга за частотою категорія — витоки через незареєстровані слухачі. Якщо в onStart був викликаний registerListener, але в onStop/onDestroy не викликаний unregisterListener — об'єкт-слухач утримується системою навіть після знищення активності. LeakCanary однозначно показує, який слухач і в якому системному сервісі залишився живим.
// Типовий витік: Activity захоплена в колбек синглтона
object AnalyticsManager {
private var callback: ((String) -> Unit)? = null
fun register(callback: (String) -> Unit) {
this.callback = callback // сильне посилання на колбек
}
fun unregister() {
callback = null // НЕ ЗАБУДЬ викликати в onDestroy!
}
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
AnalyticsManager.register { event ->
logEvent(event) // лямбда захоплює this
}
// якщо не викликати unregister в onDestroy → витік Activity
}
}
Третя категорія — витоки через Fragment в BackStack. Якщо FragmentTransaction.addToBackStack() викликається без видалення Fragment при поверненні, старі екземпляри Fragment залишаються в пам'яті. LeakCanary допомагає виявити такі приховані витоки на ранніх етапах розробки.
Для кожного виявленого витоку LeakCanary пропонує опис і рекомендації щодо виправлення. У версії 2.14 додано інтеграцію з Android Lint — бібліотека може автоматично створювати задачі в issue tracker при виявленні витоку в CI.
Часті запитання
Так, обов'язково. LeakCanary підключається через debugImplementation в build.gradle, що автоматично виключає його з релізної збірки. Якщо підключити через implementation, бібліотека потрапить у релізний APK і буде показувати витоки кінцевим користувачам — це неприпустимо.
Вплив на продуктивність мінімальний. LeakCanary активується тільки після onDestroy компонента і не втручається у відображення UI або обробку дотиків. Єдина витрата — коротка пауза примусового GC (близько 100 мс) і запис heap dump при витоку (соті частки секунди).
LeakCanary автоматично зберігає heap dump у форматі HPROF у папку додатка. Файл можна експортувати через Android Studio: Device File Explorer → data/data/com.example/files/leakcanary/. Для перегляду відкрийте файл у Memory Profiler через Capture → Open Heap Dump.
Так, починаючи з версії 2.12 LeakCanary повністю підтримує Jetpack Compose. Бібліотека відстежує Composition-контексти та State-об'єкти, автоматично визначаючи витоки в Composable-функціях. Окреме налаштування не потрібне — працює з коробки.
Хибні спрацьовування можливі, але рідкісні. LeakCanary використовує потрійний виклик GC перед оголошенням витоку, що виключає більшість false positive. Якщо ви вважаєте, що спрацьовування хибне — створіть IgnoredReference для конкретного класу в конфігурації.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також