Pull to refresh

Кому жить, а кому умереть: приоритеты процессов в Android

Reading time 7 min
Views 34K
Original author: Ian Lake
Примечание переводчика: при переводе старался максимально пользоваться терминологией, которую предлагает сам Google в русскоязычной версии документации по Android, таким образом «service» стал «службой», «content provider» стал «поставщиком контента», и так далее. А вот «activity» стать «операцией» так и не смог — не пересилил я себя. Извините.

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


Иерархия процессов в Android


Если вы обратитесь к документации, то обнаружите, что самые важные процессы называются процессами переднего плана, за ними следуют видимые процессы, служебные процессы, фоновые и, наконец, пустые процессы. Кстати, заметьте, что хотя говорить мы будем о компонентах Android-приложений (службах, Activity, и так далее), Android всегда уничтожает процессы, а не компоненты. Разумеется, это никак не противоречит сборке мусора (в результате которой в систему возвращается память, занимаемая объектами, на которые больше никто не ссылается), но это тема для отдельного поста.

image

Процессы переднего плана


Вы наверняка думаете, что то, с чем взаимодействует пользователь в любой отдельно взятый момент времени является тем компонентом системы, который уничтожать никак нельзя (по крайней мере пока пользователь продолжает свою работу), и вы абсолютно правы. Одно но: «то, с чем взаимодействует пользователь в любой отдельно взятый момент времени» — слегка нечёткое определение. Одним из компонентов, попадающих в эту категорию, является Activity переднего плана — та, в которой onResume() уже вызван, а onPause() — ещё нет.

Пока одни Activity полагаются в своей работе только на самих себя, другие перекладывают часть её на привязанные службы (bound services). Любой процесс, содержащий службу, привязанную к Activity переднего плана, получает от системы точно такой же приоритет, как и процесс, содержащий саму Activity переднего плана. И это правильно: если Activity переднего плана полагает, что для её работы необходимо держать постоянное соединение со службой, то держать эту службу живой и невредимой в интересах как самой Activity, так и Android. Тот же самый принцип применяется и к поставщикам контента (content providers).

Но кто сказал, что Activity — это единственный компонент, исчезновение которого тут же заставит пользователя негодовать? Я вот точно бы рассвирепел, если бы моя музыка внезапно прекратила играть, или же подсказки от моей навигационной системы растворились бы в тумане. К счастью, Android позволяет службам уведомить систему о том, что они обладают высоким приоритетам через вызов метода startForeground(). Вызов этого метода является наилучшим способом обеспечить фоновое проигрывание музыки, а, что касается других задач, то перед тем как вызвать startForeground(), нужно спросить себя: «А пользователь точно сразу же заметит то, что моя служба прекратила свою работу?» Службы переднего плана должны использоваться только в критических случаях, тогда, когда прерывание работы станет сразу же очевидным.

Примечание: службы переднего плана требуют, чтобы вы размещали уведомление, которое сообщало бы пользователю о том, что служба работает. Если вам кажется, что в вашем конкретном случае уведомление не нужно, то, наверное, служба переднего плана вам и не нужна (это нормально, есть и другие способы обеспечить работу в фоновом режиме, о которых будет рассказано далее).

Есть несколько других случаев, в которых приоритет процесса временно повышается до приоритета процесса переднего плана. К ним относится выполнение службой следующих методов жизненного цикла: onCreate(), onStartCommand() и onDestroy(). Выполнение приёмником широковещательных сообщений (broadcast receiver) метода onReceive() тоже относится к ним же. Это повышение приоритета необходимо для того, чтобы сделать данные методы жизненного цикла атомарными и дать возможность каждому компоненту выполнить их без того, чтобы быть уничтоженным системой.

Видимые процессы


Так, стоп, я же уже рассказал про Activity переднего плана? Рассказал, но Android в неисповедимой мудрости своей позволяет возникать таким ситуациям, когда ваша Activity является видимой, но не находится на переднем плане. Такое может случиться, когда Activity переднего плана стартует другую Activity, тема которой наследуется от Dialog. Или когда стартуемая Activity является полупрозрачной. Или когда вы вызываете системный диалог с запросом у пользователя тех или иных разрешений (который, на самом деле, является Activity!).

Activity является видимой от вызова onStart() до вызова onStop(). Между этими двумя вызовами можно делать всё, что ожидается от видимой Activity: обновление экрана, и так далее.

Кстати, процессы, в которых находятся привязанные службы и поставщики контента видимых Activity тоже получают такой же приоритет как и процесс с видимой Activity (подобно тому, как это происходит у Activity переднего плана), и по тем же причинам: чтобы гарантировать, что эти зависимые процессы не будут уничтожены системой.

Обратите внимание на следующий момент: то, что ваш процесс является видимым, не гарантирует того, что он не будет уничтожен системой. Если процессы переднего плана будут требовать под свои нужны много памяти, есть вероятность того, что Android все-таки пойдёт на крайние меры и прихлопнет видимый процесс. Для пользователя это будет выглядеть следующим образом видимая Activity, находящаяся под Activity переднего плана, будет замещена чёрным экраном. Разумеется, если вы правильно пересоздаёте убиенную Activity, ваш процесс и ваша Activity будут созданы заново без потери состояния как только Activity переднего плана будет уничтожена.

