Zaawansowane metody komunikacji między serwisem a aktywnością w Androidzie część 2

W pierwszej części przedstawiłem podstawowe metody komunikacji między serwisem a aktywnością. Ten wpis będzie kontynuacją poprzedniego wpisu i dziś poznasz bardziej zaawansowane metody, które możesz wdrożyć w swoim projekcie.

Jeżeli domyślnie uruchamiasz serwis w androidzie to tworzy się jedna instancja serwisu i pracuje w głównym wątku co aplikacja. Jeżeli zablokujemy taki wątek na kilka sekund, powoduje to rzucenie błędu do systemu i użytkownik dostanie komunikat, że aplikacja nie odpowiada. Takim przykładem może być przetwarzanie danych z bazy danych, pobieranie plików itp.
Istnieje również możliwość uruchomienia serwisu w całkowicie innym procesie, niezależnie od wszystkich zadań znajdujących się w głównym wątku. Proces ten posiada własny przydział pamięci, grupę wątków oraz priorytety przetwarzania zadań. Takie podejście może być bardzo przydatne, gdy trzeba działać niezależnie od głównego wątku aplikacji. Aby taki mechanizm wdrożyć w swoim projekcie skorzystaj z IntentService – dzięki niemu oddzielasz główny wątek od ciężkiej pracy. Nie ma potrzeby uruchamiania AsyncTaska. Klasa IntentService posiada właściwości cyklu życia komponentu Service, ale dodaje również wbudowane przetwarzanie zadań w tłe. Pamiętaj, że IntentService ma swoje ograniczenia, przetwarza zadania sekwencyjne, nie ma kontaktu z interfejsem użytkownika itp. Po więcej szczegółów zajrzyj do dokumentacji. W Androidzie Oreo IntentService został zastąpiony przez JobScheduler.  Innym sposobem uruchomienia usługi w innym procesie jest odpowiedni wpis w AndroidManifest:

<service
  android:name="LocalService"
  android:process=":externalProcess" />

Jeśli nazwa przypisana do atrybutu „android:process” zaczyna się od dwukropka („:”), usługa uruchomi się w osobnym procesie. Jeśli nazwa procesu zaczyna się od małej litery, usługa zostanie uruchomiona w globalnym procesie o tej nazwie pod warunkiem, że ma na to zezwolenie. Dzięki temu komponenty w różnych aplikacjach mogą współdzielić proces, zmniejszając zużycie zasobów. A co z komunikacją?
Komunikacja między różnymi procesami nosi nazwę IPC (Inter Process Communication). IPC może przyjąć jedną z dwóch form:

  1. asynchroniczną – nadawca nie czeka, aż odbiorca odbierze wiadomość. Jeżeli aktywność wyśle wiadomość do serwisu, to użytkownik może dalej korzystać z interfejsu – aktywność nie zostanie zawieszona. Wyniki będą zwrócone aktywności dopiero po zakończeniu zadania.
  2. synchroniczną – wysyłanie i odbieranie wiadomości jest zsynchronizowane. Jeżeli aktywność wyślę wiadomość do serwisu to będzie czekać, aż serwis odpowie, a jeżeli serwisowi zajmuje dużo czasu na zwrócenie wiadomości to aktywność może zostać zawieszona. Stosuj tą formę ostrożnie!

W serwisach międzyprocesowych możemy komunikować się wykorzystując Messengera lub implementując interfejs AIDL

2. Messenger.

Czym jest Messenger? To nic innego jak komunikator, który przesyła wiadomości między aktywnością a serwisem i odwrotnie oraz może pracować między różnymi procesami. Zobacz pierwsze na kod serwisu:

private final Messenger mMessenger = new Messenger(new IncomingHandler());
@Nullable
@Override
public IBinder onBind(Intent intent) {
    return mMessenger.getBinder();
}
class IncomingHandler extends Handler {
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case 0:
               mStartCounting = false;
                break;
            default:
                super.handleMessage(msg);
        }
    }
}

Metoda onCreate() nie zmienia się (zobacz pierwszą cześć). Po stronie aktywności mamy taki kod:

private Messenger mService  = null;
private ServiceConnection mConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        mService  = new Messenger(service);
        mBound = true;
    }
    @Override
    public void onServiceDisconnected(ComponentName name) {
        mService = null;
        mBound = false;
    }
};
public void sendMessenge() {
    if (!mBound) return;
    int MSG_STOP = 0;
    Message msg = Message.obtain(null, MSG_STOP);
    try {
        mService.send(msg);
    } catch (RemoteException e) {
        e.printStackTrace();
    }
}

