Программирование Android Процессы и потоки Fri, March 29 2024  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.

Процессы и потоки Печать
Добавил(а) microsin   

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

В этом документе рассматривается, как процессы и потоки работают в приложении Android (перевод документации [1]). Чтобы лучше понять материал, рекомендую ознакомиться со статьей [2], где описывается Activity - важный компонент программы Android. Все непонятные термины и сокращения ищите в Словарике [6].

Для начала стоит коротко рассмотреть, что такое процесс (Process) и что такое поток (Thread), почему их разделили в отдельные понятия. И процесс, и поток относятся к работающему коду, которое выполняется операционной системой Android. Процесс более широкое логическое понятие, олицетворяющее работающее приложение или службу. Процесс может объединять в себе несколько потоков, т. е. в одном процессе может работать несколько потоков (в процессе должен быть как минимум 1 поток), т. е. без потока не может быть работающего процесса. Поток - это элементарная единица, выполняющая какие-то действия.

[Процессы (Process)]

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

Запись в манифесте для каждого типа элемента компонента — < activity >, < service >, < receiver > и < provider > — поддерживает атрибут android:process, который указывает процесс, в котором этот компонент должен работать. Вы можете установить этот атрибут так, что каждый компонент будет работать в своем собственном процессе, или так, что некоторые компоненты будут разделять процесс с другими, в то время как другие не будут этого делать. Вы также можете установить android:process так, чтобы компоненты различных приложений работали в одном и том же процессе — при условии, что приложения используют один и тот же идентификатор пользователя (Linux user ID), и что они подписаны одними и теми же сертификатами.

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

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

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

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

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

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

1. Foreground process (процесс верхнего уровня)

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

• Если в этом процессе размещена активность (Activity [2]), в которой работает пользователь (в Activity был вызван её метод onResume()).
• Если в этом процессе размещена служба (Service [4]), связанная с активностью, с которой взаимодействует пользователь.
• Если в этом процессе размещена служба (Service), запущенная "на верхнем уровне" (running "in the foreground") — служба вызвала startForeground().
• Если в этом процессе размещена служба (Service), которая выполняет один из методов обратного вызова (callback), обслуживающих жизненный цикл активности (onCreate(), onStart() или onDestroy()).
• Если в этом процессе размещен BroadcastReceiver, который выполняет свой метод onReceive().

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

2. Visible process (видимый процесс)

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

• Если в этом процессе размещена активность (Activity [2]), которая не находится на переднем плане, но все еще видима для пользователя (был вызван метод onPause() активности). Это может произойти, например, если foreground-активность запустила диалог, в результате чего запустившая диалог активность переместилась на экране на задний план, но все еще видна за окном диалога.
• Если в этом процессе размещена служба (Service), связанная с активностью на переднем плане или видимой (foreground или visible).

Visible-процесс считается очень важным, и он не будет прибит, пока это не потребуется для сохранения работоспособности foreground-процессов.

3. Service process (процесс службы)

Процесс, в котором работает служба, запущенная методом startService(), и которая не попадает в одну из предыдущих более важных категорий (Foreground или Visible). Поскольку процессы службы могут быть не привязаны напрямую к тому, что сейчас видит пользователь на экране, они обычно делают вещи, которые все же нужны пользователю (такие как фоновое проигрывание музыки или загрузка данных через сеть). Поэтому система сохранит работу Service-процессов, пока есть ресурсы для работы всех более важных (foreground и visible) процессов.

4. Background process

Если буквально перевести название, то получится "процесс на заднем плане". Это процесс, в котором размещена активность, которая сейчас невидима для пользователя (был вызван метод onStop() активности). Эти процессы не влияют напрямую на работу с пользователем, и система убьет их в любой момент, когда нужно освободить память для процессов видов foreground, visible или service. Обычно есть множество запущенных на заднем плане процессов, и они удерживаются в списке LRU (least recently used, используемые недавно). Список LRU нужен для того, чтобы можно было прибивать Background-процессы, основываясь на порядке, в котором работал с процессами пользователь, т. е. те Background-процессы, с которыми позже всего работал пользователь, будут прибиты в последнюю очередь. Если в активности корректно реализованы методы жизненного цикла (lifecycle methods, подробнее см. [2]), и активность сохраняет свое текущее состояние, то завершение её процесса пользователь не заметит, потому что когда пользователь вернется обратно к этой активности, активность восстановит все свое видимое состояние. См. документ Activities, раздел посвященный сохранению и восстановлению состояния.

5. Empty process (пустой процесс)

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

Android оценивает важность процесса на базе самом важном из компонентов, которые в настоящее время активны в процессе. Например, если в процессе размещена служба и видимая активность, то важность процесса будет visible, а не service.

