Пользователи не думают об оптимизации кода. Они просто замечают, что после часа в вашем приложении телефон нагрелся и потерял 20% заряда. Дальше — одна звезда и отзыв «убивает батарею». В App Store такие отзывы топят рейтинг быстрее, чем баги.
По данным Apple, приложения в топ-категориях расходуют в среднем на 30–40% меньше энергии, чем аутсайдеры в тех же категориях. Это не совпадение — это результат осознанной работы с энергопотреблением.
Сначала измеряй, потом оптимизируй
Самая частая ошибка — начинать оптимизацию «вслепую», по ощущениям. Xcode даёт два инструмента, которые нужно использовать до любых изменений.
Energy Gauge — в Debug Navigator (⌘+7) есть вкладка с молнией. Она показывает текущее потребление CPU, сети, геолокации и экрана в реальном времени. Уровни: Low / Medium / High / Very High. Если приложение стоит на паузе, а уровень всё равно High — где-то фоновая утечка.
Instruments → Energy Log — более глубокий инструмент. Запишите сессию работы с приложением и посмотрите временную шкалу. Видно, когда именно потребление растёт и какой компонент виноват. Полезные шаблоны: «Energy Usage», «Network», «Location».
Правило простое: профилируй на реальном устройстве, не в симуляторе. Симулятор не отражает реальное энергопотребление от железа.
Сеть: главный батарейный вор
Радиомодуль — один из самых энергоёмких компонентов iPhone. Каждый раз, когда приложение поднимает соединение, тратится энергия на активацию антенны. И дело не только в объёме трафика — дело в количестве обращений.
Batching запросов. Если у вас 10 маленьких API-запросов в минуту, лучше объединить их в один. Аналитику не нужно отправлять после каждого события — копите события в очередь и отправляйте пачкой раз в несколько минут.
URLSession и конфигурация. Используйте URLSessionConfiguration.background для загрузок, которые не нужны прямо сейчас. iOS сама выберет момент, когда отправить данные — например, когда устройство подключено к Wi-Fi и заряжается.
let config = URLSessionConfiguration.background(withIdentifier: "com.app.background")
config.isDiscretionary = true // iOS выберет удобное время
let session = URLSession(configuration: config)
Параметр isDiscretionary = true — ключевой. Он говорит системе: «Я не спешу, сделай это когда выгодно». Apple рекомендует его для любых некритичных загрузок.
Кэширование. Не запрашивай то, что уже есть. URLCache работает из коробки, но многие разработчики его не настраивают. Укажи разумные лимиты и политику — и сетевых запросов станет меньше.
HTTP/2 и multiplexing. Если сервер поддерживает HTTP/2, несколько запросов идут по одному соединению. Радиомодуль остаётся в менее активном состоянии дольше.
Геолокация: осторожно, жрёт всё
GPS — один из самых энергоёмких модулей в телефоне. Приложения, которые держат CLLocationManager постоянно активным с точностью kCLLocationAccuracyBest, могут разряжать батарею в фоне за несколько часов.
Первый вопрос: а вам вообще нужна точная геолокация? Для большинства задач — нет.
Уровни точности и их цена:
kCLLocationAccuracyBestForNavigation — максимум, только для навигации в реальном времени
kCLLocationAccuracyBest — очень высокая точность, высокое потребление
kCLLocationAccuracyNearestTenMeters — 10 метров, умеренное потребление
kCLLocationAccuracyHundredMeters — 100 метров, низкое потребление
kCLLocationAccuracyKilometer / ThreeKilometers — почти не расходует батарею
Для приложения, которое показывает ближайшие кафе, точность в 100 метров — вполне достаточно. Используйте минимально необходимую.
Significant location changes. Если нужно знать только город или район — используйте startMonitoringSignificantLocationChanges() вместо обычного мониторинга. Обновления приходят только при смене вышки сотовой связи. Потребление — минимальное.
Останавливайте геолокацию, когда она не нужна. Вызов stopUpdatingLocation() в момент, когда пользователь свернул приложение — обязательная практика. Многие забывают об этом.
func applicationDidEnterBackground(_ application: UIApplication) {
locationManager.stopUpdatingLocation()
}
Region monitoring вместо постоянного трекинга. Если нужно знать, что пользователь пришёл в точку А — используйте startMonitoring(for: CLCircularRegion(...)). iOS сама разбудит приложение, когда это произойдёт, и вам не нужно постоянно опрашивать GPS.
Фоновые задачи: не злоупотребляй
iOS жёстко ограничивает фоновые процессы — и это хорошо. Но разработчики иногда просят у системы больше, чем нужно.
BGTaskScheduler — современный способ планировать фоновую работу. Есть два типа задач:
BGAppRefreshTask — короткие обновления (до 30 секунд), iOS решает когда их запустить
BGProcessingTask — более длинные операции, можно запрашивать Wi-Fi и зарядку
BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.app.refresh", using: nil) { task in
// выполни работу
task.setTaskCompleted(success: true)
}
Запрашивайте requiresExternalPower = true для тяжёлых задач — iOS запустит их только при зарядке:
let request = BGProcessingTaskRequest(identifier: "com.app.heavyTask")
request.requiresExternalPower = true
request.requiresNetworkConnectivity = true
Главное правило: не держи фоновые задачи живыми дольше необходимого. Вызови setTaskCompleted как только закончил.
CPU: меньше — лучше
Высокая нагрузка на процессор = горячий телефон = быстрая разрядка. Тут несколько типичных проблем.
Таймеры без ума. Timer.scheduledTimer с маленьким интервалом в фоне — классическая ошибка. Если обновляешь UI — делай это только когда приложение активно. Используй CADisplayLink для привязки к частоте экрана вместо произвольных таймеров.
Лишние вычисления на главном потоке. Тяжёлые операции — парсинг JSON, обработка изображений, криптография — нужно выносить в фоновый поток. Иначе блокируется UI, растёт нагрузка на CPU, тратится батарея.
DispatchQueue.global(qos: .utility).async {
let result = heavyComputation()
DispatchQueue.main.async {
self.updateUI(with: result)
}
}
Используй qos: .utility или qos: .background вместо .userInitiated для некритичных задач — это снижает приоритет и расход энергии.
Metal и GPU. Если делаешь обработку изображений или сложные математические вычисления — GPU часто эффективнее CPU по соотношению производительность/ватт. Metal даёт прямой доступ. Но это нужно только там, где действительно есть нагрузка — не усложняй ради усложнения.
Экран: не рисуй лишнего
Дисплей — второй по прожорливости компонент после радиомодуля. Несколько правил.
Dark Mode. На OLED-экранах (iPhone X и новее) чёрный пиксель не светится вообще — это буквально ноль потребления. Поддержка тёмной темы через traitCollection.userInterfaceStyle — это не только UX, но и реальная экономия батареи на современных устройствах.
Частота обновления. ProMotion-дисплеи (120 Гц) потребляют больше, чем 60 Гц. iOS адаптивно снижает частоту, но если ваши анимации постоянно держат экран на максимуме — это заметно на батарее. Используй CADisplayLink.preferredFrameRateRange для контроля там, где высокая частота не нужна.
Избегай overdraw. Рисовать один пиксель несколько раз — лишняя работа для GPU. В Instruments есть инструмент «Core Animation», который показывает перекрывающиеся слои. Минимизируй прозрачность там, где она не нужна.
Сенсоры: выключай когда не используешь
Акселерометр, гироскоп, магнитометр, барометр — каждый из них тратит энергию. Типичная ошибка: запустить CMMotionManager в начале работы приложения и забыть остановить.
// Плохо: запустили и забыли
motionManager.startAccelerometerUpdates(to: .main) { data, error in
// ...
}
// Правильно: выключаем когда не нужно
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
motionManager.stopAccelerometerUpdates()
}
Частота опроса тоже важна. accelerometerUpdateInterval = 0.1 (10 Гц) против 0.01 (100 Гц) — разница по потреблению существенная. Выбирай минимально необходимую частоту для задачи.
Push-уведомления вместо polling
Если приложению нужно реагировать на события с сервера — используй push, а не периодические запросы. Polling раз в минуту — это 60 сетевых запросов в час, 60 активаций радиомодуля.
APNs (Apple Push Notification service) разбудит приложение только когда пришло реальное событие. Это экономит и батарею, и трафик.
Если push по каким-то причинам не подходит — хотя бы увеличь интервал polling до минимально приемлемого. Иногда проблема не в клиенте, а в том, как спроектирован сервер. Для приложений, которым нужна серверная часть с push и интеграциями, команда REEXY (r3xy.ru) делает API и бэкенд-интеграции — от 1 500 ₽. Правильно спроектированный сервер сам по себе снимает половину проблем с батареей на клиенте.
Мониторинг в продакшне
Оптимизация без метрик — это работа вслепую. MetricKit (доступен с iOS 13) даёт реальные данные о потреблении с устройств пользователей.
class AppDelegate: UIResponder, UIApplicationDelegate, MXMetricManagerSubscriber {
func applicationDidFinishLaunching(_ application: UIApplication) {
MXMetricManager.shared.add(self)
}
func didReceive(_ payloads: [MXMetricPayload]) {
for payload in payloads {
// payload.cpuMetrics, payload.networkTransferMetrics и т.д.
print(payload.jsonRepresentation())
}
}
}
MetricKit собирает данные агрегированно и отдаёт раз в сутки. Смотри на MXCPUMetric, MXNetworkTransferMetric, MXLocationActivityMetric — они дают реальную картину того, как приложение ведёт себя на разных устройствах у разных пользователей.
Xcode Organizer тоже показывает Battery Usage Reports — агрегированную статистику по всем установкам. Если видишь всплеск — ищи что изменилось в последнем релизе.
Что делать прямо сейчас
Если не знаешь с чего начать — вот короткий список:
- Открой Instruments, запиши сессию с Energy Log. Найди топ-3 источника потребления.
- Проверь, что
CLLocationManager останавливается при сворачивании приложения.
- Посмотри на все
Timer — нет ли тех, что работают в фоне без нужды.
- Включи
isDiscretionary = true для некритичных сетевых задач.
- Поддержи Dark Mode если ещё не сделал.
Большинство проблем с батареей — это не архитектурные просчёты, а несколько конкретных мест, где разработчик не подумал о том, что происходит когда приложение не на экране. Профилируй, смотри на цифры, фикси точечно.