Статьи

Параллелизм на Android с сервисом

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

Чтобы следовать этому руководству, вам понадобится некоторое понимание параллелизма на Android. Если вы не знаете об этом много, вы можете сначала прочитать некоторые другие наши статьи на эту тему.

  • Android SDK
    Android с нуля: фоновые операции
    Пол Требилкокс-Руис
  • Android
    Понимание значений AsyncTask за 60 секунд
    Пол Требилкокс-Руис
  • Android SDK
    Понимание параллелизма на Android с помощью HaMeR
    Жестяная мегали
  • Android SDK
    Практический параллелизм на Android с HaMeR
    Жестяная мегали

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

Service — это простой компонент, который создается системой для выполнения длительной работы, которая не обязательно зависит от взаимодействия с пользователем. Он может быть независимым от жизненного цикла действия, а также может выполняться в совершенно ином процессе.

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

Service имеет две основные функции:

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

Существует много путаницы по поводу сервисов и потоков. Когда Service объявлена, она не содержит Thread . Фактически, по умолчанию он запускается непосредственно в главном потоке, и любая работа, выполняемая над ним, может потенциально заморозить приложение. (Если это не IntentService , подкласс Service который уже поставляется с настроенным рабочим потоком.)

Итак, как сервисы предлагают решение для параллелизма? Ну, Service по умолчанию не содержит потока, но его можно легко настроить для работы с собственным потоком или с пулом потоков. Мы увидим больше об этом ниже.

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

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

Существует два типа Service : запущенный и связанный.

Запущенный сервис запускается через Context.startService() . Обычно он выполняет только одну операцию и будет работать бесконечно, пока операция не завершится, а затем отключится. Как правило, он не возвращает никакого результата в пользовательский интерфейс.

Связанный сервис запускается через Context.bindService() , и он обеспечивает двустороннюю связь между клиентом и Service . Он также может подключаться к нескольким клиентам. Он разрушается, когда к нему не подключен ни один клиент.

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

Чтобы использовать службу, расширьте класс Service и переопределите его методы обратного вызова в соответствии с типом Service . Как упоминалось ранее, для запущенных служб должен быть реализован метод onStartCommand() а для связанных служб — метод onBind() . На самом деле, onBind() Метод должен быть объявлен для любого типа сервиса, но он может возвращать ноль для запущенных сервисов.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
public class CustomService extends Service {
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        // Execute your operations
        // Service wont be terminated automatically
        return Service.START_NOT_STICKY;
    }
 
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        // Creates a connection with a client
        // using a interface implemented on IBinder
        return null;
    }
}
  • onStartCommand() : запускается Context.startService() . Это обычно вызывается из деятельности. После вызова служба может работать бесконечно, и вы можете остановить ее, вызвав stopSelf() или stopService() .
  • onBind() : onBind() когда компонент хочет подключиться к сервису. Вызывается в системе с помощью Context.bindService() . Он возвращает IBinder который предоставляет интерфейс для связи с клиентом.

Жизненный цикл службы также важно учитывать. onCreate() и onDestroy() должны быть реализованы для инициализации и завершения любых ресурсов или операций службы.

Компонент Service должен быть объявлен в манифесте с элементом <service> . В этом объявлении также возможно, но не обязательно, установить другой процесс для запуска Service .

1
2
3
4
5
6
7
8
9
<manifest … >
  …
  <application … >
      <service
        android:name=».ExampleService»
        android:process=»:my_process»/>
      …
  </application>
</manifest>

Чтобы инициировать запущенный сервис, вы должны вызвать Context.startService() . Intent должно быть создано с Context и Context Service . Любая соответствующая информация или данные также должны быть переданы в этом Intent .