Кроме того, важность процесса может быть увеличена в зависимости от того, какие процессы зависят от него — процесс, который обслуживает другой процесс, не может быть понижен в ранге ниже, чем процесс, который он обслуживает. Например, если провайдер контента (content provider) в процессе A обслуживает клиента в процессе B, или если служба (service) в процессе A привязана к компоненту в процессе B, то процесс A всегда рассматривается как минимум важным на столько же, как и процесс B.

Поскольку процесс со службой (service) оценивается важнее, чем процесс с background активностями, то активность, которая инициирует длительную операцию, могла бы для улучшения обслуживания пользователя запустить службу (service) для этой длительной операции, вместо того, чтобы просто создать отдельный рабочий поток — особенно если эта операция должна пережить активность. К примеру, если активность выгружает картинку на сайт, то она должна запустить службу для этого действия, чтобы выгрузка могла продолжаться и тогда, когда активность ушла в background (т. е. даже если пользователь оставил эту активность и перешел к другим действиям). Использование службы гарантирует, что операция получит приоритет как минимум процесса службы "service process", независимо от того, что случится с самой активностью. По той же самой причине приемники широковещательных сообщений (broadcast receiver) должны использовать службы, а не просто помещать в поток потребляющие время процессора действия.

[Потоки (Thread)]

Когда запускается приложение, система Android создает для него поток, называемый "main". Этот поток очень важен, потому что он отвечает за обработку событий (dispatching events) и направление их к виджетам интерфейса пользователя, включая события отрисовки. Это также поток, в котором Ваше приложение взаимодействует с компонентами тулкита интерфейса пользователя (Android UI toolkit, компоненты из пакетов android.widget и android.view). Также main thread иногда называют потоком интерфейса пользователя (UI thread).

Система не создает отдельный поток для каждого экземпляра компонента. Таким образом, все компоненты, запущенные в главном потоке UI, работают в том же самом потоке, и системные вызовы каждого компонента диспетчеризированы из этого потока. Следовательно методы, которые отвечают за системные обратные вызовы (system callbacks, такие как onKeyDown() для сообщения о действиях пользователя, или метод жизненного цикла), всегда работают в потоке UI процесса.

Например, когда пользователь касается кнопки на экране, UI thread приложения перенаправляет событие касания виджету, который в свою очередь устанавливает свое нажатое состояние (pressed state), и помещает запрос в очередь событий. Затем UI thread обрабатывает запрос и оповещает виджет от том, что он должен перерисовать сам себя.

Когда Ваше приложение выполняет интенсивную работу в ответ на взаимодействие с пользователем, эта однопоточная модель может привести к низкой производительности, если Вы не построите свое приложение должным образом. В частности, если все происходит в потоке интерфейса (UI thread), то долгие операции наподобие доступа к сети или запросы к базе данных будут блокировать весь пользовательский интерфейс. Когда поток UI заблокирован (например на циклах или ожидании), то события интерфейса не могут быть обработаны, включая события перерисовки. С точки зрения пользователя приложение зависнет. Еще хуже, если UI будет заблокирован больше, чем несколько секунд (в настоящее время этот таймаут составляет около 5 секунд), тогда пользователю будет представлено позорное окно диалога ANR (application not responding, приложение не отвечает). Пользователь может решить выйти из программы, и даже деинсталлировать её, потому что программа работает странно и его это не устраивает [3].

Кроме того, Android UI toolkit не является потокобезопасным (not thread-safe). Так что Вы не должны манипулировать интерфейсом пользователя из дополнительного рабочего потока (worker thread) — все манипуляции с графикой интерфейса должны происходить только из UI thread. Таким образом, отсюда вытекают 2 правила для однопоточной модели приложения Android:

1. Нельзя блокировать выполнение потока обработки интерфейса пользователя (UI thread).
2. Нельзя обращаться к Android UI toolkit из других потоков. Можно работать с Android UI toolkit только из кода UI thread.

[Дополнительные рабочие потоки (Worker thread)]

По причине применения вышеописанной однопоточной модели, для скорости отклика UI приложения жизненно важно, чтобы Вы не блокировали выполнение UI thread. Если Вам нужно выполнить операции, которые не завершатся немедленно, то нужно их перенести в отдельные потоки (так называемые "background", фоновые потоки, или "worker", рабочие потоки).

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

public void onClick(View v)
{
    new Thread(new Runnable()
    {
        public void run()
        {
            Bitmap b = loadImageFromNetwork("http://example.com/image.png");
            mImageView.setImageBitmap(b);
        }
    }).start();
}

На первый взгляд все будет работать нормально, потому что создается новый поток для обработки сетевой операции. Однако здесь нарушается второе правило однопоточной модели обработки интерфейса пользователя: нельзя обращаться к Android UI toolkit вне кода UI thread — в этом примере ImageView модифицируется из worker thread, вместо того, чтобы это действие выполнилось в UI thread. В результате получим неопределенное, или неожиданное поведение, которое впоследствии будет трудно выявить и исправить.

Чтобы исправить эту проблему, Android предоставляет несколько способов получить доступ к UI thread из других потоков. Вот список этих методов, которые могут помочь:

