Wątki w Androidzie: wszystko, co musisz wiedzieć

Opublikowany: 2022-03-11

Każdy programista Androida w pewnym momencie musi poradzić sobie z wątkami w swojej aplikacji.

Gdy aplikacja jest uruchamiana w systemie Android, tworzy pierwszy wątek wykonania, znany jako wątek „główny”. Główny wątek odpowiada za wysyłanie zdarzeń do odpowiednich widżetów interfejsu użytkownika oraz komunikację z komponentami z zestawu narzędzi Android UI.

Aby aplikacja była responsywna, ważne jest, aby unikać używania głównego wątku do wykonywania jakichkolwiek operacji, które mogą spowodować jego zablokowanie.

Operacje sieciowe i wywołania bazy danych, a także ładowanie niektórych komponentów to typowe przykłady operacji, których należy unikać w głównym wątku. Gdy są wywoływane w wątku głównym, są wywoływane synchronicznie, co oznacza, że ​​interfejs użytkownika nie będzie odpowiadać do czasu zakończenia operacji. Z tego powodu są one zwykle wykonywane w oddzielnych wątkach, co pozwala uniknąć blokowania interfejsu użytkownika podczas ich wykonywania (tj. są wykonywane asynchronicznie z interfejsu użytkownika).

Android zapewnia wiele sposobów tworzenia i zarządzania wątkami, a istnieje wiele bibliotek innych firm, które sprawiają, że zarządzanie wątkami jest o wiele przyjemniejsze. Jednak przy tak wielu różnych podejściach wybór właściwego może być dość mylący.

W tym artykule dowiesz się o niektórych typowych scenariuszach rozwoju systemu Android, w których wątki stają się niezbędne, oraz o kilku prostych rozwiązaniach, które można zastosować w tych scenariuszach i nie tylko.

Wątki w Androidzie

W systemie Android możesz podzielić wszystkie składniki wątków na dwie podstawowe kategorie:

  1. Wątki dołączone do działania/fragmentu: Te wątki są powiązane z cyklem życia działania/fragmentu i są kończone, gdy tylko działanie/fragment zostanie zniszczony.
  2. Wątki, które nie są dołączone do żadnego działania/fragmentu: Te wątki mogą nadal działać po okresie istnienia działania/fragmentu (jeśli istnieje), z którego zostały utworzone.

Nawlekanie komponentów, które dołączają do działania/fragmentu

Zadanie asynchroniczne

AsyncTask to najbardziej podstawowy składnik systemu Android do obsługi wątków. Jest prosty w użyciu i może być dobry w przypadku podstawowych scenariuszy.

