Секрет долголетия вашего приложения
Интро
Закрывая приложение из недавних мы ожидаем, что оно будет убито. И чаще всего так он и было. Но в какой-то момент что-то пошло не так, и многие начали замечать, что настройки, которые должны были примениться после перезапуска приложения, игнорируются. Раньше такого точно не было и этому свидетельствуют жалобы на новое поведение.
Оказалось, что процесс приложения продолжает работать. И теперь, чтобы точно перезапустить приложение, нужно останавливать его в настройках. В первую очередь подумал, что это новая секретная оптимизацией недавних обновлений системы, так как при живом процессе onCreate
у Application
не вызывается повторно, а скорость запуска приложения ускоряется в разы. И оптимизацию скорее всего сделали в Android 14 и в свежих релизах. У меня на руках два устройства: Pixel 7 и Pixel 6 Pro. Оба с Android 14, но на Pixel 6 Pro нет такой оптимизации, — приложение умирает при удалении из недавних — но и сборка там февральская, когда как на Pixel 7 сентябрьская сборка и оптимизация присутствует.
Исследуем
Решил проверить не только наше приложение, и оказалось, что оптимизация работает не для всех. Например, сработала для Telegram и ряда банков, но не сработала, например, для Tooba. Для последнего в логах вывелось сообщение Killing 6505:site.tooba.android/u0a410 (adj 905): remove task
, которого не было для других.
Стремясь понять всю цепочку событий начал исследование издалека, а именно с момента смахивания приложения из списка недавних. Происходит это в RecentsView, в котором в момент завершения анимации вызывается метод removeTaskInternal
. Он делегирует вызов в ActivityManagerWrapper
.
Делегирование на этом не заканчивается - обертка делегирует вызов в сервис IActivityTaskManager
.
Единственной реализацией этого сервиса является ActivityTaskManagerService
, который делегирует вызов в один из методов ActivityTaskSupervisor
.
Тут не сильно важно, что вызовется в итоге, — removeTask
или removeRootTask
— потому что removeRootTask
для простого случая начнет в цикле вызывать removeTask
для всех листьев. Он же останавливает запущенные Activity
и далее вызывает метод cleanUpRemovedTask
. В этом методе выполняются три интересных действия:
- Очистка сервисов в
cleanUpServices
- Проверка, что
Activity
завершились. Если нет, то убийство откладывается на 1000ms. После чего будет вызван методkillTaskProcessesIfPossible
- Убийство, если это возможно, в методе
killTaskProcessesIfPossible
В killTaskProcessesIfPossible
, происходит несколько проверок, к которым есть описание в виде комментариев и убийство снова делегируется в ActivityManagerInternal
путем отправки сообщения.
ActivityManagerInternal
реализует внутренний класс LocalService
, который лежит внутри ActivityManagerService
(не путать с ActivityTaskManagerService
). И вот наконец мы пришли к месту, где начинает формироваться наш лог с сообщением "remove task".
Но, кроме этого, видно, что процесс будет убит, если выполняются условия:
В ином случае мы откладываем убийство установкой флага в ProcessRecord
. Теперь возникает вопрос: кто убьет приложение позже, так как никакое отложенного сообщение тут не отправляется?
Знакомьтесь, OomAdjuster. В задачи OomAdjuster входит определение состояния процесса, оценка oom_adj с предварительной сортировкой в CacheOomRanker, а так же он умеет замораживать и размораживать процессы, используя CachedAppOptimizer
. Все эти процессы запускаются по различным триггерам по типу запуска какого-то компонента или его завершение. Даже смерть процесса - это триггер, который прилетает в OomAdjuster
из ActivityManagerService
и инициируется в слушателе состояния процесса AppDeathRecipient
.
Убивать OomAdjuster
умеет в случаях если приложение долго остается замороженным/закешированным или в случае, когда установлен флаг waitingToKill
, который мы ранее видели в ActivityManagerService#LocalService
, и выполнены те же самые условия. Поэтому получается, что флаг waitingToKill
отложит убийство до возникновения любого триггера у OomAdjuster
.
Теперь осталось понять, что именно изменилось в этих классах за последнее время. Долго искать не пришлось - нашелся такой коммит, который попал в основную ветку в феврале 2024, а сделан был еще в ноябре в 2023.
Изменения в коммите минимальные - перед убийством добавили проверку того, что имеются started сервисы в ActivityManagerService#LocalService
и OomAdjuster
.
Забавно, что коммент перед установкой флага ожидания, не обновили.
Сначала, изменение показалось странным, ведь ранее мы встречали такой метод как cleanUpService, где должна была происходить остановка сервисов аналогично тому, что рядом происходит остановка активностей. Оказалось, что метод делегируется в ActivityServices, который останавливает сервисы только в том случае, если у них в AndroidManifest.xml
указан android:stopWithTask="true"
. В ином случае запускается большая цепочка событий определения того, когда стоит перезапустить сервис и с какими аргументами, но остановки сервиса не будет, следовательно, и остановки процесса приложения.
Эксплуатируем
Мы можем проверить гипотезы, попыткой воспроизвести проблему в приложении с одним сервисом. И, о чудо, приложение не умирает. Иногда 2 минуты, а иногда все 10.
При этом сам сервис OomAdjuster и ActivityServices уже убили ввиду его бездействия.
А вот с февральской сборкой на Pixel 6 Pro поведение не получается повторить - туда этот коммит не попал. Так же не получается повторить с флагом stopWithTask=true
.
Теперь стало интересно как долго можно сохранять приложение живым. Для этого сделал приложение, в котором сервис стартует и периодически делает какую-то работу. Но есть еще вариант заставить приложение жить еще дольше, выкинув в onDestroy
сервиса ошибку, чтобы процесс завершения сервиса не был успешным никогда. А затем эту ошибку можно поймать с помощью Thread.setDefaultUncaughtExceptionHandler
, чтобы у нас не упало приложение. Но есть проблема, и она заключается в том, что из-за того, что мы выкинули ошибку в onDestroy
, пользователь через некоторое время увидит диалог об ANR. И, если он не нажмет "Close App", то мы продолжим жить. Правда он будет получать этот диалог с определенной периодичностью.
Кроме этого, если пользователь зайдет в приложение, то оно у него не запустится дальше Splash Screen. Эту проблему можно исправить, перезапустив Looper
в Exception Handler, но это все равно не поможет отвязаться от диалога с ANR.
Тут можно вспомнить, что кроме Service
, который предотвращает убийство на некоторых версиях Android 14, есть еще BroadcastReceiver
. Поэтому попробовал отправлять сообщение в моменте закрытия приложения и получил такой же эффект - приложение не будет убито. Кроме этого, мы, конечно, не можем запускать сервис из фона, но никто не запрещал отправлять широковещательное сообщение, поэтому вполне рабочий вариант отправлять новое сообщение в том же самом BroadcastReceiver
.
Но если отправлять сообщения слишком часто, то система в OomAdjuster
нас пометит как "excessive processes" и прибьет, потому что мы выполняем слишком много транзакций через Binder
(хотя это не так уж часто возникает). И чтобы решить эту проблему, можно делать какую-либо работу или подобрать такой таймаут, чтобы и система нас не прибила, и ANR не вылез.
Так же можно поступить для Service
в Android 15 - выполнять долгую работу в onDestroy
и отправлять в конце сообщение BroadcastReceiver
. Но работу в этом случае мы можем выполнять до 200 секунд и не получать ANR.
Ну и вишенкой будет запуск фонового сервиса при получении сообщения BOOT_COMPLETED
в BroadcastReceiver
. Экспериментально получилось убедиться, что Android такое позволяет делать.
Итог
Изменение, найденное случайным образом, привело к тому, что был найден способ поддерживать живым приложение очень долго. Благодаря этому приложение будет готово к запуску всегда, а его запуск будет приравниваться к запуску обычной Activity
. В моем случае, приложение работало несколько часов до момента, пока устройство не ушло в глубокий сон. И все это без Activity
, без Foreground Service
и без permission'а на работу в фоне.
Я вынес код в отдельную библиотеку "Philosopher's Stone" и рядом добавил пример приложения, которое можно скачать в виде apk и протестировать сколько оно проживет на вашем устройстве. Для этого запустите приложение, а затем смахните его из списка недавних. Позже, периодически открывая и закрывая активность, вы будете видеть как долго работает приложение.
Опубликовано в Полуночные Зарисовки