Утечка памяти — одна из самых коварных проблем в мобильной разработке. Память приложения неуклонно растёт, пока не достигает предела, установленного ОС, после чего следует OutOfMemoryError или принудительное завершение. По данным Square Engineering, около 40% Android-приложений имеют хотя бы одну утечку памяти, которая может быть обнаружена только при профилировании. Разберём причины и методы предотвращения роста памяти.
Главное
Утечка памяти (memory leak) — ситуация, когда объект, который больше не нужен приложению, продолжает удерживаться в heap, потому что на него остаётся активная ссылка от корневого набора (GC Root). Сборщик мусора считает такой объект живым и не удаляет его.
Раздувание памяти (memory bloat) — более широкая проблема, когда приложение потребляет больше памяти, чем необходимо для выполнения текущих задач. Причины: избыточное кэширование, дублирование объектов, неоптимальные структуры данных и фрагментация heap.
В Android на каждое приложение выделяется ограниченный heap (обычно 64-512 MB в зависимости от устройства и версии ОС). В iOS ограничение менее жёсткое, но система отправляет memory warning при приближении к лимиту.
| Характеристика | Android | iOS |
|---|---|---|
| Ограничение heap | 64-512 MB (зависит от устройства) | Неявное (системное) |
| Сборка мусора | ART (Concurrent, Compact) | ARC (Automatic Reference Counting) |
| Механизм утечки | GC Root references | Retain cycles (strong reference cycles) |
| Итог | OutOfMemoryError | Memory warning → termination |
По данным Facebook Engineering Blog, утечки памяти — причина ~15% crash-репортов в мобильных приложениях. В Android к этому добавляются ANR из-за частых GC пауз при нехватке памяти.
Статическая ссылка на Activity — классика Android-утечек. Если статическое поле или синглтон хранит ссылку на Activity, она не будет собрана GC даже после finish(), пока синглтон жив. Activity — тяжёлый объект, содержащий View hierarchy, resources и Context.
object LeakHolder {
var activityRef: Activity ?= null // leak: static reference to Activity
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle ?= null) {
super.onCreate(savedInstanceState)
LeakHolder.activityRef = this // ❌ MainActivity will never be GC'd
}
}
Анонимные классы и лямбды — неявно удерживают ссылку на внешний класс. Если Runnable или Callback передаётся во внешний сервис, а Activity уничтожается, объект анонимного класса всё ещё висит в очереди и не даёт Activity уйти на сборку мусора.
В iOS основная проблема — retain cycles: два объекта держат сильные ссылки друг на друга, и ARC не может обнулить счётчик ссылок ни для одного из них. Типичный случай: closure, захватывающий self сильно, и self, держащий ссылку на closure.
LeakCanary — библиотека от Square для автоматического обнаружения утечек в Android. После уничтожения Activity или Fragment она проверяет, что объект был собран GC. Если нет — делает heap dump и показывает trace утечки.
// LeakCanary 2.x — auto-integration via Application
class ExampleApplication : Application() {
override fun onCreate() {
super.onCreate()
// LeakCanary auto-installs in debug build
// via ContentProvider — zero code setup
}
}
// Force check invocation
AppWatcher.objectWatcher.watch(watchedObject, "leak description")
Android Studio Profiler — встроенный инструмент для мониторинга памяти в реальном времени. Позволяет записать heap dump, найти объекты-подозреватые (Retained Size > 1 MB), и проследить GC root path до каждого объекта.
Для iOS используйте Xcode Memory Graph Debugger. Он визуализирует граф объектов в памяти, показывает retain cycles и позволяет мгновенно обнаружить круговые ссылки. Также доступен Instruments > Allocations для долгосрочного мониторинга.
WeakReference — базовый механизм для ссылок, которые не должны мешать сборке мусора. Если GC решит удалить объект, WeakReference вернёт null. Используется для обратных вызовов, слушателей и ссылок на UI-компоненты из фоновых потоков.
Lifecycle-aware компоненты — архитектурный подход, реализованный в Android Jetpack (Lifecycle, LiveData, Flow, coroutines). Подписки автоматически отменяются при onDestroy, что исключает основной класс утечек.
class MyViewModel : ViewModel() {
private val _data = MutableLiveData<List<User>>()
val data: LiveData<List<User>> get() = _data
fun loadData() {
viewModelScope.launch {
val result = repository.fetchData()
_data.postValue(result)
// coroutine auto-cancels on onCleared()
}
}
}
viewModelScope и lifecycleScope — встроенные CoroutineScope в Android, которые отменяются при соответствующем событии жизненного цикла. Это исключает утечки через корутины — наиболее частый сценарий в современной Android-разработке.
Memory Profiler in Android Studio — основной инструмент для мониторинга heap. Показывает live allocations, снимки heap, количество объектов по типам. Позволяет записать дамп и проанализировать его в MAT (Memory Analyzer Tool) для поиска подозрительных объектов.
Eclipse MAT — десктопный анализатор heap dump. После загрузки HPROF-файла из Android Studio, MAT строит дерево dominator, показывает retain size каждого объекта и предлагает автоматический анализ подозрительных утечек через Leak Suspects Report.
Xcode Memory Graph — визуальный дебаггер retain cycles. При нажатии на кнопку Memory Graph Debugger Xcode останавливает приложение, строит полный граф объектов в памяти и подсвечивает retain cycles красным цветом.
| Инструмент | Платформа | Особенность |
|---|---|---|
| LeakCanary | Android | Автообнаружение утечек после destroy |
| Memory Profiler | Android Studio | Heap dump + live allocations |
| Eclipse MAT | Android | Dominator tree, Leak Suspects Report |
| Memory Graph | iOS (Xcode) | Retain cycles visualiser |
По данным Google I/O 2023, приложения, использующие LeakCanary в debug-сборках, сокращают количество memory-related crash на 30-50% за первые 2 месяца после внедрения. Рекомендуется добавлять LeakCanary на этапе onboarding проекта.
Часто задаваемые вопросы
Утечка — объекты, не доступные коду, но не удаляемые GC из-за активных ссылок. Раздувание — приложение держит в памяти объекты, которые логически нужны, но в избыточном количестве (например, кеш на 50 MB при работающем приложении весом 80 MB). Раздувание лечится архитектурно, утечка — через корректное управление ссылками.
LeakCanary использует ObjectWatcher — после onDestroy() Activity он создаёт WeakReference на Activity и запускает GC. Если через 5 секунд WeakReference не очищен, LeakCanary делает heap dump, анализирует shortest reference chain от GC Root до объекта и показывает точный стек утечки с указанием файла и строки кода.
Bitmap занимает память вне heap Java в нативной памяти (native heap). Размер одного Bitmap = ширина × высота × 4 байта (ARGB_8888). Фото 12 MP (4000×3000) занимает 48 MB. Android не всегда может своевременно освободить native memory, что при накоплении нескольких Bitmap приводит к OOM даже при достаточном Java heap.
Retain cycle — ситуация в ARC, когда два объекта держат сильные ссылки друг на друга, и счётчик ссылок никогда не достигает нуля. Типичный пример: ViewController с сильной ссылкой на closure, а closure захватывает self сильно. Решение: использовать [weak self] или [unowned self] в замыканиях.
Размер heap зависит от устройства и версии Android. Для старых устройств (API 15-24) — 64-128 MB. Для современных (API 25+) — 256-512 MB. Точное значение можно получить через ActivityManager.getMemoryClass(). Для больших приложений (игры, редакторы) есть largeHeap=true в манифесте, дающее до 1 GB.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также