Tak jak we wcześniejszym wpisie korzystamy z bindService(intent, mConnection, Context.BIND_AUTO_CREATE). Aby wysłać wiadomość wywołujemy metodę sendMessenge(). Proste? Proste 🙂
Messenger – ta klasa reprezentuje odniesienie do Handler’a, z którego klienci (na przykład aktywność) mogą korzystać w celu wysyłania do usługi wiadomości. Pozwala to na realizację komunikacji opartej na komunikatach w różnych procesach. W naszym przypadku, po połączeniu się z usługą, aktywność otrzymuje komunikator zainicjowany przez usługę. Klient może go używać do wysyłania wywołań IPC. Wiadomości przychodzące muszą być przetwarzane w metodzie handleMessageMessage – definiuje wiadomość zawierającą opis i dowolny obiekt danych, który można wysłać do HandleraHandler – pozwala na wysyłanie i przetwarzanie obiektów Message powiązanych z wątkiem MessageQueue.
Jeśli chcesz otrzymywać odpowiedzi od usługi, musisz zainicjować własny komunikator i wysłać wiadomość do usługi z polem replyTo. W ten sposób, gdy usługa otrzyma wiadomość zawierającą pole replyTo z komunikatorem, wyodrębni ten komunikator i wyśle przez niego informacje zwrotną. Po stronie aktywności komunikator tworzymy w ten sposób:

private final Messenger mActivityMessenger = new Messenger(new ActivityHandler(this));
static class ActivityHandler extends Handler {
    private final WeakReference<MainActivity> mActivity;
    public ActivityHandler(MainActivity activity) {
        mActivity = new WeakReference<>(activity);
    }
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case 1:
                Log.d("TAG", "Activity: "+ msg.getData().getInt("number") );
            break;
        }
    }
}

Aby sprawdzić stan liczby to możemy zrobić taką metodę:

private void checkNumber(){
    if (mBound) {
        try {
            Message msg = Message.obtain(null, 1, 0, 0);
            msg.replyTo = mActivityMessenger;
            mService.send(msg);
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }
}

Po stronie usługi w metodzie handleMessage do pętli switch dodajemy:

case 1:
    Bundle bundle = new Bundle();
    bundle.putInt("number",mCounter);
    Message replyMsg = Message.obtain(null, 1);
    replyMsg.setData(bundle);
    Messenger activityMessenger = msg.replyTo;
    try {
        activityMessenger.send(replyMsg);
    } catch (RemoteException e) {
        e.printStackTrace();
    }
    break;

Zauważ też, że Android BinderProxy, którego komunikator używa do wysyłania danych między procesami, ma niewielkie limity rozmiarów i może spowodować błąd android.os.TransactionTooLargeException. Dokumentacja informuje o limicie 1Mb. Nawet przez podzielenie wiadomości na mniejsze fragmenty, może pojawić się wyjątek „JavaBinder exception “FAILED BINDER TRANSACTION”. Zaleca się więc użycie ContentProvider lub ParcelFileDescriptor w przypadku przesyłania dużych danych.

3. AIDL (Android Interface Definition Language).

Co jeśli masz dwie aplikacje, które mają podobną logikę i korzystają wspólnie z jednej bazy? Załóżmy, że masz aplikację z serwisem oraz klientem. Serwis komunikuje się z bazą danych. Klient i drugi klient (druga aplikacja) chcą pobrać dane z tych baz. Jakbyś to wykonał?
Zasadniczo serwis definiuje interfejs metod, które mogą zostać wywołane przez klienta. Najprostszym i najbardziej popularnym sposobem jest
stworzenie interfejsu w języku AIDL zdefiniowanym w pliku .aidl. Kompilacja pliku AIDL generuje kod Java, który obsługuje komunikację IPC. Pierwszym krokiem jest zdefiniowanie interfejsu (kontraktu komunikacji) w pliku .aidl. Opis interfejsu zawiera definicje metod, które proces klienta może wywołać w procesie serwisu, powracając do naszego przykładu z pierwszej części, plik .aidl będzie miał postać:

// IMyAidlInterface.aidl
package net.myenv.myapplication;
interface IMyAidlInterface {
    void startCounting(boolean b);
    int getNumber();
}

Taki plik możesz stworzyć w Android Studio klikając prawym klawiszem na projekt, następnie NEW > AIDL > AIDL File. Po stworzeniu pliku przebuduj projekt, aby wygenerował się plik Java z interfejsem. Plik zapisze się w src/aidl/, a kiedy skompilujesz swoją aplikację, narzędzia SDK wygenerują plik interfejsu w katalogu twojego projektu gen/. Wygenerowana nazwa pliku jest zgodna z nazwą pliku aidl.
Kolejnym krokiem jest zaimplementowanie interfejsu w serwisie. Ten interfejs ma wewnętrzną klasę abstrakcyjną o nazwie Stub, która rozszerza Binder i implementuje metody z interfejsu AIDL. więc nasz usług może wyglądać tak:

@Nullable
@Override
public IBinder onBind(Intent intent) {
    return mBinder;
}
private IMyAidlInterface.Stub mBinder = new IMyAidlInterface.Stub() {
    @Override
    public void startCounting(boolean b) throws RemoteException {
        mStartCounting = false;
    }
    @Override
    public int getNumber() throws RemoteException {
        return mCounter;
    }
};

Po wdrożeniu interfejsu dla swojej usługi należy udostępnić go klientom, aby mogli się z nim połączyć. Klasa Stub definiuje również kilka metod pomocniczych, w tym metodę asInterface(), która pobiera IBinder i zwraca instancję interfejsu pośredniczącego. W aktywności korzystamy z wiązanej usługi czyli robimy to tak:

IMyAidlInterface mService;
 private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mService = IMyAidlInterface.Stub.asInterface(service);
            mBound = true;
        }
        @Override
        public void onServiceDisconnected(ComponentName name) {
            mService = null;
            mBound = false;
        }
    };

