Мобильная разработка
Memory Management
Январь 2014, разработчики Instagram получают багрепорт: при долгом скроллинге ленты на iPhone 5s приложение крашится через ~15 минут. Crash log показывает OOM (Out Of Memory). Команда тратит две недели, чтобы найти источник: closure в каждой фото-ячейке захватывает ViewController через self, и при reuse cell старая ссылка остаётся в памяти. После добавления `[weak self]` в 47 closures - крах исчезает. Это типичная история retain cycle в Swift, повторяющаяся в любой crowded-feed приложении. На Android аналог - 'утечка Activity при повороте экрана', добившая до 70% crashes в early-versions WhatsApp. Memory management - не academic тема. Это то, что отделяет приложение, которое работает 8 часов в день, от приложения, которое нужно перезапускать каждые 20 минут.
- **Instagram (Meta)** - после рефакторинга closures с `[weak self]` в 2014 уровень OOM crashes упал на 38%; LeakCanary интегрирован в debug builds Android приложения для отлова утечек регрессии.
- **Spotify** - на старых Android с 1 GB RAM использует object pooling для UI элементов плейлиста, чтобы избежать GC pauses при скроллинге; 0 allocations per frame в hot path.
- **Apple Photos** - autoreleasepool блоки в batch-обработке Library Migration; без них импорт 100K фотографий невозможен из-за peak memory.
Утечки памяти: retain cycles и context leaks
Приложение iOS Photos падает на iPhone 15 Pro Max с 8 GB RAM. На сцене - 30 фотографий 4K из ленты, и каждая держит 50 MB ссылку через closure capture self. После 200 прокруток отображённых ячеек ARC не освобождает 30*200 = 6000 объектов из памяти, потому что closure захватывает strong-ссылку на ViewController. Это retain cycle - классическая утечка iOS, появляющаяся в любой closure-heavy кодовой базе. На Android аналог - утечка Context: AsyncTask с неявной ссылкой на Activity, которая удерживает целое окно после поворота экрана. Утечки не приводят к мгновенному краху - они накапливаются, постепенно ухудшая производительность из-за GC pressure, и приводят к OOM-краху через десятки минут использования. Symptom: 'у меня всё хорошо, но через 20 минут приложение тормозит и крашится'.
Weak vs unowned в Swift: weak обнуляется в nil при освобождении объекта, unowned остаётся 'dangling' ссылкой - использование приведёт к crash. Правило: weak для опциональных связей (delegate), unowned для гарантированно живущих дольше (self в captured closure внутри объекта).
Почему retain cycle с closure не отлавливается ARC автоматически?
Профилирование памяти: Instruments и Profiler
Утечку нельзя 'почитать в коде' - её нужно найти эмпирически. iOS-инструменты: Instruments из Xcode, два важных инструмента - Leaks (детектор unreachable объектов) и Allocations (трекинг allocations за период). Сценарий профилирования: запустить Leaks, выполнить тестовый кейс 10 раз (открыть-закрыть экран), проверить - после возврата на исходный экран счётчик объектов класса ScreenViewController должен быть 0. Если 10 - утечка. На Android: Android Studio Profiler - вкладка Memory. Аналогичная техника: capture heap dump перед и после, сравнить - какие классы выросли. Дополнительно LeakCanary - библиотека от Square, автоматически детектирующая утечки Activity/Fragment через WeakReference + idle handler.
Heap snapshot diff - наиболее эффективная техника поиска утечек. Сделать snapshot 1 -> выполнить операцию N раз -> snapshot 2. В diff видно: какие классы выросли на N инстансов. Большинство утечек становятся очевидными за минуты этого метода.
Heap snapshot diff показывает, что после 10 открытий-закрытий экрана класс DetailViewController вырос на 10 инстансов. Какой первый шаг в диагностике?
ARC в iOS: автоматический reference counting
ARC (Automatic Reference Counting) - компиляторная техника управления памятью в Swift и Objective-C, появившаяся в 2011 году. Компилятор анализирует код и автоматически вставляет вызовы `retain` (увеличить счётчик ссылок) и `release` (уменьшить). Когда счётчик достигает 0, объект освобождается синхронно. Преимущества над GC: детерминированное время освобождения (на момент release, не 'когда-нибудь'), нет stop-the-world пауз, низкая overhead в hot loop. Недостатки: ARC не умеет ловить циклы ссылок (см. retain cycle ранее), требует ручной разметки weak/unowned. Эффективность: на iPhone 15 Pro Max release одного объекта - ~10 наносекунд. В сравнении: Hotspot JVM GC при размере heap 200 MB - ~10-50 миллисекунд stop-the-world паузы. iOS-приложения чувствуют это как 'непрерывную плавность' против 'спорадического jitter' в Android.
Autorelease pool: NSAutoreleasePool в Objective-C и @autoreleasepool в Swift откладывают release до конца блока. Используется для batch-операций: вычисление, создающее тысячи временных объектов внутри loop, освобождает их не сразу, а по выходу из autoreleasepool - что критично для долгих фоновых задач.
Почему ARC даёт более 'плавный' UX, чем GC, при равных параметрах железа?
GC в Android: ART и поколения
Android (ART - Android Runtime) использует tracing garbage collector с поколениями (generational GC). Объекты делятся на young (молодые, недавно созданные) и old (пережившие несколько сборок). Young generation собирается часто, дёшево (минор GC, ~1-2 мс) - большинство объектов умирают молодыми (lambda-замыкания, temporary string, results of method calls). Old generation собирается редко, дорого (major GC, 10-50 мс) - тут живут долгоживущие объекты как Activity, ViewModel. ART добавил concurrent collector (с Android 8.0): большая часть работы делается в фоне, stop-the-world пауза сокращается до 1-3 мс. Тем не менее, разработчик может ухудшать ситуацию: создавая много temporary objects в onDraw (вызывается 60 раз в секунду), пишет в Bitmap.Config.ARGB_8888 вместо RGB_565 (удваивает использование heap), не освобождает Drawable resources.
GC pressure - не то же самое, что утечка. Утечка - объекты, которые должны быть освобождены, но нет. GC pressure - много короткоживущих allocations, заставляющих GC работать постоянно. На каждое создание Vector2 объекта в game loop накапливается работа GC. Решение: object pooling - переиспользовать объекты вместо создания новых.
ARC (iOS) и GC (Android) - просто разные реализации одной идеи 'автоматического управления памятью'
ARC - компиляторная техника со spread-out стоимостью и detrministic timings; GC - рантайм-техника с batch-стоимостью и недетерминированными паузами; продакт-следствия разные
ARC требует от разработчика дисциплины (weak/unowned, autoreleasepool), но даёт предсказуемость. GC прощает большее (нет циклов), но привносит stop-the-world паузы. Это влияет на UX: 'плавность iOS' и 'случайные подвисания Android' - часто следствие именно архитектурного различия памятиМенеджмента.
В чём ключевое различие между утечкой памяти и GC pressure на Android?
Ключевые идеи
- **Утечки памяти** - retain cycles в Swift (closures с strong self), context leaks в Android (AsyncTask держит Activity); лечатся weak/unowned и привязкой к lifecycle.
- **Профилирование** - heap snapshot diff (Instruments на iOS, Profiler на Android, LeakCanary автоматически); первый шаг диагностики - retain tree.
- **ARC** - компиляторная техника, детерминированная, без stop-the-world, но требует weak/unowned для циклов; autoreleasepool для batch.
- **ART GC** - generational + concurrent, мiniмизирует паузы, но чувствителен к GC pressure от излишних allocations; лечится object pooling.
Связанные темы
Memory management - фундамент производительности мобильных приложений:
- Оптимизация рендеринга — GC pauses напрямую ломают плавность 60 fps; allocations в onDraw - типичная причина jank на Android, обсуждавшаяся в предыдущем уроке
- Многопоточность в мобильных приложениях — Threading и memory tied: dispatch_async с captured self - частая причина retain cycle; правильный async-pattern требует понимания ownership
Вопросы для размышления
- Возврат к мотивации: команда Instagram потратила 2 недели на поиск утечки. Какой workflow можно было бы внедрить с самого начала, чтобы такие утечки находились автоматически?
- В каких случаях reference counting (ARC) явно проигрывает tracing GC (ART) с точки зрения программиста, и почему Apple всё равно выбрала ARC?
- Если ваше приложение работает на 'железе' от 1 GB RAM (бюджетные Android) до 16 GB RAM (iPhone Pro), какие memory-стратегии должны быть universal, а какие - адаптивные?
Связанные уроки
- mob-12 — Оптимизация рендеринга зависит от memory management
- mob-03 — Swift ARC - конкретная реализация memory management для iOS
- mob-06 — Android GC - альтернативный подход memory management
- mob-17 — Memory leaks обнаруживаются через профилировщики в тестах
- gd-18 — Memory management в играх решает те же проблемы что и в мобильных
- alg-01-big-o — Space complexity - Big-O для памяти, основа анализа
- arch-08-memory-hierarchy