Activity.runOnUiThread(Runnable)
View.post(Runnable)
View.postDelayed(Runnable, long)

Например, Вы можете исправить вышеприведенный код путем использования метода View.post(Runnable):

public void onClick(View v)
{
    new Thread(new Runnable()
    {
        public void run()
        {
            final Bitmap bitmap = loadImageFromNetwork("http://example.com/image.png");
            mImageView.post(new Runnable()
            {
                public void run()
                {
                    mImageView.setImageBitmap(bitmap);
                }
            });
        }
    }).start();
}

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

Однако сложность операций возрастает, и такой код может стать сложным и трудным для дальнейшей поддержки. Чтобы обработать более сложные взаимодействия с worker thread, Вы могли бы рассмотреть использовать Handler в Вашем worker thread для обработки сообщений, поступающих из UI thread. Возможно лучшее решение тем не менее состоит в том, чтобы расширить (extend) класс AsyncTask [5], который упрощает выполнение задач worker thread, требующих взаимодействия с UI.

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

Класс AsyncTask позволяет Вам выполнить асинхронно некую работу в интерфейсе пользователя. Он выполняет блокирующие операции в worker thread, и затем публикует результаты для UI thread, при этом от Вас не требуется самостоятельно обрабатывать потоки и/или обработчики.

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

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

public void onClick(View v)
{
    new DownloadImageTask().execute("http://example.com/image.png");
}
 
private class DownloadImageTask extends AsyncTask< String, Void, Bitmap >
{
    /** Система вызовет это для выполнения работы в worker thread, 
      * и передает параметры, полученные AsyncTask.execute() */
protected Bitmap doInBackground(String... urls) { return loadImageFromNetwork(urls[0]); }   /** Система вызовет это для выполнения работы в UI thread,
      * и передает результат из doInBackground() */
protected void onPostExecute(Bitmap result) { mImageView.setImageBitmap(result); } }

Теперь UI защищен, и код упростился, потому что здесь работа разделена на две части, одна из которых выполняется в worker thread, а другая должна быть выполнена в UI thread.

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

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

Предупреждение: Вы можете столкнуться с другой проблемой, когда используете worker thread, в том случае, когда произойдет неожиданный перезапуск Вашей activity из-за изменения конфигурации во времени выполнения. Такое может произойти, к примеру, если пользователь изменит ориентацию экрана, и это может уничтожить Ваш worker thread. Чтобы посмотреть, как Вы могли бы сохранить свою задачу во время одного из таких перезапусков, и как правильно отменить задачу, когда активность разрушается, см. исходный код примера приложения Shelves.

[Потокобезопасные (Thread-safe) методы]

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

Это прежде всего верно для методов, которые могут быть вызваны удаленно — такие как методы, привязанные к службе. Когда вызов метода, реализованного в IBinder, происходит в том же самом процессе, в котором работает IBinder, метод выполняется в потоке вызывающей стороны. Однако, когда вызов происходит из другого процесса, метод выполняется в потоке, выбранном из пула потоков, которые система поддерживает в том же процессе, что и IBinder (он не выполняется в UI thread процесса). Например, в то время как метод onBind() службы был бы вызван из UI thread процесса сервиса, методы, реализованные в объекте, который возвращает onBind() (для примера, подкласс, который реализует методы RPC) были бы вызваны из потоков в пуле. Поскольку у службы может быть больше одного клиента, то один и тот же метод IBinder в одно и то же время может задействовать более чем один поток пула. Таким образом, методы IBinder должны быть реализованы потокобезопасно (thread-safe).

Точно так же провайдер контента (content provider) может принять запросы данных, поступающие из других процессов. Поскольку классы ContentResolver и ContentProvider скрывают детали о том, как обрабатываются коммуникации между процессами, то методы ContentProvider, отвечающие на эти запросы — query(), insert(), delete(), update() и getType() — вызываются из пула потоков в процессе провайдера контента, не в UI thread для процесса. Поскольку эти методы могут быть вызваны из любого другого количества потоков в одно и то же время, они также должны быть реализованы thread-safe.

[Взаимодействие между процессами (Interprocess Communication)]

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

Чтобы выполнить IPC, Ваше приложение должно сделать привязку к службе с использованием bindService(). Для дополнительной информации см. документацию разработчика служб [4].

[Ссылки]

1. Processes and Threads site:developer.android.com.
2. Класс Activity.
3. Как сделать Android-приложение быстрым и отзывчивым.
4. Службы Android.
5. Работа с потоками через AsyncTask.
6. Словарик Android.

 

Комментарии  

 
-2 #1 Георгий Чеботарев 13.04.2023 13:52
Интересно, насколько рационально выносить тяжелые операции в другие процессы? Скажем, например, сервис-синхронизации или broadcast-reveiver-ы? Выделяться ли дополнительные ресурсы под такой процесс?
Цитировать
 

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


Защитный код
Обновить

Top of Page