Ваши пользователи жалуются на то, что приложение очень быстро сажает заряд телефона? Запущенный фоновый сервис внезапно останавливается? Сообщения от FCM не доходят до пользователя? Что связывает эти три серьезных вопроса? Ответ прост — неверно выстроенная работа с энергопотреблением приложения.
Давайте разберемся в основных моментах, связанных с этой темой. Возможно, это позволит вам в будущем избежать ошибок, с которыми сталкивалось большинство разработчиков мобильных приложений.
В интернете огромное количество разрозненной информации, собрать которую в единое общее руководство было одной из основных целей этой статьи.
Общая информация
В Android есть следующие платформенные фичи для оптимизации энергопотребления:
- Doze and App Standby
- App Standby Buckets
- Background restrictions
- Power management restrictions
- Testing and troubleshooting
В Android 6 появились две фичи для сохранения заряда батареи за счет управления поведением приложений, когда устройство не на зарядке:
- Doze Mode.
- App Standby.
Doze Mode
Когда устройство находится в режиме Doze, доступ приложений к определенным ресурсам откладывается до появления окна обслуживания (maintenance window). Список конкретных ограничений.
Если пользователь оставляет на какое-то время устройство отключенным от зарядки и с выключенным экраном, то оно переходит в режим Doze. В этом режиме система пытается сохранить заряд батареи, ограничивая доступ приложений к сетевым и ресурсоемким службам, откладывает Jobs, синхронизацию и Alarms.
Периодически система выходит из режима Doze, чтобы приложения могли выполнить отложенные действия. Во время этого окна обслуживания (maintenance window) система запускает все отложенные синхронизации, Jobs, Alarms и позволяет приложениям получить доступ к сети.
Со временем система все реже и реже планирует maintenance windows, что помогает снизить расход энергии, когда устройство не на зарядке.
В режиме Doze к приложениям применяются следующие ограничения:
- Доступ в сеть приостановлен.
- Стандартные AlarmManager откладываются до следующего окна обслуживания.
- Система не сканирует Wi-Fi.
- Система не позволяет запускаться sync adapters.
- Система не позволяет запускаться JobScheduler.
Чеклист для приложения в режиме Doze:
- Использовать FCM для обмена сообщениями.
- Если пользователь должен сразу увидеть уведомление, то нужно использовать FCM с высоким приоритетом.
- Предоставлять достаточное количество информации в сообщении, чтобы избежать последующих запросов в сеть.
- Установить критически оповещения с setAndAllowWhileIdle() and setExactAndAllowWhileIdle().
- Протестировать приложение в режиме Doze.
App StandBy, App StandBy Buckets
App StandBy позволяет системе определить, что приложение простаивает, когда пользователь не пользуется им активно. App StandBy запускается, когда не выполняется ни одно из следующих условий:
- Пользователь явно запускает приложение.
- Приложение находится на переднем плане (явно или в качестве Foreground service, либо используется другой Activity).
- Приложение генерирует уведомления, которые пользователь видит на экране блокировки или в области уведомлений.
- Приложение является активным приложением администратора устройств.
Когда устройство подключается к зарядке, система выпускает приложения из режима Standby, что позволяет им выполнять любые задачи. Если устройство не используется в течение длительного периода времени, система предоставляет бездействующим приложениям доступ в сеть примерно раз в день.
Определение частоты использования отличается у разных производителей, особенно «жестит» Samsung.
В Android 9 появились новые фичи для управления питанием устройства. Они делятся на две категории:
- App standby buckets. Система ограничивает доступ приложения к ресурсам устройства в зависимости от модели поведения пользователя.
- Battery Saver Improvements. Когда включена функция экономии заряда батареи, система накладывает ограничения на все приложения.
Эти ограничения применяются ко всем приложениям независимо от их targetSdk
.
App StandBy Buckets помогает системе приоритизировать запросы приложений к ресурсам на основании того, как давно и как часто использовалось приложение. На основе шаблонов использования приложение помещается в один из пяти сегментов. Система ограничивает ресурсы устройства, доступные для каждого приложения, в зависимости от того, в каком сегменте находится приложение.
Пять сегментов, назначаемые приложениям в зависимости от приоритета:
- Active. Приложение находится в активном сегменте, если пользователь в настоящий момент использует приложение. Т.е. если видна Activity, или запущен Foreground service, или есть synchronized adapter, связанный с приложением на переднем плане, или пользователь кликнул на уведомление. Если приложение в активном сегменте, то никакие ограничения на использование ресурсов устройства не накладываются.
- Working set. Приложение находится в этом сегменте, если часто запускается, но в данный момент не активно. Система накладывает умеренные ограничения на действия этого приложения.
- Frequent. Приложение находится в этом сегменте, если используется часто, но не каждый день. Система накладывает больше ограничений, также накладываются ограничения на количество сообщений FCM с высоким приоритетом.
- Rare. Приложение находится в этом сегменте, если оно редко используется. В этом случае система накладывает строгие ограничения и на получение сообщений FCM с высоким приоритетом. Система также ограничивает возможность приложения подключаться к интернету.
- Never. Это сегмент для приложений, которые были установлены, но никогда не запускались. Система накладывает жесткие ограничения.
Каждый производитель может установить свои критерии присвоения неактивных приложений к сегментам.
Для определения сегмента, в который система поместит приложение, используется машинное обучение. С его помощью прогнозируется поведение пользователя. Например, если приложение из сегмента Rare было только что использовано и перешло в Active, то это не означает, что после использования приложение поднимется в более приоритетный сегмент. Сегменты определяются на основе прогнозов будущих действий пользователя, а не на основе недавнего использования.
Полезная информация по работе с App StandBy Buckets:
- НЕ пытаться манипулировать тем, к какому сегменту система отнесет приложение.
- Создать Launcher Activity, если ее нет.
- Создавать обработчик нажатий на уведомления. Если с ними нельзя взаимодействовать, то приложение не сможет перейти в активный сегмент.
- Если приложение не показывает пользователю уведомление при получении high-priority FCM-уведомления, то пользователь не сможет взаимодействовать с приложением, и оно не перейдет в активный сегмент. Если многие сообщения будут помечены как high-priority, то приложение исчерпает свою квоту на такие сообщения, и все последующие будут иметь normal-priority.
Firebase Cloud Messaging с App StandBy и режимом Doze
Необходимо использовать FCM для взаимодействия с приложением во время простоя устройства. FCM оптимизирован для работы в режимах ожидания Doze и App StandBy с помощью высокоприоритетных FCM-сообщений. Высокоприоритетные сообщения позволяют разбудить приложение для доступа к сети, даже если устройство находится в режиме Doze или приложение в режиме App StandBy. В обоих режимах система доставляет сообщение и дает приложению временный доступ к сетевым сервисам, а затем возвращает устройство или приложение в режим ожидания.
Как протестировать приложение с различными ограничениями системы
Тестирование Doze Mode
- Получить доступ к ADB (android device bridge) в текущей сессии:
export PATH=«~/Library/Android/sdk/platform-tools»:$PATH
- Перевести систему в режим ожидания:
adb shell dumpsys deviceidle force-idle
- Выйти из режима ожидания:
adb shell dumpsys deviceidle unforce
- Активировать устройство:
adb shell dumpsys battery reset
- Проверить поведение приложения.
Тестирование приложения с App StandBy для Android < 9
- Перевести приложение в App StandBy:
$ adb shell dumpsys battery unplug
$ adb shell am set-inactive <package_name> true
- Пробудить приложение:
$ adb shell am set-inactive <package_name> false
$ adb shell am get-inactive <package_name>
- Проверить работу приложения. Убедиться, что восстанавливается корректно. Проверить, продолжают ли работать уведомления и фоновые процессы.
Тестирование App Standby Buckets
Можно вручную переместить приложение в определенный App StandBy bucket с помощью команды:
adb shell am set-standby-bucket <package_name> active|working_set|frequent|rare
Команда проверки, в каком сегменте сейчас приложение:
adb shell am get-standby-bucket <package_name>
Тестирование ограничений на фоновые процессы
- Вручную применить ограничения на выполнение фоновых задач:
adb shell cmd appops set <package_name> RUN_ANY_IN_BACKGROUND ignore
- Убрать ограничения на выполнение фоновых процессов:
adb shell cmd appops set <package_name> RUN_ANY_IN_BACKGROUND allow
Тестирование режима Battery safety
- Отключить устройство от ПК:
- Проверить поведение устройства в условиях экономии энергии:
adb shell settings put global low_power 1
- Отменить ручную настройку:
adb shell dumpsys battery reset
Фоновые оптимизации.
Ограничения, начиная с Android 7:
- Не отправляются широковещательные сообщения `CONNECTIVITY_ACTION`, если receiver объявлен в манифесте. Если receiver зарегистрирован динамически, то сообщение будет получено.
- Приложения не могут получать или отправлять `ACTION_NEW_PICTURE` или `ACTION_NEW_VIDEO`.
Ограничения, начиная с Android 9:
Если система замечает, что приложение потребляет чрезмерное количество ресурсов, она уведомляет пользователя и дает ему возможность ограничить действия приложения. Это поведение включает в себя:
- Чрезмерные wake locks.
- Избыточное количество фоновых сервисов.
Точные ограничения определяются производителем устройства.
Battery Historian
Инструмент Battery Historian дает представление о расходе заряда батареи. Инструмент визуализирует связанные с энергопотреблением события и предоставляет разнообразные данные, которые могут помочь вам определить поведение приложения, разряжающего батарею.
Анализ приложения с помощью Battery Historian
Предварительно необходимо установить Docker.
- Получить доступ к ADB (android device bridge) в текущей сессии:
export PATH=«~/Library/Android/sdk/platform-tools»:$PATH
- Подключить устройство к ПК.
- Убить текущий ADB-сервер.
adb kill-server
- Проверить доступные устройства:
adb devices
- Сбросить данные о батарее:
adb shell dumpsys batterystats --reset
- Отключить устройство и пройти по выбранному вами сценарию использования приложения.
- Подключить устройство.
- Проверить, что устройство подключено:
adb devices
- Сделать дамп данных батареи:
adb shell dumpsys batterystats > [path/b]batterystats.txt
- Создать отчет для данных:
adb bugreport [path/]bugreport.zip
- Запустить (порт можно указать любой):
docker run -p 5554:5554 gcr.io/android-battery-historian/stable:3.0 --port 5554
- В браузере перейти по ссылке http://localhost:5554 и открыть ZIP файл.
- Вот так примерно будет выглядеть график BatteryHistorian:
На этом графике вы можете узнать, в какой момент запустился ваш сервис, установились wake locks, был запущен JobScheduler и другую информацию. Возможно, вы даже узнаете о своем приложении то, чего еще не знали и о чем не подозревали. Уделите этому инструменту пару-тройку свободных часов и гарантирую, вы не пожалеете.
Energy Profiler
Energy Profiler — встроенный в Android Studio анализатор энергопотребления. Думаю, тут не стоит задерживаться. Этот инструмент довольно хорошо описан, и каждый может оценить его в действии.
BatteryStats + UI-тесты
В этой главе мы разберем, как можно использовать связку из BatteryStats и UI-тестирования.
- Перед запуском теста я написал bash-скрипт:
echo Write test class path e.g. <путь_к_классу_с_тестом>
read testName
export PATH=«~/Library/Android/sdk/platform-tools»:$PATH
adb shell dumpsys battery unplug
adb shell dumpsys batterystats --reset
adb shell am instrument -w \ -e class $testName \ com.myapp.test/androidx.test.runner.AndroidJUnitRunner
adb shell dumpsys batterystats | awk -f BatteryStatsParseScript.awk > BatteryTestsResult.txt
adb shell dumpsys batterystats > BatteryTestsResultFull.txt
adb shell dumpsys batterystats reset
echo You can find the output file in the parent directory named BatteryTestsResult.txt
- Для начала нужно ввести расположение класса с тестом. Например, класс `com.myApp.MyTestEspressoTest`.
- Далее подключается ADB.
- Устройство отключается от ПК.
- Сбрасывается статистика BatteryStats.
- Запускается тест, подставляет класс, введенный нами ранее, и используемый фреймворк для тестирования.
- Выгружается информация об энергопотреблении и парсится в более читаемый формат с помощью .awk-файла. Далее этот файл сохраняется под именем BatteryTestsResultFull.txt в главной папке приложения (или в любой другой, которую вы выберете).
- Выводится сообщение с расположением файла с результатом.
- Сбрасывается статистика BatteryStats.
- Вы восхитительны!
Для парсинга файла, получившегося после теста, применяется .awk-файл. Сам файл я решил не прикладывать, т.к. он получился огромным, и не все будут использовать те же поля, что использовал я. В результате получаем текстовый BatteryTestsResult.txt такого содержания:
Estimated battery capacity: 3700 mAh
Time on battery: 32s 609ms (100.0%) realtime, 32s 610ms (100.0%) uptime
App Uid u0a358
Cpu Usage: 1.56 mAh
Radio Usage: mAh
WiFi Usage: 0.0476 mAh
Wake Usage: mAh
Sensor Usage: mAh
GPS Usage: 0.0417 mAh
Total App Usage: 1.65mAh
Total time in seconds: 32 seconds
Usage per second: 0.0515625 mAh/seconds
User activity: 14 touch
Wi-Fi network: 335.22KB received, 342.84KB sent (packets 745 received, 758 sent)
Результат более удобочитаемый, что стандартный файл BatteryStats. При желании вы можете добавить необходимые поля для анализа либо вместо .awk-файла использовать регулярные выражения.
P.S. Проблемы с Samsung
При написании статьи я наткнулся на полезный сайт https://dontkillmyapp.com, на котором можно узнать, какие ограничения накладывают различные производители на энергопотребление устройств. Самой частой проблемой, с которой я сталкивался, была жалоба пользователей Samsung на высокое энергопотребление различными приложениями. И на этом ресурсе я нашел ответ на свой вопрос.
Вместе с релизом Samsung S8 была представлена утилита для увеличения времени работы батареи под названием App Power Monitor. И чтобы приложения работали корректно, их нужно вносить в whitelist. Также Samsung — рекордсмен по убийству приложений благодаря его «Адаптивной батарее».
На сайте есть рекомендации для разработчиков по обходу ограничений, но в случае с данным производителем:
Чтобы до конца понять, как работает и от каких факторов зависит энергопотребление Android-приложения, одной статьи, конечно, недостаточно. Но надеюсь, что я выполнил свою главную цель — заинтересовать вас этой темой, и вы сможете оптимизировать работу с энергопотреблением. На этом у меня все.
deinlandel
Столько препон простым разработчикам для того, чтобы их приложения могли нормально работать в фоне.
А Google Play Services как жрали батарею в три горла, так и жрут. Но им можно, они же системные!
Tangeman
Обычно приложениям (если это не мессенжер или навигатор) вообще не нужно работать в фоне, так что я лично руками и ногами за полное убивание фона кроме явно отмеченных (и хорошо обоснованных) приложений.
Всякие календари, будильники и напоминалки могут ставить таймер для просыпания (и то на короткое время), пуши тоже сами будят кого надо, работа в фоне для этого не нужна, и больше я кейсов для работы в фоне не вижу. Те кто сможет убедить пользователя — получат галочку "работай", а не смогут — пусть спят и не используют ресурсы, которых и так мало даже на high-end устройствах (батарейка в частности).
У меня Google Play Services вообще не видны, если на телефон ничего фонового не ставить (и убить всё остальное принудительно) — то он почти не жрёт ничего пока спит (Galaxy S7 — минимум неделя до 10%), а планшет (Galaxy Tab S6) с прибитым фоном (кроме гугля) вообще неделями может в standby находится (последний раз 20 дней сна с остатком в примерно 60%).
sumanai
У меня на данный момент не рутованный телефон (жду анлока), и «Система андроид» куда-то сожрала целых 13% батареи за 3 часа с последней зарядки, хотя я полчаса смотрел Аниме и час слушал музыку. Вот зачем оно в фоне что-то делало?
Как рутану, посношу всё нафиг, на старом телефоне никаких левых процессов не было, только активные приложения, экран да сеть, всё остальное в пределах погрешности.
Tangeman
То есть полчаса аниме и час музыки вы не считаете потребителем батарейки, она у вас расходуется исключительно в фоне?
sumanai
Полчаса аниме и час музыки я считаю целевым потреблением батареи. Полчаса видео это 25% на сам плеер и 25% на экран, вполне себе ожидаемое потребление для сложной задачи. 15% на музыку так же не вызывает сильного удивления, задача лёгкая, но не даёт устройству полностью уснуть. По 7% на сеть и режим ожидания… В принципе сойдёт, сигнал у меня дома действительно так себе.
А вот пусть даже 13%, но не понятно на что, вот это меня печалит.
И да, я не особо жалуюсь на батарею, 5000 мАч в принципе позволяет хоть игрушку утром запустить, и дотянуть до вечера, но блин, всё равно не хочется тратить заряд на удовлетворение внутренних потребностей софта от Google.
Tangeman
Почему вы думаете что это исключительно софт от гугла? В "Android System" показываются не только расходы самой системы и софта от гугла, туда вообще могут попасть "расходы" любого софта которые использует систему (скажите спасибо неточной системе учёта). То есть в зависимости от вендора, версии и ещё может быть фаз Луны может получится так что условный навигатор попадёт в учете энергии в Android System, а не сам на себя. Использование сенсоров и сети тоже не всегда отражается там где должно, к сожалению, и без рута этого не увидеть (а иногда даже и с рутом).
Если у вас абсолютно голый телефон без ничего (кроме софта от гугла) и он жрёт 13 процентов за три часа в спящем режиме, ещё и с учётом такой монстрообразной батарейки — да, тогда это (вероятно) проблема гугла — но если это не телефон гугла (судя по батарейке — не он) — то с вероятностью 95% это косяки вендора.
То есть сам по себе андроид вполне даже умеет экономить батарейку, но вот в чистом виде он редко бывает в природе, вендоры любят косячить.
sumanai
Извиняюсь, наверное ввёл в заблуждение. 13% это от общего потребления, батарейка просела процента на 3 (и ещё через 3 часа ни капли не потратилась).
Собственно это и буду исправлять с рутом. Всякие msa да многозначительные Analytics явно не для моей пользы стоят. Да и гугловские сервисы порежу. На прошлых телефонах после допиливания у меня никакие «Система Android» в принципе не отсвечивали, старичок SGS2 мог 2 недели пролежать, несмотря на не самый энергоэффективный процессор и батарею в смешные по нынешним временам 1450 мАч.
deinlandel
А есть приложения, которым надо работать в фоне вполне легально (gps трекеры например), я как раз такое разрабатываю. Там есть большое уведомление в статусбаре, чтобы всегда быть в курсе фоновой работы, и большой тумблер, который позволяет эту работу в фоне выключить. Никто не просит давать любому приложению в фоне делать что угодно. Всё, о чём разработчики умоляли Гугл — это дать возможность попросить у пользователя право работать в фоне. В ответ гугл такой возможности не дали и продолжили закручивать гайки. Кроме того, сертификацию гугла проходят всякие веселые «кастомизации» системного апи от Huawei и Xiaomi, которые убивают приложения даже вручную добавленные в список исключений или имеющие foreground сервисы.
Вот еще отличный пример:
www.androidpolice.com/2020/07/24/covid-19-tracing-apps-may-fail-to-notify-exposed-users-due-to-aggressive-oem-battery-saving-measures
>У меня Google Play Services вообще не видны,
А у меня видны, и у многих моих знакомых и пользователей — тоже. Достаточно вбить в гугл «google play services drains battery», чтобы убедиться, что проблема массовая. Причём отключить эту дрянь никак нельзя, потому что все правила энергосбережения на гугловые сервисы не распространяются. Системное приложение ведь всегда работает хорошо и не может жрать батарейку просто так!
Tangeman
Про это я сказал отдельно. Хотя даже для трекера есть возможность получать только изменения позиции от сервисов, а не мониторить их постоянно через GPS (иногда этого достаточно, и гораздо более экономно).
Как я уже сказал выше, если они видят Google Play Services в расходах батарейки — это не значит что сами сервисы её расходуют. Любое приложение (системное или нет), которое использует эти сервисы (включая GPS) может туда попасть "за компанию".
Да, я не спорю что проблема массовая, но если бы это была проблема сервисов, то удаление стороннего приложения (у меня однажны таковым оказалось Sleep as Android) не привело бы к снижению расходов в самих сервисах (а именно там они и отображались, вместо самих себя).
Что ещё более забавно — GSam Battery Monitor как-то тоже сошёл с ума и стал жрать батарейку в фоне, при этом пеняя на систему — но его переустановка решила вопрос.
Так что не всегда виноват гугл или его сервисы, но люди верят тому что видят, а то что у них стоит ещё 100500+ приложений (системных или нет) которые на самом деле могут быть виноваты, никого не волнует.
sumanai
В смысле нельзя? Рут и отключаем. Телефон без рута я вообще за свой телефон не считаю, даже фоток не лью, ибо кто его знает, куда он их сольёт. А гугл активно сливает, даже при коннекте через сотовую сеть целый мегабайт слил неизвестно на что, а уж сколько льёт через WiFi (который стандартными средствами не ограничивается), даже в сказке не описать.
Neikist
Беда в том что даже с этим флагом некоторые вендоры любят приложения убивать. И пофиг им как на мнение разработчика, так и на мнение пользователя который приложению все нужные флаги выставил для возможности более полноценной работы в фоне.