Процессы и потоки

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

Процессы

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

Манифест позволяет для каждого из элементов <activity>, <service>, <receiver> и <provider> указать атрибут android:process, который указывает имя процесса, в котором компонент должен быть запущен. Вы можете установить данный атрибут для запуска каждого компонента в его собственном процессе или использовать один процесс для нескольких компонентов. Вы можете также установить атрибут android:process для запуска компонентов разных приложений в одном процессе – то есть приложения будут использовать один и тот же идентификатор пользователя Linux и должны быть подписаны одним и тем же сертификатом.

Элемент <application> также поддерживает атрибут android:process, он используется для указания имени процесса для всех компонентов приложения.

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

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

Жизненный цикл процесса

Система Android пытается сохранить процесс приложения как можно дольше, но конце концов иногда требуется удалять старые процессы для освобождения памяти для новых более важных процессов. Чтобы решить какой процесс сохранить, а какой остановить, система строит “иерархию важности”, основываясь на состоянии работающих в процессе компонентов. Процессы с меньшей важностью удаляются первыми, затем следующие за ними, и так далее до освобождения необходимого количества ресурсов.

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

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

Процессы, которые требуются для текущих операций пользователя. Процесс выполняется на переднем плане в следующих случаях:

  • Он содержит явление, с которым взаимодействует пользователь.
  • Он содержит сервис, связанный с явлением, с которым взаимодействует пользователь.
  • Он содержит сервис, который запущен “на переднем плане” с помощью метода startForeground().
  • Он содержит сервис, который выполняет один из методов жизненного цикла (onCreate(), onStart() или onDestroy())
  • Он содержит широковещательный приемник, который выполняет метод onReceive().

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

2.Видимый процесс

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

  • Если он содержит явление не на переднем плане, но еще видимое пользователю (когда выполняется метод onPause()). Это может произойти, например, если на переднем плане явления открылось диалоговое окно, которое лишь частично скрывает явление.
  • Если он содержит сервис, связанный с видимым явлением.

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

3.Процесс сервиса
Процесс, в котором содержится сервис, запущенный с помощью метода startService() и не попадающий в другие две высшие категории. Хотя процесс сервиса напрямую не связан с тем, что видит пользователь на экране, он как правило делает что-то важное для пользователя (проигрывает музыку в фоновом режиме или загружает данные по сети), поэтому система оставляет его, пока хватает памяти для вышестоящих процессов.
4.Фоновый процесс
Процесс, который содержит скрытое явление. Такой процесс на влияет на работу пользователя и система может убить его в любое время. Обычно существует много фоновых процессов и они хранятся в списке LRU (наиболее давно используемые). Если явление правильно реализует методы жизненного цикла и сохраняет свое текущее состояние, остановка его процесса не повлияет на пользовательский интерфейс.
5.Пустой процесс
Процесс, который не содержит компоненты приложения. Единственная причина сохранения таких процессов – это кэширование для ускорения запуска компонента, который должен работать в данном процессе. Система часто уничтожает такие процессы, чтобы сбалансировать общий объем ресурсов системы между кэшем процессов и основным кэшем ядра.

Android устанавливает самый высокий уровень для процесса с учетом компонентов, которые в нем выполняются в настоящее время. Например, если процесс содержит сервис и видимое явление, он получает уроень 2, а не уровень 3.

Кроме того, рейтинг процесса может быть увеличен если другие процессы зависят от него. Например, если поставщик содержимого в процессе А обслуживает клиента в процессе В, или сервис в процессе А связан с компонентов в процессе В – процесс А всегда считается более важным, чем процесс В.

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

Потоки

При запуске приложения, система создает поток в котором оно будет выполняться, называемый “главный поток”. Данный поток очень важен, поскольку он отвечает за обработку событий пользовательских виджетов, включая события прорисовки. Это поток, в котором ваше приложение взаимодействует с компонентами Android UI (компоненты из пакетов android.widget и android.view). Главный поток иногда также называют потоком пользовательского интерфейса.

Система не создает отдельные потоки для каждого экземпляра компонентов. Все компоненты запускаются в одном и том же процессе и создаются в главном потоке, где система система управляет ими. Следовательно, методы обратного вызова системы (например onKeyDown()) всегда запускаются в главном потоке.

Например, когда пользователь нажимает кнопку на экране, главный поток вашего приложения отправляет событие нажатия в виджет, который в свою очередь устанавливает состояние “нажат” и передает запрос аннулирования в очередь событий. Главный поток удаляет событие из очереди и уведомляет виджет, что он должен перерисовать сам себя.

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

