Приложение не крэшится сразу — оно постепенно растёт в памяти. Через 20 минут использования появляется лёгкая задумчивость. Через 40 — системный Memory Pressure убивает фоновые процессы. Через час — SIGKILL от iOS или OOM-киллер Android завершает приложение. Пользователь думает, что приложение «глючит». Firebase Crashlytics ничего не покажет — это не крэш, это убийство системой. Даже если Memory Warning срабатывает, UI может стать тормозным из-за сброса кэшей, что ведёт к негативным отзывам. По статистике, 80% мобильных приложений имеют утечки памяти, которые приводят к потере 15% пользователей в месяц. Мы, как мобильные разработчики с 5-летним опытом, ежедневно сталкиваемся с такими кейсами. Наши инженеры находят утечки там, где стандартные инструменты молчат. Экономия на серверных расходах может достигать 30% при устранении утечек. Свяжитесь с нами — получите консультацию по профилированию вашего приложения.
Как провести профилирование памяти за 5 шагов
- Настройка инструментов: Подключите Instruments для iOS, LeakCanary для Android, DevTools Memory для Flutter.
- Сценарий тестирования: Определите ключевые экраны и действия (навигация, загрузка данных, работа с изображениями).
- Сбор данных: Запустите профилировщик, выполняйте сценарий, фиксируйте snapshots heap до и после каждого действия.
- Анализ: Ищите объекты, которые не освобождаются (retain cycles, listeners, контекст). Используйте LeakCanary для автоматического детекта.
- Оптимизация: По найденным утечкам вносите правки (weak references, cancel subscriptions, close cursors). Повторите тест.
Подробнее о настройке LeakCanary
Подключите через debugImplementation: `debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")`. После запуска при утечке появится notification с полным стеком.Как найти утечку памяти на iOS?
Два шаблона для анализа памяти в Instruments:
Allocations — все выделения памяти, живые и мёртвые объекты. Генерации (кнопка Generation) позволяют сравнить объекты в памяти до и после действия. Если после закрытия экрана объекты этого экрана остались в живой памяти — утечка.
Leaks — автоматический детектор retain-циклов. Красная иконка = найденный цикл. Показывает граф зависимостей с виновниками.
Классический retain-цикл в Swift:
// Утечка: ViewController держит closure, closure захватывает ViewController
class PhotoViewController: UIViewController {
var onPhotoLoaded: (() -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
onPhotoLoaded = {
self.imageView.image = UIImage(named: "photo") // strong capture
}
}
}
// Правильно:
onPhotoLoaded = { [weak self] in
self?.imageView.image = UIImage(named: "photo")
}
[weak self] — стандарт для любых closure, захватывающих self в долгоживущих объектах. Инструмент Leaks это найдёт, но часто показывает симптом, а не причину. Следуем по графу в стек, ищем корневой strong reference.
Согласно Apple Instruments Documentation, использование поколений позволяет выявить до 90% утечек.
Почему изображения съедают память?
UIImage(named:) кэширует изображение в системном кэше. Для часто используемых иконок — хорошо. Для больших фото, которые загружаются один раз — нет. Используем UIImage(contentsOfFile:) — не кэширует.
Декодирование изображения происходит при первом отображении, не при создании UIImage. Предварительное декодирование на background thread:
func decodedImage(_ image: UIImage) -> UIImage {
UIGraphicsBeginImageContextWithOptions(image.size, true, 0)
defer { UIGraphicsEndImageContext() }
image.draw(in: CGRect(origin: .zero, size: image.size))
return UIGraphicsGetImageFromCurrentImageContext() ?? image
}
После этого вызова изображение уже декодировано и лежит в памяти как bitmap. Передаём в UI без задержки на декодировку.
Android: Memory Profiler и LeakCanary
Android Studio Memory Profiler показывает Heap в реальном времени: Java Heap, Native Heap, Stack, Code, Graphics. Кнопка Dump Heap сохраняет снимок — анализируем в hprof viewer или конвертируем для Eclipse Memory Analyzer.
Но самый полезный инструмент в бою — LeakCanary. Подключается в debugImplementation, работает автоматически:
// build.gradle.kts
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")
При обнаружении утечки LeakCanary показывает notification с полным стеком: что удерживает что, через какую цепочку. Не нужно вручную анализировать heap-dump.
Частые причины утечек на Android:
- Context в статических полях или синглтонах.
- Незакрытый Cursor от ContentProvider или SQLiteDatabase.
- Listener, не снятый при onDestroy.
Сравнение инструментов профилирования:
| Платформа | Инструмент | Метод обнаружения | Сложность |
|---|---|---|---|
| iOS | Instruments Allocations | Снимки heap с генерациями | Средняя |
| iOS | Instruments Leaks | Автопоиск retain-циклов | Низкая |
| Android | Memory Profiler | Dump heap + MAT | Высокая |
| Android | LeakCanary | Автоматический мониторинг | Низкая |
| Flutter | DevTools Memory | Snapshot групп объектов | Средняя |
LeakCanary находит утечки в 10 раз быстрее ручного анализа heap-dump. На практике 80% утечек происходят из-за retain-циклов или неправильного управления контекстом.
Типичные утечки и их причины:
| Тип утечки | Платформа | Причина | Решение |
|---|---|---|---|
| Retain-цикл в closure | iOS | Сильный захват self | Использовать weak self |
| Context в статическом поле | Android | Activity передана в синглтон | Использовать Application context |
| Незакрытый StreamSubscription | Flutter | Отсутствие cancel в dispose | Вызвать cancel в dispose |
Что делать с Cursor и подписками?
override fun onStart() {
super.onStart()
locationManager.requestLocationUpdates(provider, 0, 0f, this)
}
override fun onStop() {
super.onStop()
locationManager.removeUpdates(this) // иначе Activity не умрёт
}
Всегда закрывайте Cursor в блоке finally. Подписки на location или sensor снимайте в onStop()/onPause().
Flutter: Observatory и DevTools Memory
Flutter DevTools → Memory tab — snapshot-профилировщик. Показывает группы объектов по типу. Dart:core, package:myapp — смотрим на классы с неожиданно большим количеством экземпляров.
Типичная утечка в Flutter — StreamSubscription без cancel():
class MyWidget extends StatefulWidget { ... }
class _MyWidgetState extends State<MyWidget> {
late StreamSubscription _sub;
@override
void initState() {
super.initState();
_sub = someStream.listen((event) { ... });
}
@override
void dispose() {
_sub.cancel(); // обязательно
super.dispose();
}
}
Без _sub.cancel() в dispose() подписка живёт дольше виджета, удерживает замыкание с ссылкой на State.
Наш подход
Мы проводим полное профилирование с использованием Apple Instruments documentation и LeakCanary official site. Наши инженеры сертифицированы Apple и Google, имеют опыт более 5 лет и лицензии на разработку. После аудита вы получите отчёт с конкретными утечками и готовыми правками кода. Стоимость услуги рассчитывается индивидуально, но экономия от устранения утечек часто окупает её за месяц.
Что входит в услугу:
- Профилирование памяти через Instruments Allocations / Memory Profiler / DevTools
- Настройка LeakCanary для Android-проекта
- Анализ heap-dumps и поиск retain-циклов
- Аудит работы с изображениями (кэш, декодирование)
- Проверка паттернов работы с listeners, subscriptions, closures
- Отчёт с конкретными утечками и правками
Сроки: от 2 до 3 дней в зависимости от размера приложения. Закажите консультацию — мы найдём утечки и предложим готовые правки. Свяжитесь с нами, чтобы получить оценку вашего проекта.