Примечание: одной из причин того, что результат startActivityForResult() обрабатывается в onActivityResult(), а результат requestPermissions() — в onRequestPermissionsResult(), а не в функциях обратного вызова, как раз-таки и является возможность уничтожения видимого процесса — если весь ваш процесс будет уничтожен, то и все существующие в нем функции обратного вызова будут уничтожены тоже. Поэтому, если вы видите библиотеки, использующие подход с функциями обратного вызова, знайте: они могут работать не так, как вам хочется, в случае нехватки памяти в системе.

Служебные процессы


Если ваш процесс не попадает ни в одну из вышеназванных категорий, но в нём есть запущенная служба, то ваш процесс получает приоритет служебного процесса. Такие случаи типичны для многих приложений, которые осуществляют фоновую работу (например, загрузку данных), но результаты этой работы не являются настолько важным, как в случае служб переднего плана.

И эти процессы не являются такими уж бесполезными, как может показаться. Для большого круга задач такой процесс является наилучшим способом выполнения фоновых задач, так как он будет уничтожен только если в видимых процессах и процессах переднего плана происходит столько всего, что нужно срочно откуда-то взять память.

Обратите особое внимание на константу, возвращаемую вами из onStartCommand(), так как она определяет поведение системы в случае, если ваш процесс будет уничтожен при нехватке памяти:

  • START_STICKY подразумевает, что система возродит вашу службу тогда, когда это будет возможно, но она не будет заново посылать службе последний ею полученный Intent (например, вы сами можете восстановить состояние, или у вас есть самописный жизненный цикл, определяющий, что делать при старте и останове службы).
  • START_REDELIVER_INTENT предназначается для служб, которые хотят запускаться заново с тем же самым Intent'ом, который был получен ими в onStartCommand()
  • START_NOT_STICKY пригодится службам, которые необязательно перезапускать, если они тихонько растворились в тумане. Такое поведение может быть полезно для служб, выполняющих такие периодические задачи, выполнение которых можно на время опустить.


Фоновые процессы


Допустим, что у вас была Activity переднего плана, и пользователь нажал на кнопку Home, что привело к вызову onStop(). Если кроме этой Activity у вас больше не было ничего, что позволяло бы и дальше сохранять высокий приоритет вашего процесса, то ваш процесс переходит в разряд фоновых процессов. Они, кстати, и занимают большую часть памяти устройства, на случай, если пользователь решит потом вернуться к одному из ранее открытых приложений.

Android не уничтожает процессы направо и налево, так как запуск их с нуля довольно ресурсозатратная операция. Поэтому они могут оставаться в памяти некоторое время, перед тем как быть уничтоженными, если на горизонте появится новый высокоприоритетный процесс. Уничтожение происходит в порядке вытеснения давно неиспользуемых: первыми уничтожаются те процессы, которые не использовались более всего. Так же как и в случае с уничтожением видимых процессов/Activity, нужно уметь правильно пересоздать Activity без потери данных.

Пустые процессы


Как и в любой иерархии, всегда есть самый низкий уровень. В пустых процессах нет ни одного активного компонента, и Android может уничтожить их в любой момент, но обычно они всё-таки удерживаются в памяти какое-то время (это опять к слову об эффективном использовании памяти, которое не заключается в том, чтобы вычищать всё подряд).

На что стоит обратить внимание


Хотя мы и говорили о приоритетах процессов в терминах того, какие компоненты запущены в ваших процессах, помните, что иерархия существует на уровне процессов, а не компонентов. Один лишь компонент (например, служба переднего плана) переведёт весь ваш процесс в категорию процессов переднего плана. Хотя большинство приложений прекрасно живут в одном процессе, нужно помнить, что если жизненный цикл вашего приложения может сильно отличаться от запуска к запуску, или же в приложении есть как очень тяжелая задача, так и долго выполняющаяся, но легковесная, причем первая не зависит от второй, то есть смысл выделить два процесса. Пусть тяжёлая задача выполняется в первом процессе, а легковесная во втором, и тогда в случае нехватки памяти тяжёлая может быть приостановлена, а легковесная продолжит свою работу.

Важно также помнить, что приоритет вашего процесса определяется тем, что происходит на уровне компонентов. Это означает, что если вы запустите очень важную задачу, которая должна выполняться долгое время, в отдельном потоке, но из-под Activity, то вас ждёт неприятный сюрприз, когда ваш процесс внезапно станет фоновым. Используйте доступные вам инструменты (например, службы, или службы переднего плана), чтобы гарантировать, что система в курсе того, что вы делаете.

Не забывайте о других и помните о вашем пользователе


Создавайте ваше приложение таким образом, чтобы оно постоянно работало на том уровне приоритета, который ему нужен — не больше и не меньше. Помните также, что вы как разработчик имеете на руках устройство, которое намного мощнее слабейших устройств конечных пользователей, и из-за этого вы можете никогда не увидеть своими глазами уничтожение видимого процесса (а служебного — и подавно), но это не значит, что этого никогда не происходит.

Хоть я и рекомендую обычно покупать для тестирования как можно более слабое устройство, вы можете потестировать поведение своего приложения при его уничтожении даже на своём флагмане. Чтобы уничтожить своё приложение (вместе со всеми его процессами), выполните следующее:

adb shell am force-stop com.example.packagename

Если у вас несколько процессов, вы можете сначала найти PID нужного вам процесса, посмотрев на вторую колонку (то есть первое число) результата следующей команды:

adb shell ps | grep com.example.packagename

И потом прихлопнуть этот процесс:

adb shell kill PID

Это будет первым шагом к тому, чтобы добиться корректной работы приложения на большинстве устройств, вне зависимости от того, какие ограничения по памяти будут возникать.
Tags:
Hubs:
+16
Comments 4
Comments Comments 4

Articles