Przykładowe użycie:

 public class ExampleActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); new MyTask().execute(url); } private class MyTask extends AsyncTask<String, Void, String> { @Override protected String doInBackground(String... params) { String url = params[0]; return doSomeWork(url); } @Override protected void onPostExecute(String result) { super.onPostExecute(result); // do something with result } } }

AsyncTask nie działa jednak, jeśli chcesz, aby odroczone zadanie zostało uruchomione poza okres istnienia działania/fragmentu. Warto zauważyć, że nawet coś tak prostego jak obracanie ekranu może spowodować zniszczenie aktywności.

Ładowarki

Ładowarki są rozwiązaniem powyższego problemu. Ładowacze mogą automatycznie zatrzymać się po zniszczeniu działania, a także mogą się ponownie uruchomić po odtworzeniu działania.

Istnieją głównie dwa typy programów ładujących: AsyncTaskLoader i CursorLoader . Więcej o CursorLoader się w dalszej części tego artykułu.

AsyncTaskLoader jest podobny do AsyncTask , ale nieco bardziej skomplikowany.

Przykładowe użycie:

 public class ExampleActivity extends Activity{ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getLoaderManager().initLoader(1, null, new MyLoaderCallbacks()); } private class MyLoaderCallbacks implements LoaderManager.LoaderCallbacks { @Override public Loader onCreateLoader(int id, Bundle args) { return new MyLoader(ExampleActivity.this); } @Override public void onLoadFinished(Loader loader, Object data) { } @Override public void onLoaderReset(Loader loader) { } } private class MyLoader extends AsyncTaskLoader { public MyLoader(Context context) { super(context); } @Override public Object loadInBackground() { return someWorkToDo(); } } }

Nawlekanie komponentów, które nie są dołączone do działania/fragmentu

Usługa

Service to składnik przydatny do wykonywania długich (lub potencjalnie długich) operacji bez interfejsu użytkownika.

Service działa w głównym wątku swojego procesu hostingowego; usługa nie tworzy własnego wątku i nie działa w osobnym procesie, chyba że określisz inaczej.

Przykładowe użycie:

 public class ExampleService extends Service { @Override public int onStartCommand(Intent intent, int flags, int startId) { doSomeLongProccesingWork(); stopSelf(); return START_NOT_STICKY; } @Nullable @Override public IBinder onBind(Intent intent) { return null; } }

W przypadku Service Twoim obowiązkiem jest zatrzymanie jej po zakończeniu pracy przez wywołanie metody stopSelf() lub stopService() .

Usługa intencji

Podobnie jak Service , IntentService działa w osobnym wątku i zatrzymuje się automatycznie po zakończeniu pracy.

IntentService jest zwykle używany do krótkich zadań, które nie muszą być dołączone do żadnego interfejsu użytkownika.

Przykładowe użycie:

 public class ExampleService extends IntentService { public ExampleService() { super("ExampleService"); } @Override protected void onHandleIntent(Intent intent) { doSomeShortWork(); } }

Siedem wzorców wątków w Androidzie

Przypadek użycia nr 1: Wysyłanie żądania przez sieć bez wymagania odpowiedzi z serwera

Czasami możesz chcieć wysłać żądanie API do serwera bez konieczności martwienia się o jego odpowiedź. Na przykład możesz wysyłać token rejestracji wypychanej do zaplecza aplikacji.

Ponieważ wiąże się to z wysłaniem żądania przez sieć, powinieneś to zrobić z wątku innego niż wątek główny.

Opcja 1: AsyncTask lub programy ładujące

Możesz użyć AsyncTask lub programów ładujących do nawiązania połączenia i to zadziała.

Jednak zarówno AsyncTask , jak i programy ładujące są zależne od cyklu życia działania. Oznacza to, że albo będziesz musiał poczekać na wykonanie wywołania i spróbować uniemożliwić użytkownikowi opuszczenie działania, albo mieć nadzieję, że zostanie ono wykonane, zanim działanie zostanie zniszczone.

Opcja 2: Usługa

Service może lepiej pasować do tego przypadku użycia, ponieważ nie jest powiązana z żadną aktywnością. Dzięki temu będzie mógł kontynuować połączenie sieciowe nawet po zniszczeniu aktywności. Dodatkowo, ponieważ odpowiedź z serwera nie jest potrzebna, usługa również nie byłaby tutaj ograniczająca.

Jednak ponieważ usługa zacznie działać w wątku interfejsu użytkownika, nadal będziesz musiał samodzielnie zarządzać wątkami. Musisz także upewnić się, że usługa została zatrzymana po zakończeniu połączenia sieciowego.

Wymagałoby to więcej wysiłku, niż powinno być konieczne do tak prostej czynności.

Opcja 3: IntentService

To moim zdaniem byłaby najlepsza opcja.

Ponieważ IntentService nie łączy się z żadną aktywnością i działa w wątku innym niż interfejs użytkownika, doskonale spełnia nasze potrzeby. Co więcej, IntentService zatrzymuje się automatycznie, więc nie ma potrzeby ręcznego zarządzania nim.

Przypadek użycia nr 2: Wykonywanie połączenia sieciowego i uzyskiwanie odpowiedzi z serwera

Ten przypadek użycia jest prawdopodobnie nieco bardziej powszechny. Na przykład możesz chcieć wywołać interfejs API na zapleczu i użyć jego odpowiedzi do wypełnienia pól na ekranie.

Opcja 1: Usługa lub IntentService

Chociaż Service lub IntentService wypadły dobrze w poprzednim przypadku użycia, użycie ich tutaj nie byłoby dobrym pomysłem. Próba pobrania danych z Service lub IntentService do głównego wątku interfejsu użytkownika może bardzo skomplikować sprawę.

Opcja 2: AsyncTask lub programy ładujące

Na pierwszy rzut oka oczywistym rozwiązaniem wydaje się AsyncTask lub loadery. Są łatwe w użyciu — proste i nieskomplikowane.

Jednak podczas korzystania z AsyncTask lub programów ładujących zauważysz, że istnieje potrzeba napisania kodu standardowego. Co więcej, obsługa błędów staje się w przypadku tych komponentów głównym zadaniem. Nawet w przypadku prostego połączenia sieciowego musisz być świadomy potencjalnych wyjątków, złapać je i odpowiednio postępować. To zmusza nas do zapakowania naszej odpowiedzi w niestandardową klasę zawierającą dane, z możliwymi informacjami o błędach, a flaga wskazuje, czy akcja się powiodła, czy nie.

To sporo pracy przy każdym połączeniu. Na szczęście obecnie dostępne jest znacznie lepsze i prostsze rozwiązanie: RxJava.

Opcja 3: RxJava

Być może słyszałeś o RxJava, bibliotece opracowanej przez Netflix. To prawie magia w Javie.

RxAndroid umożliwia korzystanie z RxJava w systemie Android i sprawia, że ​​radzenie sobie z zadaniami asynchronicznymi jest dziecinnie proste. Możesz dowiedzieć się więcej o RxJava na Androida tutaj.

RxJava udostępnia dwa komponenty: Observer i Subscriber .

Obserwator to komponent, który zawiera pewną akcję. Wykonuje tę akcję i zwraca wynik, jeśli się powiedzie, lub błąd, jeśli się nie powiedzie.

Z drugiej strony subskrybent jest komponentem, który może otrzymać wynik (lub błąd) od obserwowalnego, subskrybując go.

W RxJava najpierw tworzysz obserwowalny:

 Observable.create((ObservableOnSubscribe<Data>) e -> { Data data = mRestApi.getData(); e.onNext(data); })

Po utworzeniu obserwowalnego, możesz go zasubskrybować.

Dzięki bibliotece RxAndroid możesz kontrolować wątek, w którym chcesz wykonać akcję w obserwowalnym, oraz wątek, w którym chcesz uzyskać odpowiedź (tj. wynik lub błąd).

Łączysz obserwowalny z tymi dwiema funkcjami:

 .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()

Harmonogramy to komponenty, które wykonują akcję w określonym wątku. AndroidSchedulers.mainThread() jest harmonogramem powiązanym z głównym wątkiem.

Biorąc pod uwagę, że nasze wywołanie API to mRestApi.getData() i zwraca obiekt Data , podstawowe wywołanie może wyglądać tak:

 Observable.create((ObservableOnSubscribe<Data>) e -> { try { Data data = mRestApi.getData(); e.onNext(data); } catch (Exception ex) { e.onError(ex); } }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(match -> Log.i(“rest api, "success"), throwable -> Log.e(“rest api, "error: %s" + throwable.getMessage()));

Nawet nie wchodząc w inne korzyści płynące z używania RxJava, możesz już zobaczyć, w jaki sposób RxJava pozwala nam pisać bardziej dojrzały kod, oddzielając złożoność wątków.

Przypadek użycia nr 3: Łączenie połączeń sieciowych

W przypadku wywołań sieciowych, które muszą być wykonywane po kolei (tj. gdy każda operacja zależy od odpowiedzi/wynika poprzedniej operacji), należy szczególnie uważać na generowanie kodu spaghetti.

Na przykład może być konieczne wykonanie wywołania interfejsu API z tokenem, który należy najpierw pobrać za pomocą innego wywołania interfejsu API.

Opcja 1: AsyncTask lub programy ładujące

Używanie AsyncTask lub programów ładujących prawie na pewno doprowadzi do kodu spaghetti. Ogólna funkcjonalność będzie trudna do uzyskania i będzie wymagała ogromnej ilości nadmiarowego kodu wzorcowego w całym projekcie.

Opcja 2: RxJava przy użyciu flatMap

W RxJava operator flatMap pobiera wyemitowaną wartość ze źródła obserwowalnego i zwraca inny obserwowalny. Możesz utworzyć obserwowalny, a następnie utworzyć inny obserwowalny, używając wartości wyemitowanej z pierwszego, co w zasadzie połączy je w łańcuch.

Krok 1. Utwórz obserwowalny, który pobiera token:

 public Observable<String> getTokenObservable() { return Observable.create(subscriber -> { try { String token = mRestApi.getToken(); subscriber.onNext(token); } catch (IOException e) { subscriber.onError(e); } }); }

Krok 2. Utwórz obserwowalny, który pobiera dane za pomocą tokena:

 public Observable<String> getDataObservable(String token) { return Observable.create(subscriber -> { try { Data data = mRestApi.getData(token); subscriber.onNext(data); } catch (IOException e) { subscriber.onError(e); } }); }

Krok 3. Połącz ze sobą dwa obserwable i zasubskrybuj:

 getTokenObservable() .flatMap(new Function<String, Observable<Data>>() { @Override public Observable<Data> apply(String token) throws Exception { return getDataObservable(token); } }) .subscribe(data -> { doSomethingWithData(data) }, error -> handleError(e));

Należy zauważyć, że zastosowanie tego podejścia nie ogranicza się do połączeń sieciowych; może działać z dowolnym zestawem akcji, które należy uruchomić w sekwencji, ale w osobnych wątkach.

Wszystkie powyższe przypadki użycia są dość proste. Przełączanie między wątkami następowało dopiero po zakończeniu każdego zadania. Bardziej zaawansowane scenariusze — na przykład, w których co najmniej dwa wątki muszą aktywnie komunikować się ze sobą — również mogą być obsługiwane przez to podejście.

Przypadek użycia nr 4: komunikuj się z wątkiem interfejsu użytkownika z innego wątku

Rozważ scenariusz, w którym chcesz przesłać plik i zaktualizować interfejs użytkownika po jego zakończeniu.

Ponieważ przesyłanie pliku może zająć dużo czasu, nie ma potrzeby, aby użytkownik czekał. Możesz użyć usługi i prawdopodobnie IntentService , aby zaimplementować tę funkcjonalność tutaj.

W tym przypadku jednak większym wyzwaniem jest możliwość wywołania metody w wątku interfejsu użytkownika po zakończeniu przesyłania pliku (które zostało wykonane w osobnym wątku).

Opcja 1: RxJava wewnątrz usługi

RxJava, samodzielnie lub wewnątrz IntentService , może nie być idealny. Będziesz musiał użyć mechanizmu opartego na wywołaniach zwrotnych podczas subskrybowania Observable , a IntentService jest zbudowany do wykonywania prostych wywołań synchronicznych, a nie wywołań zwrotnych.

Z drugiej strony w przypadku Service musisz ręcznie zatrzymać usługę, co wymaga więcej pracy.

Opcja 2: Odbiorca transmisji

Android zapewnia ten składnik, który może nasłuchiwać globalnych zdarzeń (np. zdarzeń dotyczących baterii, zdarzeń sieciowych itp.), a także zdarzeń niestandardowych. Możesz użyć tego komponentu, aby utworzyć niestandardowe zdarzenie, które jest wyzwalane po zakończeniu przesyłania.

Aby to zrobić, musisz utworzyć niestandardową klasę, która rozszerza BroadcastReceiver , zarejestrować ją w manifeście i użyć Intent i IntentFilter , aby utworzyć niestandardowe zdarzenie. Aby wyzwolić zdarzenie, będziesz potrzebować metody sendBroadcast .

Oczywisty:

 <receiver android:name="UploadReceiver"> <intent-filter> <action android:name="com.example.upload"> </action> </intent-filter> </receiver>

Odbiorca:

 public class UploadReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (intent.getBoolean(“success”, false) { Activity activity = (Activity)context; activity.updateUI(); } }

Nadawca:

 Intent intent = new Intent(); intent.setAction("com.example.upload"); sendBroadcast(intent);

Takie podejście jest realną opcją. Ale jak zauważyłeś, wymaga to trochę pracy, a zbyt wiele transmisji może spowolnić działanie.

Opcja 3: Korzystanie z programu Handler

Handler to komponent, który można dołączyć do wątku, a następnie wykonać w tym wątku jakąś akcję za pomocą prostych wiadomości lub zadań Runnable . Działa w połączeniu z innym komponentem Looper , który odpowiada za przetwarzanie wiadomości w określonym wątku.

Kiedy tworzony jest Handler , może uzyskać obiekt Looper w konstruktorze, który wskazuje, do którego wątku jest dołączony handler. Jeśli chcesz użyć funkcji obsługi dołączonej do głównego wątku, musisz użyć loopera skojarzonego z głównym wątkiem, wywołując Looper.getMainLooper() .

W takim przypadku, aby zaktualizować interfejs użytkownika z wątku w tle, możesz utworzyć procedurę obsługi dołączoną do wątku interfejsu użytkownika, a następnie opublikować akcję jako Runnable :

 Handler handler = new Handler(Looper.getMainLooper()); handler.post(new Runnable() { @Override public void run() { // update the ui from here } });

To podejście jest o wiele lepsze niż pierwsze, ale istnieje jeszcze prostszy sposób na zrobienie tego…

Opcja 3: Korzystanie z EventBus

EventBus , popularna biblioteka firmy GreenRobot, umożliwia bezpieczną komunikację między komponentami. Ponieważ nasz przypadek użycia to taki, w którym chcemy tylko zaktualizować interfejs użytkownika, może to być najprostszy i najbezpieczniejszy wybór.

Krok 1. Utwórz klasę wydarzenia. np. UIEvent .

Krok 2. Zapisz się na wydarzenie.

 @Subscribe(threadMode = ThreadMode.MAIN) public void onUIEvent(UIEvent event) {/* Do something */}; register and unregister eventbus : @Override public void onStart() { super.onStart(); EventBus.getDefault().register(this); } @Override public void onStop() { super.onStop(); EventBus.getDefault().unregister(this); }

Krok 3. Opublikuj zdarzenie: EventBus.getDefault().post(new UIEvent());

Za pomocą parametru ThreadMode w adnotacji określasz wątek, w którym chcesz subskrybować to wydarzenie. W naszym przykładzie wybieramy główny wątek, ponieważ chcemy, aby odbiorca zdarzenia mógł zaktualizować interfejs użytkownika.

W razie potrzeby możesz uporządkować klasę UIEvent , aby zawierała dodatkowe informacje.

W serwisie:

 class UploadFileService extends IntentService { // … Boolean success = uploadFile(File file); EventBus.getDefault().post(new UIEvent(success)); // ... }

W ćwiczeniu/fragmencie:

 @Subscribe(threadMode = ThreadMode.MAIN) public void onUIEvent(UIEvent event) {//show message according to the action success};

Korzystając z EventBus library , komunikacja między wątkami staje się znacznie prostsza.

Przypadek użycia nr 5: Dwustronna komunikacja między wątkami oparta na działaniach użytkownika

Załóżmy, że budujesz odtwarzacz multimedialny i chcesz, aby mógł on kontynuować odtwarzanie muzyki, nawet gdy ekran aplikacji jest zamknięty. W tym scenariuszu będziesz chciał, aby interfejs użytkownika mógł komunikować się z wątkiem multimediów (np. Odtwórz, wstrzymaj i inne akcje), a także będziesz chciał, aby wątek multimediów aktualizował interfejs użytkownika w oparciu o określone zdarzenia (np. błąd, stan buforowania itp.).

Pełny przykład odtwarzacza multimedialnego wykracza poza zakres tego artykułu. Możesz jednak znaleźć dobre tutoriale tutaj i tutaj.

Opcja 1: Korzystanie z EventBus

Możesz użyć EventBus tutaj. Jednak zazwyczaj nie jest bezpieczne publikowanie zdarzenia z wątku interfejsu użytkownika i odbieranie go w usłudze. Dzieje się tak, ponieważ po wysłaniu wiadomości nie masz możliwości sprawdzenia, czy usługa jest uruchomiona.

Opcja 2: Korzystanie z usługi BoundService

BoundService to Service powiązana z działaniem/fragmentem. Oznacza to, że aktywność/fragment zawsze wie, czy usługa działa, czy nie, a ponadto uzyskuje dostęp do publicznych metod usługi.

Aby to zaimplementować, musisz utworzyć niestandardowy Binder wewnątrz usługi i utworzyć metodę, która zwraca usługę.

 public class MediaService extends Service { private final IBinder mBinder = new MediaBinder(); public class MediaBinder extends Binder { MediaService getService() { // Return this instance of LocalService so clients can call public methods return MediaService.this; } } @Override public IBinder onBind(Intent intent) { return mBinder; } }

Aby powiązać aktywność z usługą, należy zaimplementować ServiceConnection , czyli klasę monitorującą stan usługi i użyć metody bindService do wykonania powiązania:

 // in the activity MediaService mService; // flag indicates the bound status boolean mBound; @Override protected void onStart() { super.onStart(); // Bind to LocalService Intent intent = new Intent(this, MediaService.class); bindService(intent, mConnection, Context.BIND_AUTO_CREATE); } private ServiceConnection mConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName className, IBinder service) { MediaBinder binder = (MediaBinder) service; mService = binder.getService(); mBound = true; } @Override public void onServiceDisconnected(ComponentName arg0) { mBound = false; } };

Pełny przykład wdrożenia znajdziesz tutaj.

Aby komunikować się z usługą, gdy użytkownik naciśnie przycisk Odtwórz lub Wstrzymaj, możesz powiązać z usługą, a następnie wywołać odpowiednią metodę publiczną w usłudze.

Gdy występuje zdarzenie medialne i chcesz przekazać je z powrotem do działania/fragmentu, możesz użyć jednej z wcześniejszych technik (np. BroadcastReceiver , Handler lub EventBus ).

Przypadek użycia nr 6: Równoległe wykonywanie działań i uzyskiwanie wyników

Załóżmy, że tworzysz aplikację turystyczną i chcesz pokazać atrakcje na mapie pobranej z wielu źródeł (od różnych dostawców danych). Ponieważ nie wszystkie źródła mogą być wiarygodne, możesz zignorować te, które zawiodły i mimo to kontynuować renderowanie mapy.

Aby zrównoleglić proces, każde wywołanie API musi mieć miejsce w innym wątku.

Opcja 1: Korzystanie z RxJava

W RxJava możesz połączyć wiele obserwowalnych w jedno za pomocą operatorów merge() lub concat() . Następnie możesz subskrybować „scalony” obserwowalny i czekać na wszystkie wyniki.

To podejście jednak nie zadziała zgodnie z oczekiwaniami. Jeśli jedno wywołanie API nie powiedzie się, scalony element obserwowalny zgłosi ogólną awarię.

Opcja 2: Korzystanie z natywnych komponentów Java

Usługa ExecutorService w Javie tworzy ustaloną (konfigurowalną) liczbę wątków i wykonuje na nich zadania jednocześnie. Usługa zwraca obiekt Future , który ostatecznie zwraca wszystkie wyniki za pomocą metody invokeAll() .

Każde zadanie wysyłane do ExecutorService powinno być zawarte w interfejsie Callable , który jest interfejsem do tworzenia zadania, które może zgłosić wyjątek.

Gdy uzyskasz wyniki z invokeAll() , możesz sprawdzić każdy wynik i odpowiednio postępować.

Załóżmy na przykład, że masz trzy typy atrakcji przychodzące z trzech różnych punktów końcowych i chcesz wykonać trzy równoległe połączenia:

 ExecutorService pool = Executors.newFixedThreadPool(3); List<Callable<Object>> tasks = new ArrayList<>(); tasks.add(new Callable<Object>() { @Override public Integer call() throws Exception { return mRest.getAttractionType1(); } }); // ... try { List<Future<Object>> results = pool.invokeAll(tasks); for (Future result : results) { try { Object response = result.get(); if (response instance of AttractionType1... {} if (response instance of AttractionType2... {} ... } catch (ExecutionException e) { e.printStackTrace(); } } } catch (InterruptedException e) { e.printStackTrace(); }

W ten sposób wykonujesz wszystkie akcje równolegle. W związku z tym można sprawdzić błędy w każdej akcji osobno i odpowiednio zignorować poszczególne błędy.

Takie podejście jest łatwiejsze niż korzystanie z RxJava. Jest prostszy, krótszy i nie powoduje niepowodzenia wszystkich działań z powodu jednego wyjątku.

Przypadek użycia nr 7: Zapytanie o lokalną bazę danych SQLite

W przypadku lokalnej bazy danych SQLite zaleca się korzystanie z bazy danych z wątku w tle, ponieważ wywołania bazy danych (zwłaszcza w przypadku dużych baz danych lub złożonych zapytań) mogą być czasochłonne, powodując zawieszanie się interfejsu użytkownika.

Podczas zapytania o dane SQLite otrzymujesz obiekt Cursor , który może być następnie użyty do pobrania rzeczywistych danych.

 Cursor cursor = getData(); String name = cursor.getString(<colum_number>);

Opcja 1: Korzystanie z RxJava

Możesz użyć RxJava i pobrać dane z bazy danych, tak jak my pobieramy dane z naszego zaplecza:

 public Observable<Cursor> getLocalDataObservable() { return Observable.create(subscriber -> { Cursor cursor = mDbHandler.getData(); subscriber.onNext(cursor); }); }

Możesz użyć obserwowalnego zwróconego przez getLocalDataObservable() w następujący sposób:

 getLocalDataObservable() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(cursor -> String name = cursor.getString(0), throwable -> Log.e(“db, "error: %s" + throwable.getMessage()));

Chociaż jest to z pewnością dobre podejście, jest jeszcze lepsze, ponieważ istnieje komponent stworzony specjalnie dla tego scenariusza.

Opcja 2: Korzystanie z CursorLoader + ContentProvider

System Android udostępnia CursorLoader , natywny składnik do ładowania danych SQLite i zarządzania odpowiednim wątkiem. Jest to Loader zwracający Cursor , którego możemy użyć do pobrania danych, wywołując proste metody, takie jak getString() , getLong() , itp.

 public class SimpleCursorLoader extends FragmentActivity implements LoaderManager.LoaderCallbacks<Cursor> { public static final String TAG = SimpleCursorLoader.class.getSimpleName(); private static final int LOADER_ID = 0x01; private TextView textView; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.simple_cursor_loader); textView = (TextView) findViewById(R.id.text_view); getSupportLoaderManager().initLoader(LOADER_ID, null, this); } public Loader<Cursor> onCreateLoader(int i, Bundle bundle) { return new CursorLoader(this, Uri.parse("content://com.github.browep.cursorloader.data") , new String[]{"col1"}, null, null, null); } public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) { if (cursor != null && cursor.moveToFirst()) { String text = textView.getText().toString(); while (cursor.moveToNext()) { text += "<br />" + cursor.getString(1); cursor.moveToNext(); } textView.setText(Html.fromHtml(text) ); } } public void onLoaderReset(Loader<Cursor> cursorLoader) { } }

CursorLoader współpracuje ze składnikiem ContentProvider . Ten składnik zapewnia mnóstwo funkcji baz danych w czasie rzeczywistym (np. powiadomienia o zmianach, wyzwalacze itp.), które umożliwiają programistom znacznie łatwiejsze zaimplementowanie lepszego doświadczenia użytkownika.

Nie ma rozwiązania Silver Bullet dla wątków w Androidzie

Android zapewnia wiele sposobów obsługi wątków i zarządzania nimi, ale żaden z nich nie jest srebrną kulą.

Wybór odpowiedniego podejścia do wątków, w zależności od przypadku użycia, może mieć duży wpływ na to, aby ogólne rozwiązanie było łatwe do wdrożenia i zrozumienia. Natywne komponenty pasują dobrze w niektórych przypadkach, ale nie we wszystkich. To samo dotyczy wymyślnych rozwiązań innych firm.

Mam nadzieję, że ten artykuł okaże się przydatny podczas pracy nad kolejnym projektem na Androida. Podziel się z nami swoim doświadczeniem związanym z wątkowaniem w systemie Android lub dowolnym przypadkiem użycia, w którym powyższe rozwiązania działają dobrze - lub nie - w komentarzach poniżej.