Współbieżność w Androidzie w ramach usługi
Polish (Polski) translation by Mateusz Kurlit (you can also view the original English article)
W tym poradniku omówimy komponent Service
i jego superklasę IntentService
. Nauczymy się kiedy i jak korzystać z tego komponentu w celu tworzenia rozwiązań współbieżności dla długotrwałych operacji w tle. Rzucimy również okiem na IPC (Inter Process Communication), aby dowiedzieć się jak komunikować się z usługami działającymi w różnych procesach.
Aby wykorzystać w pełni ten poradnik, powinieneś posiadać podstawową wiedzę o współbieżności w Androidzie. Jeśli nie wiesz za wiele na ten temat, zachęcam najpierw do przeczytania naszych artykułów.
- Android SDKAndroid od podstaw: operacje w tlePaul Trebilcox-Ruiz
- AndroidOmówienie wartości AsyncTask w 60 sekundPaul Trebilcox-Ruiz
- Android SDKOmówienie współbieżności w Androidzie w ramach HaMeRTin Megali
- Android SDKPraktyczna współbieżność w Androidzie w ramach HaMeRTin Megali
1. Komponent usługi
Komponent Service
jest bardzo ważną częścią struktury współbieżności w Androidzie. Realizuje konieczność przeprowadzania długotrwałych operacji w aplikacji lub wyposaża w pewną funkcjonalność inne aplikacje. W tym poradniku, skoncentrujemy się wyłącznie na uruchamianiu długotrwałych zadań w komponencie Service
i omówimy jak wykorzystać je w udoskonalaniu współbieżności.
Co to jest usługa?
Service
to prosty komponent uruchamiany przez system w celu przeprowadzania długotrwałych operacji, które niekoniecznie zależą od samego użytkownika. Może być niezależny od cyklu życia aktywności oraz działać w całkowicie innym procesie.
Zanim przejdziemy do dyskusji o tym, co reprezentuje Service
, warto podkreślić, że usługi są powszechnie wykorzystywane w długotrwałych operacjach tła i przeprowadzaniu zadań na różnych procesach, jednak sam Service
nie reprezentuje wątku ani procesu. Będzie tylko działać w wątku tła lub w innym procesie, jeśli wyraźnie zostanie to polecone.
Service
posiada dwie główne funkcje:
- Instrument dla aplikacji, który przekazuje systemowi informację, że chce coś robić w tle.
- Instrument dla aplikacji, który przedstawia pewną funkcjonalność innym aplikacjom.
Usługi i wątki
Istnieje wiele nieporozumień związanych z usługami i wątkami. Po zadeklarowaniu Service
, nie zawiera on Thread
. W gruncie rzeczy, domyślnie działa bezpośrednio w głównym wątku i dowolna operacja wykonywana w tym wątku potencjalnie może zawiesić aplikację. (Chyba że użyjemy podklasy IntentService
, która zawiera skonfigurowany wątek roboczy.)
Jak więc usługi oferują rozwiązanie współbieżności? Domyślnie Service
nie zawiera wątku, ale może być z łatwością skonfigurowany do pracy z własnym lub pulą wątków. Za chwilę dowiemy się więcej na ten temat.
Pomijając brak wbudowanego wątku, Service
jest doskonałym rozwiązaniem problemów ze współbieżnością oraz pewnych sytuacji. Oto kilka argumentów przemawiających za wyborem Service
w porównaniu do innych rozwiązań współbieżności, takich jak AsyncTask
lub struktura HaMeR:
-
Service
może być niezależny od cyklów życia aktywności. -
Service
jest odpowiedni do przeprowadzania długotrwałych operacji. - Usługi nie zależą od działań użytkownika.
- Podczas działania w różnych procesach, Android może starać się zachować usługi w pamięci, nawet gdy brakuje zasobów w systemie.
-
Service
może być uruchomiony ponownie w celu wznowienia swojej pracy.
Typy usług
Istnieją dwa typy Service
, uruchomiony i związany.
Context.startService()
rozpoczyna pierwszy typ. Generalnie przeprowadza tylko jedną operację, działa do jej zakończenia, następnie sam się wyłącza. Zwykle nie zwraca żadnych wyników interfejsowi użytkownika.
Context.bindService()
rozpoczyna drugi typ i umożliwia dwukierunkową komunikację między klientem i Service
. Potrafi również łączyć się z wieloma klientami. Wyłącza się, jeśli żaden klient nie jest do niego podłączony.
Aby móc wybierać między tymi dwoma typami, Service
musi zaimplementowane wywołania zwrotne: onStartCommand()
, aby działać jako uruchomiona usługa oraz onBind()
jako związana. Service może podjąć decyzję o wdrożeniu jednego typu, ale potrafi również bezproblemowo zaimplementować oba.
2. Wdrożenie usługi
Aby użyć usługi, rozszerz klasę Service
i nadpisz jej metody wywołania zwrotnego według typu Service
. Jak wspomniano wcześniej, należy zaimplementować metodę onStartCommand()
dla uruchomionej usługi oraz onBind()
dla związanej. Tak naprawdę, metoda onBind()
musi być zadeklarowana dla obu typów, ale może zwracać wartość zerową uruchomionym usługom.
1 |
public class CustomService extends Service { |
2 |
@Override
|
3 |
public int onStartCommand(Intent intent, int flags, int startId) { |
4 |
// Execute your operations
|
5 |
// Service wont be terminated automatically
|
6 |
return Service.START_NOT_STICKY; |
7 |
}
|
8 |
|
9 |
@Nullable
|
10 |
@Override
|
11 |
public IBinder onBind(Intent intent) { |
12 |
// Creates a connection with a client
|
13 |
// using a interface implemented on IBinder
|
14 |
return null; |
15 |
}
|
16 |
}
|
-
onStartCommand()
: rozpoczęta przezContext.startService()
. Zazwyczaj wywoływana z aktywności. Po wywołaniu, usługa może działać w nieskończoność, da się ją zatrzymać wywołującstopSelf()
lubstopService()
. -
onBind()
pojawia się, kiedy komponent chce połączyć się z usługą. Wywoływana w systemie przezContext.bindService()
. ZwracaIBinder
, który wyświetla interfejs komunikujący się z klientem.
Warto również wziąć pod uwagę cykl życia usługi. Metody onCreate()
i onDestroy()
powinny być zaimplementowane w celu inicjowania i wyłączania zasobów lub operacji w ramach usługi.
Deklarowanie usługi w manifeście
Komponent Service
musi być zadeklarowany w manifeście z elementem <service>
. W deklaracji można, ale nie trzeba ustawiać innego procesu dla uruchamiania Service
.
1 |
<manifest ... > |
2 |
... |
3 |
<application ... > |
4 |
<service
|
5 |
android:name=".ExampleService" |
6 |
android:process=":my_process"/> |
7 |
... |
8 |
</application>
|
9 |
</manifest>
|
2.2. Praca z uruchomionymi usługami
Aby zainicjować uruchomioną usługę, musisz wywołać metodę Context.startService()
. Intent
należy utworzyć razem z klasami Context
i Service
. Również wszystkie istotne informacje lub dane powinny zostać przekazane do Intent
.
1 |
Intent serviceIntent = new Intent(this, CustomService.class); |
2 |
// Pass data to be processed on the Service
|
3 |
Bundle data = new Bundle(); |
4 |
data.putInt("OperationType", 99); |
5 |
data.putString("DownloadURL", "https://mydownloadurl.com"); |
6 |
serviceIntent.putExtras(data); |
7 |
// Starting the Service
|
8 |
startService(serviceIntent); |
W klasie Service
, powinniśmy zająć się metodą onStartCommand()
. Należy w niej wywołać konkretną operację, którą chcesz wykonać za pomocą uruchomionej usługi. W ten sposób, przetworzysz Intent
, aby odebrać informacje wysłane przez klienta. startId
reprezentuje unikalne ID tworzone automatycznie dla tego żądania, a flags
może zawierać dodatkowe informacje.
1 |
@Override
|
2 |
public int onStartCommand(Intent intent, int flags, int startId) { |
3 |
|
4 |
Bundle data = intent.getExtras(); |
5 |
if (data != null) { |
6 |
int operation = data.getInt(KEY_OPERATION); |
7 |
// Check what operation to perform and send a msg
|
8 |
if ( operation == OP_DOWNLOAD){ |
9 |
// make a download
|
10 |
}
|
11 |
}
|
12 |
|
13 |
return START_STICKY; |
14 |
}
|
onStartCommand()
zwraca stałą int
, która kontroluje zachowanie.
-
Service.START_STICKY
: usługa jest uruchamiana ponownie, jeśli zostanie zamknięta.
-
Service.START_NOT_STICKY
: usługa nie jest uruchamiana ponownie.
-
Service.START_REDELIVER_INTENT
: usługa jest uruchamiana ponownie po awarii, a przetwarzane intencje zostają ponownie dostarczone.
Jak wspomniano wcześniej, uruchomioną usługę należy zatrzymać, inaczej będzie działać w nieskończoność. Wystarczy, że w Service
wywoła się stopSelf()
lub w kliencie stopService()
.
1 |
void someOperation() { |
2 |
// do some long-running operation
|
3 |
// and stop the service when it is done
|
4 |
stopSelf(); |
5 |
}
|
Łączenie z usługami
Komponenty mogą tworzyć połączenia z usługami, ustanawiając z nimi dwukierunkową komunikację. Klient musi wywołać Context.bindService()
, przekazując do Intent
, interfejs ServiceConnection
i flags
jako patrametry. Service
może być związany z wieloma klientami i zostanie wyłączony, gdy żaden z nich nie będzie do niego podłączony.
1 |
void bindWithService() { |
2 |
Intent intent = new Intent(this, PlayerService.class); |
3 |
// bind with Service
|
4 |
bindService(intent, mConnection, Context.BIND_AUTO_CREATE); |
5 |
}
|
Istnieje możliwość przesyłania obiektów Message
do usług. Wystarczy utworzyć Messenger
po stronie klienta w implementacji interfejsu ServiceConnection.onServiceConnected
i użyć go do przesyłania obiektów Message
do Service
.
1 |
private ServiceConnection mConnection = new ServiceConnection() { |
2 |
@Override
|
3 |
public void onServiceConnected(ComponentName className, |
4 |
IBinder service) { |
5 |
// use the IBinder received to create a Messenger
|
6 |
mServiceMessenger = new Messenger(service); |
7 |
mBound = true; |
8 |
}
|
9 |
|
10 |
@Override
|
11 |
public void onServiceDisconnected(ComponentName arg0) { |
12 |
mBound = false; |
13 |
mServiceMessenger = null; |
14 |
}
|
15 |
};
|
Można również przekazać odpowiedź Messenger
do Service
, aby klient otrzymywał wiadomości. Ale uważaj, klient może być niedostępny, aby otrzymywać wiadomości usługi. Możesz również użyć BroadcastReceiver
lub dowolnego rozwiązania dotyczącego powiadomień.
1 |
private Handler mResponseHandler = new Handler() { |
2 |
@Override
|
3 |
public void handleMessage(Message msg) { |
4 |
// handle response from Service
|
5 |
}
|
6 |
};
|
7 |
Message msgReply = Message.obtain(); |
8 |
msgReply.replyTo = new Messenger(mResponseHandler); |
9 |
try { |
10 |
mServiceMessenger.send(msgReply); |
11 |
} catch (RemoteException e) { |
12 |
e.printStackTrace(); |
13 |
}
|
Ważne, aby rozwiązać połączenie z usługą po wyłączeniu klienta.
1 |
@Override
|
2 |
protected void onDestroy() { |
3 |
super.onDestroy(); |
4 |
// disconnect from service
|
5 |
if (mBound) { |
6 |
unbindService(mConnection); |
7 |
mBound = false; |
8 |
}
|
9 |
}
|
Po stronie Service
, musisz zaimplementować metodę Service.onBind()
, dostarczając IBinder
za pośrednictwem Messenger
. W ten sposób przekażesz odpowiedź Handler
do obsługi obiektów Message
otrzymanych od klienta.
1 |
IncomingHandler(PlayerService playerService) { |
2 |
mPlayerService = new WeakReference<>(playerService); |
3 |
}
|
4 |
|
5 |
@Override
|
6 |
public void handleMessage(Message msg) { |
7 |
// handle messages
|
8 |
}
|
9 |
}
|
10 |
|
11 |
public IBinder onBind(Intent intent) { |
12 |
// pass a Binder using the Messenger created
|
13 |
return mMessenger.getBinder(); |
14 |
}
|
15 |
|
16 |
final Messenger mMessenger = new Messenger(new IncomingHandler(this)); |
3. Współbieżność w ramach usług
Nadszedł czas, aby omówić rozwiązywanie problemów ze współbieżnością w ramach usług. Jak wspomniano wcześniej, standardowy Service
nie zawiera żadnych dodatkowych wątków i domyślnie działa w głównym Thread
. Aby pozbyć się tego problemu, musisz dodać roboczy Thread
, pulę wątków lub wykonać Service
w innym procesie. Możesz również skorzystać z podklasy o nazwie IntentService
, która już zawiera Thread
.
Uruchamianie usług w wątku roboczym
Aby wykonać Service
w Thread
tła, wystarczy stworzyć dodatkowy Thread
i uruchomić tam zadanie. Jednakże, Android oferuje nam lepsze rozwiązanie. Jednym z najlepszych sposobów na wykorzystanie systemu jest zaimplementowanie struktury HaMeR wewnątrz Service
, na przykład zapętlając Thread
za pomocą kolejki komunikatów, która może przetwarzać wiadomości w nieskończoność.
Warto zrozumieć, że ta implementacja przetworzy zadania sekwencyjnie. Jeśli chcesz jednocześnie otrzymywać i przetwarzać wiele zadań, powinieneś użyć puli wątków. Kwestia wykorzystania puli wątków nie wchodzi w zakres tego poradnika.
Aby użyć HaMeR musisz dostarczyć Service
z Looper
, Handler
i HandlerThread
.
1 |
private Looper mServiceLooper; |
2 |
private ServiceHandler mServiceHandler; |
3 |
// Handler to receive messages from client
|
4 |
private final class ServiceHandler extends Handler { |
5 |
ServiceHandler(Looper looper) { |
6 |
super(looper); |
7 |
}
|
8 |
|
9 |
@Override
|
10 |
public void handleMessage(Message msg) { |
11 |
super.handleMessage(msg); |
12 |
// handle messages
|
13 |
|
14 |
// stopping Service using startId
|
15 |
stopSelf( msg.arg1 ); |
16 |
}
|
17 |
}
|
18 |
|
19 |
@Override
|
20 |
public void onCreate() { |
21 |
HandlerThread thread = new HandlerThread("ServiceThread", |
22 |
Process.THREAD_PRIORITY_BACKGROUND); |
23 |
thread.start(); |
24 |
|
25 |
mServiceLooper = thread.getLooper(); |
26 |
mServiceHandler = new ServiceHandler(mServiceLooper); |
27 |
|
28 |
} |
Jeśli nie znasz struktury HaMeR, przeczytaj nasze poradniki o HaMeR dla współbieżności w Androidzie.
- Android SDKOmówienie współbieżności na Androidzie w ramach HaMeRTin Megali
- Android SDKPraktyczna współbieżność na Androidzie w ramach HaMeRTin Megali
IntentService
Jeśli nie ma potrzeby, aby Service
działał przez dłuższy czas, możesz użyć podklasy IntentService
, która potrafi uruchamiać zadania wewnątrz wątków tła. Tak naprawdę, IntentService
to Service
z implementacją bardzo podobną do zaproponowanej powyżej.
Aby użyć tej klasy, wystarczy ją rozszerzyć i zaimplementować metodę onHandleIntent()
, która będzie wywoływana za każdym razem, gdy klient wezwie startService()
w Service
. Należy zwrócić uwagę, że IntentService
zostanie zatrzymana zaraz po zakończeniu zadania.
1 |
public class MyIntentService extends IntentService { |
2 |
|
3 |
public MyIntentService() { |
4 |
super("MyIntentService"); |
5 |
}
|
6 |
|
7 |
@Override
|
8 |
protected void onHandleIntent(Intent intent) { |
9 |
// handle Intents send by startService
|
10 |
}
|
11 |
}
|
IPC (Inter Process Communication)
Service
może działać w całkowicie innym Process
, niezależnie od wszystkich zadań znajdujących się w głównym wątku. Proces posiada własny przydział pamięci, grupę wątków oraz priorytety przetwarzania. Takie podejście może być bardzo przydatne, gdy trzeba działać niezależnie od głównego procesu.
Komunikacja między różnymi procesami nosi nazwę IPC (Inter Process Communication). W Service
istnieją dwa sposoby na wykonanie IPC: wykorzystując Messenger
lub implementując interfejs AIDL
.
Nauczyliśmy się jak wysyłać i odbierać wiadomości między usługami. Wszystko co należy zrobić to stworzyć Messenger
za pomocą instancji IBinder
otrzymaną podczas połączenia i wykorzystać go do przesłania odpowiedzi obiektowi Messenger
z powrotem do Service
.
1 |
private Handler mResponseHandler = new Handler() { |
2 |
@Override
|
3 |
public void handleMessage(Message msg) { |
4 |
// handle response from Service
|
5 |
}
|
6 |
};
|
7 |
|
8 |
private ServiceConnection mConnection = new ServiceConnection() { |
9 |
@Override
|
10 |
public void onServiceConnected(ComponentName className, |
11 |
IBinder service) { |
12 |
// use the IBinder received to create a Messenger
|
13 |
mServiceMessenger = new Messenger(service); |
14 |
|
15 |
Message msgReply = Message.obtain(); |
16 |
msgReply.replyTo = new Messenger(mResponseHandler); |
17 |
try { |
18 |
mServiceMessenger.send(msgReply); |
19 |
} catch (RemoteException e) { |
20 |
e.printStackTrace(); |
21 |
}
|
22 |
}
|
Interfejs AIDL
jest bardzo rozbudowany oraz umożliwiający bezpośrednie wywoływanie metod Service
działających w oddzielnych procesach i warto skorzystać z niego, gdy mamy do czynienia ze złożonym Service
. AIDL
jest bardzo trudny w implementacji oraz rzadko wykorzystywany, dlatego nie zostanie omówiony w tym poradniku.
4. Podsumowanie
Usługi mogą być proste lub złożone. Wszystko zależy od wymagań danej aplikacji. W tym poradniku, starałem się wyjaśnić jak najwięcej kwestii, jednakże skupiłem się tylko na wykorzystaniu usług w celach współbieżności, ale istnieje więcej możliwości dla tego komponentu. Jeśli chcesz poszerzyć wiedzę na ten temat, przejrzyj dokumentację i poradniki Androida.
Do zobaczenia wkrótce!