I jeszcze podłączenie się do usługi:

bindService(intent, mConnection, Context.BIND_AUTO_CREATE);

Aby odebrać liczbę i zatrzymać liczenie, robimy to w ten sposób:

try {
    mService.startCounting(false);
    Log.d("TAG", "Activity: "+mService.getNumber() );
} catch (RemoteException e) {
    e.printStackTrace();
}

Podczas korzystania z Messengera tworzone są kolejki wszystkich żądań klienta, które usługa otrzymuje pojedynczo. Wszystko to dzieje się w jednym wątku. Jeśli chcesz, aby Twoja usługa obsługiwała jednocześnie wiele połączeń, musisz użyć AIDL bezpośrednio i upewnić się, że Twoja usługa jest zdolna do wielowątkowości, a także zapewnić bezpieczeństwo wątków. Nie można zagwarantować, że komunikacja będzie wykonywane w głównym wątku, więc od początku musisz pomyśleć o wielowątkowości i odpowiednio zbudować usługę, aby była bezpieczna dla wątków. Generalnie nie zaleca się stosowania AIDL bezpośrednio, ponieważ jest to bardzo skomplikowana implementacja i trzeba zapewnić bezpieczeństwo i obsługę wielowątkowości. Więc za nim zastosujesz tą metodę pomyśl czy jesteś wstanie spełnić te warunki. Pamiętaj, że to co przedstawiłem wyżej jest to bardzo uproszczony przykład. Jeżeli chcesz więcej poczytać o AIDL zajrzyj tutaj.

4. Inne metody komunikacji między serwisem a androidem.

Oprócz wymienionych metod możemy do tej listy dodać:

  • Event buszewnętrzna biblioteka, która upraszcza komunikację między komponentami takimi jak: nadawcami i odbiorcami zdarzeń, aktywnościami, fragmentami i wątkami oraz omija złożone i podatne na błędy zależności i problemy z cyklem życia.
  • SharedMemory – klasa wprowadzone w API Androida 27,  aby aplikacje mogły tworzyć i wykorzystywać pamięć współdzieloną za pomocą asmem (/dev/ashmem). Jeżeli chcesz wiedzieć w jaki sposób to zastosować zobacz ten poradnik.

Podsumowanie.

We wcześniejszym wpisie poznałeś podstawowe metody komunikacji między serwisem a aktywnością w androidzie. Kod, który omówiliśmy, działa w wątkach w tym samym procesie, ale kończy się niepowodzeniem w przypadku usług zdalnych, w których usługa działa w zupełnie innym procesie. Dlatego w tym wpisie przedstawiłem Ci bardziej zaawansowane metody komunikacji między procesowej.
W tym samouczku zobaczyłeś, jak stworzyć komunikację między serwisem a aktywnością, wysyłać dwukierunkowe wiadomości między procesami komunikacyjnymi (IPC) za pomocą Messenger i AIDL.
Jeżeli będzie zainteresowanie wielowątkowością to nie wykluczam o tym napisać na blogu 🙂 Jeżeli masz pytania zadawaj je przez komentarze lub formularz kontaktowy.
Aktualizacja [6.06.2018]: Dzięki uwagom JohnOliver naniosłem poprawki w pierwszej i drugiej cześć wpisu.

Co dalej?

  • Polub stronę MYENV na Facebooku oraz śledź mnie na Twitterze
  • Zachęcam do komentowania i pisania propozycji tematów, o których chcesz przeczytać
  • Poleć ten wpis za pomocą poniższych przycisków. Będę Ci za to bardzo wdzięczny 🙂
  • Życzę Ci miłego dnia i miłego kodowania 🙂