1
2
3
4
5
6
7
8
Intent serviceIntent = new Intent(this, CustomService.class);
// Pass data to be processed on the Service
Bundle data = new Bundle();
data.putInt(«OperationType», 99);
data.putString(«DownloadURL», «http://mydownloadurl.com»);
serviceIntent.putExtras(data);
// Starting the Service
startService(serviceIntent);

В вашем классе Service метод, который вас должен беспокоить, это onStartCommand() . Именно по этому методу вы должны вызывать любую операцию, которую хотите выполнить в запущенной службе. Вы обработаете Intent для сбора информации, отправленной клиентом. startId представляет собой уникальный идентификатор, автоматически создаваемый для этого конкретного запроса, а flags также могут содержать дополнительную информацию о нем.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
@Override
   public int onStartCommand(Intent intent, int flags, int startId) {
 
       Bundle data = intent.getExtras();
       if (data != null) {
           int operation = data.getInt(KEY_OPERATION);
           // Check what operation to perform and send a msg
           if ( operation == OP_DOWNLOAD){
               // make a download
           }
       }
 
       return START_STICKY;
   }

onStartCommand() возвращает константу int которая управляет поведением:

  • Service.START_STICKY : служба перезапускается, если она прекращается.
  • Service.START_NOT_STICKY : Сервис не перезапущен.
  • Service.START_REDELIVER_INTENT : Служба перезапускается после сбоя, и намерения и обработка будут доставлены.

Как упоминалось ранее, запущенная служба должна быть остановлена, в противном случае она будет работать бесконечно. Это может быть сделано либо Service вызывающей stopSelf() для себя, либо клиентом, вызывающим stopService() для нее.

1
2
3
4
5
void someOperation() {
       // do some long-running operation
       // and stop the service when it is done
       stopSelf();
   }

Компоненты могут создавать соединения со службами, устанавливая двустороннюю связь с ними. Клиент должен вызвать Context.bindService() , передав в качестве параметров Intent , интерфейс ServiceConnection и flag . Service может быть привязан к нескольким клиентам, и он будет уничтожен, если к нему не подключены клиенты.

1
2
3
4
5
void bindWithService() {
       Intent intent = new Intent(this, PlayerService.class);
       // bind with Service
       bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
   }

Возможна отправка объектов Message в сервисы. Для этого вам необходимо создать Messenger на стороне клиента в реализации интерфейса ServiceConnection.onServiceConnected и использовать его для отправки объектов Message в Service .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
private ServiceConnection mConnection = new ServiceConnection() {
       @Override
       public void onServiceConnected(ComponentName className,
                                      IBinder service) {
           // use the IBinder received to create a Messenger
           mServiceMessenger = new Messenger(service);
           mBound = true;
       }
 
       @Override
       public void onServiceDisconnected(ComponentName arg0) {
           mBound = false;
           mServiceMessenger = null;
       }
   };

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

01
02
03
04
05
06
07
08
09
10
11
12
13
private Handler mResponseHandler = new Handler() {
       @Override
       public void handleMessage(Message msg) {
           // handle response from Service
       }
   };
   Message msgReply = Message.obtain();
   msgReply.replyTo = new Messenger(mResponseHandler);
   try {
       mServiceMessenger.send(msgReply);
   } catch (RemoteException e) {
       e.printStackTrace();
   }

Важно отсоединиться от Сервиса, когда клиент уничтожается.

1
2
3
4
5
6
7
8
9
@Override
   protected void onDestroy() {
       super.onDestroy();
       // disconnect from service
       if (mBound) {
           unbindService(mConnection);
           mBound = false;
       }
   }

На стороне Service вы должны реализовать метод Service.onBind() , предоставляющий IBinder предоставленный из Messenger . Это будет ретранслировать Handler ответа для обработки объектов Message полученных от клиента.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
IncomingHandler(PlayerService playerService) {
           mPlayerService = new WeakReference<>(playerService);
       }
 
       @Override
       public void handleMessage(Message msg) {
           // handle messages
       }
   }
    
   public IBinder onBind(Intent intent) {
       // pass a Binder using the Messenger created
       return mMessenger.getBinder();
   }
    
   final Messenger mMessenger = new Messenger(new IncomingHandler(this));

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

Чтобы Service выполнялась в фоновом Thread вы можете просто создать дополнительный Thread и запустить его там. Однако Android предлагает нам лучшее решение. Один из способов извлечь максимальную выгоду из системы — внедрить инфраструктуру HaMeR внутри Service , например, зациклив поток с очередью сообщений, которая может обрабатывать сообщения бесконечно.

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

Для использования HaMeR вы должны предоставить Сервису Looper , Handler и HandlerThread .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private Looper mServiceLooper;
   private ServiceHandler mServiceHandler;
   // Handler to receive messages from client
   private final class ServiceHandler extends Handler {
       ServiceHandler(Looper looper) {
           super(looper);
       }
 
       @Override
       public void handleMessage(Message msg) {
           super.handleMessage(msg);
           // handle messages
 
           // stopping Service using startId
           stopSelf( msg.arg1 );
       }
   }
    
   @Override
   public void onCreate() {
       HandlerThread thread = new HandlerThread(«ServiceThread»,
               Process.THREAD_PRIORITY_BACKGROUND);
       thread.start();
 
       mServiceLooper = thread.getLooper();
       mServiceHandler = new ServiceHandler(mServiceLooper);
 
   }

Если среда HaMeR вам незнакома, прочитайте наши учебники по параллельности HaMer для Android.

  • Android SDK
    Понимание параллелизма на Android с помощью HaMeR
    Жестяная мегали
  • Android SDK
    Практический параллелизм на Android с HaMeR
    Жестяная мегали

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

Чтобы использовать этот класс, все, что вам нужно сделать, это расширить его и реализовать onHandleIntent() , метод ловушки, который будет вызываться каждый раз, когда клиент вызывает startService() в этой Service . Важно помнить, что IntentService остановится, как только его работа будет завершена.

01
02
03
04
05
06
07
08
09
10
11
public class MyIntentService extends IntentService {
     
    public MyIntentService() {
        super(«MyIntentService»);
    }
 
    @Override
    protected void onHandleIntent(Intent intent) {
        // handle Intents send by startService
    }
}

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

Связь между различными процессами называется IPC (межпроцессное взаимодействие) . В Service есть два основных способа сделать IPC: использование Messenger или реализация интерфейса AIDL .

Мы узнали, как отправлять и получать сообщения между службами. Все, что вам нужно сделать, — это создать Messenger с IBinder экземпляра IBinder полученного в процессе подключения, и использовать его для отправки ответного Messenger обратно в Service .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
private Handler mResponseHandler = new Handler() {
       @Override
       public void handleMessage(Message msg) {
           // handle response from Service
       }
   };
 
   private ServiceConnection mConnection = new ServiceConnection() {
       @Override
       public void onServiceConnected(ComponentName className,
                                      IBinder service) {
           // use the IBinder received to create a Messenger
           mServiceMessenger = new Messenger(service);
 
           Message msgReply = Message.obtain();
           msgReply.replyTo = new Messenger(mResponseHandler);
           try {
               mServiceMessenger.send(msgReply);
           } catch (RemoteException e) {
               e.printStackTrace();
           }
       }

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

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

До скорого!