Кроме того, инструментарий Android UI не является потокобезопасным. Вы не должны управлять пользовательским интерфейсом из рабочего потока (о нем чуть ниже) – все манипуляции должны производиться в потоке пользовательского интерфейса. Проще говоря есть два правила для модели приложения с одним потоком:

  1. Не блокируйте поток пользовательского интерфейса
  2. Не используйте инструментарий Android UI за пределами потока пользовательского интерфейса.

Рабочие потоки

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

Ниже приведен код для обработчика нажатия, который скачивает изображение из сети в отдельном потоке и показывает его в элементе ImageView:

На первый взгляд кажется, что такой код хорошо работает, поскольку создает новый поток для работы с сетью. Однако, он нарушает второе правило однопоточной модели: не используйте инструментарий Android UI за пределаеми UI потока, а в данном примере содержимое компонента ImageView меняется в рабочем потоке, а не в потоке пользовательского интерфейса. Это может привести к непредсказуемому результату, отладка которого может занять много времени.

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

Например, вы можете исправить код с помощью метода View.post(Runnable) следующим образом:

Такая реализация безопасна: операции с сетью осуществляется в одном потоке, а работа с ImageView в потоке пользовательского интерфейса.

Однако, при возрастании сложности операций, код может быть сложно поддерживать. Для более сложных взаимодействий с рабочим потоком можно использовать объект Handler для обработки сообщений из потока пользовательского интерфейса. Возможно лучшим решением может стать расширение класса AsyncTask, упрощающий выполнение задач рабочего потока, которые должны взаимодействовать с потоком UI.

Использование AsyncTask

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

Для его использования, вы должны создать подкласс AsyncTask и реализовать метод обратного вызова doInBackground(), который запускается в пуле фоновых потоков. Для обновления пользовательского интерфейса, вы должны реализовать метод onPostExecute(), который передает результат из метода doInBackground() и работает в потоке пользовательского интерфейса, благодаря чему вы можете обновить пользовательский интерфейс. Вы можете запустить задачу из потока пользовательского интерфейса с помощью метода execute().

Вы можете реализовать предыдущий пример с использованием AsyncTask следующим образом:

Теперь пользовательский интерфейс работает безопасно, а код стал проще, поскольку он разделяет работу на часть, которую должна выполняться в рабочем потоке и часть, которая должна выполняться в потоке пользовательского интерфейса.

Прочитайте руководство по AsyncTask для полного понимания его работы, а здесь вы приведем краткое описание:

  • Вы можете указать тип параметров, прогресс и конечное значение задачи.
  • Метод doInBackground() выполняется автоматически в рабочем потоке.
  • Методы onPreExecute(), onPostExecute() и onProgressUpdate() вызываются в потоке пользовательского интерфейса.
  • Значение, которое возвращается методом doInBackground() передается в onPostExecute().
  • Вы можете вызывать метод publishProgress() в любое время в методе doInBackground() для выполнения метода onProgressUpdate() в потоке пользовательского интерфейса.
  • Вы можете отменить задачу в любое время их из любого потока.

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

Потоко-безопасные методы

В некоторых ситуациях, методы, которые вы реализуете, могут вызываться более чем из одного потока и следовательно должны обеспечивать безопасность потоков.

Это прежде всего относится к методам, которые могут быть вызваны удаленно – например методы связанного сервиса. Когда метод, реализованный в объекте IBinder вызывается в том же процессе, в котором запущен объект IBinder, метод выполняется в потоке вызывающего объекта. Однако, когда вызов происходит в другом процессе, метод выполняется в одном из потоков процесса IBinder (но не в потоке пользовательского интерфейса). Например, в то время, как метод onBind() будет вызываться из потока пользовательского интерфейса процесса самого сервиса, методы объекта, который вернет onBind() будут вызваны из пула потоков. Поскольку сервис может иметь более одного клиента, может использоваться более одного пула потоков для одних и тех же методов объекта IBinder. Следовательно методы объекта IBinder должны быть реализованы как потоко-безопасные.

Кроме того, поставщики содержимого могут получать запросы данных из других процессов. Хотя классы ContentResolver и ContentProvider скрывают подробности межпроцессного взаимодействия, методы класса ContentProvider, которые используются для ответа на запросы – query(), insert(), delete(), update() и getType() – вызываются из пула потоков процесса поставщика содержимого, но не потока пользовательского интерфейса. Поскольку эти методы могут быть вызваны из множества потоков, они также должны быть реализованы как потоко-безопасные.

Межпросессное взаимодействие

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

Для выполнения межпроцессного взаимодействия, ваше приложение должно связаться с сервисом, используя метод bindService(). Подробную информацию смотрите в разделе Сервисы.

Добавить комментарий