Додаток не крешиться одразу — він поступово зростає в пам'яті. Через 20 хвилин використання з'являється легка задумливість. Через 40 — системний Memory Pressure вбиває фонові процеси. Через годину — SIGKILL від iOS або OOM-кілер Android завершує додаток. Користувач думає, що додаток «глючить». Firebase Crashlytics нічого не покаже — це не крэш, це вбивство системою. Навіть якщо Memory Warning спрацьовує, UI може стати гальмівним через скидання кешів, що веде до негативних відгуків. За статистикою, 80% мобільних додатків мають витоки пам'яті, які призводять до втрати 15% користувачів на місяць. Ми, як мобільні розробники з 10-річним досвідом, щодня стикаємося з такими кейсами. Наші інженери знаходять витоки там, де стандартні інструменти мовчать. Економія на серверних витратах може сягати 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, мають досвід понад 10 років та ліцензії на розробку. Після аудиту ви отримаєте звіт з конкретними витоками та готовими правками коду. Вартість послуги розраховується індивідуально, але економія від усунення витоків часто окупає її за місяць.
Що входить в послугу:
- Профілювання пам'яті через Instruments Allocations / Memory Profiler / DevTools
- Налаштування LeakCanary для Android-проекту
- Аналіз heap-dumps та пошук retain-циклів
- Аудит роботи з зображеннями (кеш, декодування)
- Перевірка паттернів роботи з listeners, subscriptions, closures
- Звіт з конкретними витоками та правками
Терміни: від 2 до 3 днів залежно від розміру додатку. Замовте консультацію — ми знайдемо витоки та запропонуємо готові правки. Зв'яжіться з нами, щоб отримати оцінку вашого проекту.
Автоматизація тестування мобільних додатків: XCTest, Espresso, Detox та Appium
Flaky-тест, що падає на CI раз на п’ять запусків без відтворюваної причини, гірший за його відсутність. Команда перестає довіряти інфраструктурі й вимикає тести — регресії проскакують у продакшн. Ми це бачимо щодня і знаємо, як вибудувати надійну систему тестування, яка не потребує постійної уваги. Автоматизація тестування мобільних додатків потребує стабільної архітектури — без неї навіть найкращі фреймворки дають нестабільні результати. Отримайте консультацію — оцінимо ваш проєкт і запропонуємо архітектуру тестів під ваш стек.
Чому flaky-тести небезпечні?
Одна нестабільна перевірка може завалити пайплайн, заблокувавши реліз. Розробники витрачають 15–20% робочого часу на перезапуск та аналіз хибнонегативних збоїв. Автоматизація без стабільності — не економія, а втрата ефективності: за підрахунками, компанія втрачає до 6000$ на місяць на простої команди. Ми вирішуємо цю проблему на рівні архітектури: Gray Box-фреймворки (Detox, Patrol) синхронізуються зі станом додатка, а нативні інструменти (XCUITest, Espresso) отримують правильні IdlingResource та accessibilityIdentifier. Результат: стабільність >99.5% на CI — це в 3 рази краще за середній показник по ринку. Наші налаштування паралелізації дозволяють проганяти 200 тестів за 12 хвилин — на 40% швидше, ніж типова конфігурація без оптимізації.
Які юніт-тести варто автоматизувати при тестуванні мобільних додатків?
На iOS XCTest — основа. Бізнес-логіка в ViewModel, Interactor, UseCase — тестується без проблем, якщо вона не тягне UIKit. Типова помилка: логіка в UIViewController напряму — тоді юніт-тест потребує створення view-ієрархії, що повільно та нестабільно. Вихід — виносити логіку в сервіси з @testable import.
Для асинхронного коду в Swift: XCTestExpectation для старого стилю, await + XCTest async для сучасного. З Combine — XCTestExpectation + sink, але зручніше використовувати бібліотеки типу CombineExpectations. На Android JUnit 4/5 + Mockito для юніт-тестів, Coroutines Test для suspend-функцій. runTest {} з kotlinx-coroutines-test — стандарт для ViewModel з StateFlow. Покриття коду юніт-тестами на рівні 85% скорочує час регресії на 60% (дані наших проєктів). Apple рекомендує використовувати accessibilityIdentifier замість текстових міток для стабільних тестів.
Чому стабільність UI-тестів важливіша за покриття?
XCUITest (iOS) та Espresso (Android) — нативні UI-тести. Працюють швидко, інтегровані з IDE, але тестують одну платформу. Головна проблема XCUITest — крихкість селекторів. app.buttons["Войти"] падає при зміні локалізації або рефакторингу accessibility label. Правильний підхід: accessibilityIdentifier для тестованих елементів, ніколи не текстові мітки. Ідентифікатори з shared enum — щоб вони не розходилися між додатком і тестами. Досвід показує: така практика знижує flakiness на 90%.
Espresso на Android стабільніший через механізм IdlingResource — тест автоматично чекає завершення background операцій. Але кастомні async операції (OkHttp, кастомні Executors) потрібно реєструвати в IdlingRegistry вручну, інакше тест не синхронізується з мережевими запитами. Ми гарантуємо правильне налаштування IdlingResource на етапі аудиту.
Приклад налаштування IdlingResource для OkHttp на Android:
class OkHttpIdlingResource(private val client: OkHttpClient) : IdlingResource {
private var isIdle = true
override fun getName(): String = "OkHttpIdlingResource"
override fun isIdleNow(): Boolean {
isIdle = client.dispatcher.runningCallsCount() == 0
return isIdle
}
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {}
}
// Реєстрація в тестовому підготовчому коді:
IdlingRegistry.getInstance().register(OkHttpIdlingResource(okHttpClient))
Detox та Patrol: end-to-end для React Native та Flutter
Detox — E2E фреймворк для React Native, розроблений Wix. Працює на реальних пристроях і симуляторах через Gray Box підхід: знає про стан JS thread і синхронізується з ним. Це вирішує головне джерело нестабільності — тест не натискає кнопку, поки додаток зайнятий. Налаштування Detox нетривіальне. Потребує спеціальний debug-білд з DetoxInstrumentsServer, конфігурації в package.json і окремого Appium-сервера не потрібно. Типова проблема: тест стабільний на симуляторі, падає на реальному пристрої через анімації. Рішення — animations: disabled в Detox конфігурації для E2E білда.
Patrol — аналог для Flutter. Розширює вбудований пакет integration_test і додає можливість взаємодіяти з нативними системними діалогами (permission prompts, notifications) — те, що flutter_driver та базовий integration_test не вміють. Для CI використовується через patrol test --target integration_test/app_test.dart.
Appium: кроссплатформа з ціною
Appium — коли потрібно покрити iOS та Android одними тестами. Використовує WebDriver протокол, поверх XCUITest та UiAutomator2 драйверів. Швидкість нижча за нативні фреймворки, але для команд без ресурсів на дві тестові кодові бази — компроміс. Appium 2.x з плагінною архітектурою помітно зручніший за першу версію. appium-doctor діагностує оточення — корисний при налаштуванні CI.
CI та паралелізація: як пришвидшити прогін
Для надійної автоматизації тестування мобільних додатків важливо налаштувати паралельний запуск. Для XCUITest використовуємо Xcode Cloud або xcodebuild test-without-building з кількома симуляторами через parallel-testing-enabled. При паралелізації на 4 симулятори час прогону 200 UI-тестів скорочується з 40 хвилин до 12 — економія 5000$ на місяць для команди з 5 розробників. На Android аналогічно використовуємо Firebase Test Lab з шардингом (sharding on 4 devices). Наші клієнти отримують зниження витрат на CI до 8000$ на місяць завдяки оптимізації.
| Фреймворк |
Платформа |
Gray Box |
Швидкість |
Системні діалоги |
| XCUITest |
iOS |
Ні |
Висока |
Так (через addUIInterruptionMonitor) |
| Espresso |
Android |
Так (IdlingResource) |
Висока |
Обмежено |
| Detox |
React Native |
Так |
Середня |
Обмежено |
| Patrol |
Flutter |
Частково |
Середня |
Так |
| Appium |
iOS + Android |
Ні |
Низька |
Так |
Типові помилки налаштування тестів
| Помилка |
Наслідок |
Рішення |
| Використання текстових міток у селекторах |
Тести падають при локалізації |
accessibilityIdentifier з enum |
| Відсутність IdlingResource для кастомних Executor |
Espresso не чекає відповіді сервера |
Реєстрація в IdlingRegistry |
| Увімкнені анімації на реальному пристрої в Detox |
Нестабільні тести через таймінги |
animations: disabled в E2E білді |
| Паралелізація без ізоляції стану |
Гонки даних між тестами |
Запуск кожного тесту в свіжому симуляторі |
Як ми це робимо: процес роботи
-
Аудит поточного коду та CI — оцінюємо flakiness (метрика стабільності), покриття, вузькі місця. Використовуємо Allure для збору звітів і Xcode Report для iOS.
-
Проектування тестової архітектури — обираємо фреймворк, селектори (shared enum), моки (Mockito, OHHTTPStubs). Визначаємо шари: unit → UI → E2E.
-
Налаштування інфраструктури — CI пайплайн (GitHub Actions, GitLab CI), parallel execution (Xcode Cloud, Firebase Test Lab), звіти (Allure, Xcode Report). Додаємо метрики: час прогону, кількість flaky-тестів.
-
Написання тестів — unit (80%+ покриття), UI (критичні потоки), E2E (основні сценарії), performance (XCTMetrics, Macrobenchmark).
-
Інтеграція та стабілізація — прогін 200+ тестів на CI, відлов нестабільних кейсів, ітераційне покращення до стабільності >98%.
-
Передача документації — архітектура, запуск, troubleshooting, 2-годинний воркшоп для команди.
Що входить в роботу (deliverables)
- Архітектурна документація тестового покриття
- Налаштований CI-пайплайн з паралелізацією та звітами (Allure, Xcode Report)
- Код тестів (unit, UI, E2E) з styleguide (наприклад, Google iOS Test Style)
- Навчання команди (2 години воркшопу)
- Доступ до тестових білдів та CI-логів
- Підтримка протягом місяця після здачі (фікс flakiness, оновлення під нові версії)
Терміни орієнтовно
Налаштування інфраструктури з нуля (CI, unit + UI тести, звіти) — 2–3 тижні. Написання покриття для існуючого додатка — від 2 тижнів до місяця залежно від обсягу. Оцінимо ваш проєкт за 2 дні — зв’яжіться з нами. 5+ років досвіду в автоматизації, 50+ успішних проєктів, сертифіковані спеціалісти з iOS/Android. Гарантуємо стабільність тестів >98% на CI після впровадження. Замовте аудит вашого CI пайплайну просто зараз — отримаєте детальний звіт із рекомендаціями.