Ваши пользователи жалуются на то, что приложение очень быстро сажает заряд телефона? Запущенный фоновый сервис внезапно останавливается? Сообщения от FCM не доходят до пользователя? Что связывает эти три серьезных вопроса? Ответ прост — неверно выстроенная работа с энергопотреблением приложения.


Давайте разберемся в основных моментах, связанных с этой темой. Возможно, это позволит вам в будущем избежать ошибок, с которыми сталкивалось большинство разработчиков мобильных приложений.


В интернете огромное количество разрозненной информации, собрать которую в единое общее руководство было одной из основных целей этой статьи.


image


Общая информация


В Android есть следующие платформенные фичи для оптимизации энергопотребления:



В 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-приложения, одной статьи, конечно, недостаточно. Но надеюсь, что я выполнил свою главную цель — заинтересовать вас этой темой, и вы сможете оптимизировать работу с энергопотреблением. На этом у меня все.