Android-Threading: Alles, was Sie wissen müssen

Veröffentlicht: 2022-03-11

Jeder Android-Entwickler muss sich irgendwann mit Threads in seiner Anwendung befassen.

Wenn eine Anwendung in Android gestartet wird, erstellt sie den ersten Ausführungsthread, der als „Hauptthread“ bezeichnet wird. Der Haupt-Thread ist für das Versenden von Ereignissen an die entsprechenden Benutzeroberflächen-Widgets sowie für die Kommunikation mit Komponenten aus dem Android-UI-Toolkit verantwortlich.

Damit Ihre Anwendung reaktionsfähig bleibt, ist es wichtig, die Verwendung des Haupt-Threads zur Ausführung von Vorgängen zu vermeiden, die dazu führen können, dass er blockiert bleibt.

Netzwerkoperationen und Datenbankaufrufe sowie das Laden bestimmter Komponenten sind gängige Beispiele für Operationen, die man im Hauptthread vermeiden sollte. Wenn sie im Hauptthread aufgerufen werden, werden sie synchron aufgerufen, was bedeutet, dass die Benutzeroberfläche vollständig nicht reagiert, bis der Vorgang abgeschlossen ist. Aus diesem Grund werden sie normalerweise in getrennten Threads ausgeführt, wodurch vermieden wird, dass die UI blockiert wird, während sie ausgeführt werden (dh sie werden asynchron von der UI ausgeführt).

Android bietet viele Möglichkeiten zum Erstellen und Verwalten von Threads, und es gibt viele Bibliotheken von Drittanbietern, die das Thread-Management viel angenehmer machen. Bei so vielen verschiedenen Ansätzen kann die Auswahl des richtigen jedoch ziemlich verwirrend sein.

In diesem Artikel lernen Sie einige gängige Szenarien in der Android-Entwicklung kennen, in denen Threading unerlässlich wird, sowie einige einfache Lösungen, die auf diese Szenarien und mehr angewendet werden können.

Threading in Android

In Android können Sie alle Threading-Komponenten in zwei grundlegende Kategorien einteilen:

  1. Threads, die an eine Aktivität/ein Fragment angehängt sind: Diese Threads sind an den Lebenszyklus der Aktivität/des Fragments gebunden und werden beendet, sobald die Aktivität/das Fragment zerstört wird.
  2. Threads, die keiner Aktivität/einem Fragment zugeordnet sind: Diese Threads können über die Lebensdauer der Aktivität/des Fragments (falls vorhanden), aus der sie hervorgegangen sind, weiter ausgeführt werden.

Threading-Komponenten, die an eine Aktivität/ein Fragment angehängt werden

AsyncTask

AsyncTask ist die grundlegendste Android-Komponente für das Threading. Es ist einfach zu bedienen und kann für grundlegende Szenarien gut sein.

Beispielverwendung:

 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 jedoch zu kurz, wenn Sie möchten, dass Ihre zurückgestellte Aufgabe über die Lebensdauer der Aktivität/des Fragments hinaus ausgeführt wird. Es ist erwähnenswert, dass selbst etwas so Einfaches wie eine Bildschirmdrehung dazu führen kann, dass die Aktivität zerstört wird.

Lader

Loader sind die Lösung für das oben genannte Problem. Loader können automatisch anhalten, wenn die Aktivität zerstört wird, und können sich auch selbst neu starten, nachdem die Aktivität neu erstellt wurde.

Es gibt hauptsächlich zwei Arten von Loadern: AsyncTaskLoader und CursorLoader . Später in diesem Artikel erfahren Sie mehr über CursorLoader .

AsyncTaskLoader ist ähnlich wie AsyncTask , aber etwas komplizierter.

Beispielverwendung:

 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(); } } }

Threading-Komponenten, die nicht an eine Aktivität/ein Fragment angehängt werden

Bedienung

Service ist eine Komponente, die nützlich ist, um lange (oder möglicherweise lange) Vorgänge ohne Benutzeroberfläche auszuführen.

Der Service wird im Haupt-Thread seines Hosting-Prozesses ausgeführt; Der Dienst erstellt keinen eigenen Thread und wird nicht in einem separaten Prozess ausgeführt, sofern Sie nichts anderes angeben.

Beispielverwendung:

 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; } }

Bei Service liegt es in Ihrer Verantwortung, ihn zu stoppen, wenn seine Arbeit abgeschlossen ist, indem Sie entweder die stopSelf() oder die Methode stopService() stopService() .

IntentService

Wie Service wird IntentService in einem separaten Thread ausgeführt und stoppt sich automatisch, nachdem es seine Arbeit abgeschlossen hat.

IntentService wird normalerweise für kurze Aufgaben verwendet, die keiner Benutzeroberfläche zugeordnet werden müssen.

Beispielverwendung:

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

Sieben Threading-Muster in Android

Anwendungsfall Nr. 1: Senden einer Anfrage über das Netzwerk, ohne dass eine Antwort vom Server erforderlich ist

Manchmal möchten Sie vielleicht eine API-Anfrage an einen Server senden, ohne sich um seine Antwort kümmern zu müssen. Beispielsweise können Sie ein Push-Registrierungstoken an das Back-End Ihrer Anwendung senden.

Da dies eine Anforderung über das Netzwerk erfordert, sollten Sie dies von einem anderen Thread als dem Haupt-Thread aus tun.

Option 1: AsyncTask oder Loader

Sie können AsyncTask oder Loader verwenden, um den Anruf zu tätigen, und es wird funktionieren.

AsyncTask und Loader sind jedoch beide vom Lebenszyklus der Aktivität abhängig. Das bedeutet, dass Sie entweder auf die Ausführung des Aufrufs warten und versuchen müssen, den Benutzer am Verlassen der Aktivität zu hindern, oder hoffen, dass er ausgeführt wird, bevor die Aktivität zerstört wird.

Option 2: Dienst

Service ist möglicherweise besser für diesen Anwendungsfall geeignet, da er nicht mit einer Aktivität verbunden ist. Es wird daher in der Lage sein, mit dem Netzwerkanruf fortzufahren, selbst nachdem die Aktivität zerstört wurde. Da die Antwort des Servers nicht benötigt wird, würde ein Dienst auch hier nicht einschränken.

Da jedoch ein Dienst auf dem UI-Thread ausgeführt wird, müssen Sie das Threading weiterhin selbst verwalten. Sie müssen auch sicherstellen, dass der Dienst beendet wird, sobald der Netzwerkaufruf abgeschlossen ist.

Dies würde mehr Aufwand erfordern, als für eine so einfache Aktion erforderlich sein sollte.

Option 3: IntentService

Dies wäre meiner Meinung nach die beste Option.

Da IntentService an keine Aktivität angehängt ist und auf einem Nicht-UI-Thread läuft, erfüllt es hier perfekt unsere Anforderungen. Darüber hinaus stoppt IntentService automatisch, sodass es auch nicht manuell verwaltet werden muss.

Anwendungsfall Nr. 2: Tätigen eines Netzwerkanrufs und Abrufen der Antwort vom Server

Dieser Anwendungsfall ist wahrscheinlich etwas häufiger. Beispielsweise möchten Sie möglicherweise eine API im Back-End aufrufen und ihre Antwort verwenden, um Felder auf dem Bildschirm auszufüllen.

Option 1: Dienst oder IntentService

Obwohl ein Service oder ein IntentService für den vorherigen Anwendungsfall gut abgeschnitten hat, wäre es keine gute Idee, sie hier zu verwenden. Der Versuch, Daten aus einem Service oder einem IntentService in den Haupt-UI-Thread zu bekommen, würde die Dinge sehr komplex machen.

Option 2: AsyncTask oder Loader

Auf den ersten Blick scheinen hier AsyncTask oder Loader die naheliegende Lösung zu sein. Sie sind einfach zu bedienen – einfach und unkompliziert.

Wenn Sie jedoch AsyncTask oder Ladeprogramme verwenden, werden Sie feststellen, dass Sie einigen Standardcode schreiben müssen. Darüber hinaus wird die Fehlerbehandlung mit diesen Komponenten zu einer großen Aufgabe. Selbst bei einem einfachen Netzwerkanruf müssen Sie sich möglicher Ausnahmen bewusst sein, sie abfangen und entsprechend handeln. Dies zwingt uns, unsere Antwort in eine benutzerdefinierte Klasse einzuschließen, die die Daten enthält, mit möglichen Fehlerinformationen, und ein Flag zeigt an, ob die Aktion erfolgreich war oder nicht.

Das ist ziemlich viel Arbeit für jeden einzelnen Anruf. Glücklicherweise gibt es jetzt eine viel bessere und einfachere Lösung: RxJava.

Option 3: RxJava

Sie haben vielleicht von RxJava gehört, der von Netflix entwickelten Bibliothek. Es ist fast magisch in Java.

RxAndroid ermöglicht die Verwendung von RxJava in Android und macht den Umgang mit asynchronen Aufgaben zum Kinderspiel. Hier erfahren Sie mehr über RxJava auf Android.

RxJava bietet zwei Komponenten: Observer und Subscriber .

Ein Beobachter ist eine Komponente, die eine Aktion enthält. Es führt diese Aktion aus und gibt das Ergebnis zurück, wenn es erfolgreich ist, oder einen Fehler, wenn es fehlschlägt.

Ein Subscriber hingegen ist eine Komponente, die das Ergebnis (oder den Fehler) von einem Observable erhalten kann, indem sie es abonniert.

Mit RxJava erstellen Sie zunächst ein Observable:

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

Sobald das Observable erstellt wurde, können Sie es abonnieren.

Mit der RxAndroid-Bibliothek können Sie den Thread steuern, in dem Sie die Aktion im Observable ausführen möchten, und den Thread, in dem Sie die Antwort (dh das Ergebnis oder den Fehler) erhalten möchten.

Mit diesen beiden Funktionen verketten Sie das Observable:

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

Scheduler sind Komponenten, die die Aktion in einem bestimmten Thread ausführen. AndroidSchedulers.mainThread() ist der Planer, der dem Haupt-Thread zugeordnet ist.

Da unser API-Aufruf mRestApi.getData() ist und ein Data zurückgibt, kann der grundlegende Aufruf wie folgt aussehen:

 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()));

Ohne auf andere Vorteile der Verwendung von RxJava einzugehen, können Sie bereits sehen, wie RxJava es uns ermöglicht, ausgereifteren Code zu schreiben, indem wir die Komplexität des Threading abstrahieren.

Anwendungsfall Nr. 3: Verkettung von Netzwerkaufrufen

Bei Netzwerkaufrufen, die nacheinander ausgeführt werden müssen (dh bei denen jede Operation von der Antwort/dem Ergebnis der vorherigen Operation abhängt), müssen Sie beim Generieren von Spaghetti-Code besonders vorsichtig sein.

Beispielsweise müssen Sie möglicherweise einen API-Aufruf mit einem Token durchführen, das Sie zuerst über einen anderen API-Aufruf abrufen müssen.

Option 1: AsyncTask oder Loader

Die Verwendung AsyncTask oder Loadern führt mit ziemlicher Sicherheit zu Spaghetti-Code. Es wird schwierig sein, die Gesamtfunktionalität richtig hinzubekommen, und es wird während Ihres gesamten Projekts eine enorme Menge an redundantem Boilerplate-Code erforderlich sein.

Option 2: RxJava mit flatMap

In RxJava nimmt der flatMap Operator einen ausgegebenen Wert von der Quell-Observable und gibt eine andere Observable zurück. Sie können ein Observable erstellen und dann ein weiteres Observable erstellen, indem Sie den ausgegebenen Wert des ersten verwenden, der sie im Grunde verkettet.

Schritt 1. Erstellen Sie das Observable, das das Token abruft:

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

Schritt 2. Erstellen Sie das Observable, das die Daten mit dem Token abruft:

 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); } }); }

Schritt 3. Verketten Sie die beiden Observables und abonnieren Sie:

 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));

Beachten Sie, dass die Verwendung dieses Ansatzes nicht auf Netzwerkanrufe beschränkt ist; Es kann mit jeder Reihe von Aktionen arbeiten, die in einer Sequenz, aber in separaten Threads ausgeführt werden müssen.

Alle oben genannten Anwendungsfälle sind recht einfach. Das Umschalten zwischen Threads erfolgte nur, nachdem jeder seine Aufgabe beendet hatte. Fortgeschrittenere Szenarien – beispielsweise wenn zwei oder mehr Threads aktiv miteinander kommunizieren müssen – können ebenfalls von diesem Ansatz unterstützt werden.

Anwendungsfall Nr. 4: Kommunizieren Sie mit dem UI-Thread von einem anderen Thread

Stellen Sie sich ein Szenario vor, in dem Sie eine Datei hochladen und die Benutzeroberfläche nach Abschluss aktualisieren möchten.

Da das Hochladen einer Datei lange dauern kann, muss der Benutzer nicht warten. Sie könnten einen Dienst und wahrscheinlich IntentService , um die Funktionalität hier zu implementieren.

In diesem Fall besteht die größere Herausforderung jedoch darin, eine Methode im UI-Thread aufzurufen, nachdem der Dateiupload (der in einem separaten Thread durchgeführt wurde) abgeschlossen ist.

Option 1: RxJava innerhalb des Dienstes

RxJava, entweder allein oder in einem IntentService , ist möglicherweise nicht ideal. Sie müssen einen Rückruf-basierten Mechanismus verwenden, wenn Sie ein Observable abonnieren, und IntentService ist für einfache synchrone Aufrufe und nicht für Rückrufe konzipiert.

Andererseits müssen Sie bei einem Service den Dienst manuell stoppen, was mehr Arbeit erfordert.

Option 2: BroadcastReceiver

Android stellt diese Komponente bereit, die globale Ereignisse (z. B. Batterieereignisse, Netzwerkereignisse usw.) sowie benutzerdefinierte Ereignisse überwachen kann. Sie können diese Komponente verwenden, um ein benutzerdefiniertes Ereignis zu erstellen, das ausgelöst wird, wenn der Upload abgeschlossen ist.

Dazu müssen Sie eine benutzerdefinierte Klasse erstellen, die BroadcastReceiver erweitert, sie im Manifest registrieren und Intent und IntentFilter , um das benutzerdefinierte Ereignis zu erstellen. Um das Ereignis auszulösen, benötigen Sie die sendBroadcast Methode.

Manifest:

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

Empfänger:

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

Absender:

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

Dieser Ansatz ist eine praktikable Option. Aber wie Sie bemerkt haben, ist es mit einiger Arbeit verbunden, und zu viele Sendungen können die Dinge verlangsamen.

Option 3: Handler verwenden

Ein Handler ist eine Komponente, die an einen Thread angehängt und dann dazu gebracht werden kann, über einfache Nachrichten oder Runnable Aufgaben eine Aktion für diesen Thread auszuführen. Es arbeitet in Verbindung mit einer anderen Komponente, Looper , die für die Nachrichtenverarbeitung in einem bestimmten Thread zuständig ist.

Wenn ein Handler erstellt wird, kann er ein Looper -Objekt im Konstruktor erhalten, das angibt, an welchen Thread der Handler angefügt ist. Wenn Sie einen an den Hauptthread angehängten Handler verwenden möchten, müssen Sie den dem Hauptthread zugeordneten Looper verwenden, indem Looper.getMainLooper() aufrufen.

In diesem Fall können Sie zum Aktualisieren der Benutzeroberfläche aus einem Hintergrund-Thread einen Handler erstellen, der an den UI-Thread angehängt ist, und dann eine Aktion als Runnable :

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

Dieser Ansatz ist viel besser als der erste, aber es gibt einen noch einfacheren Weg, dies zu tun …

Option 3: Verwenden von EventBus

EventBus , eine beliebte Bibliothek von GreenRobot, ermöglicht es Komponenten, sicher miteinander zu kommunizieren. Da wir in unserem Anwendungsfall nur die Benutzeroberfläche aktualisieren möchten, kann dies die einfachste und sicherste Wahl sein.

Schritt 1. Erstellen Sie eine Ereignisklasse. zB UIEvent .

Schritt 2. Abonnieren Sie die Veranstaltung.

 @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); }

Schritt 3. Posten Sie das Ereignis: EventBus.getDefault().post(new UIEvent());

Mit dem ThreadMode Parameter in der Anmerkung geben Sie den Thread an, auf dem Sie dieses Ereignis abonnieren möchten. In unserem Beispiel hier wählen wir den Hauptthread, da wir möchten, dass der Empfänger des Ereignisses die Benutzeroberfläche aktualisieren kann.

Sie können Ihre UIEvent -Klasse so strukturieren, dass sie bei Bedarf zusätzliche Informationen enthält.

Im Dienste:

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

In der Aktivität/dem Fragment:

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

Mit der EventBus library wird die Kommunikation zwischen Threads viel einfacher.

Anwendungsfall Nr. 5: Zwei-Wege-Kommunikation zwischen Threads basierend auf Benutzeraktionen

Angenommen, Sie bauen einen Mediaplayer und möchten, dass er auch dann weiter Musik abspielen kann, wenn der Anwendungsbildschirm geschlossen ist. In diesem Szenario möchten Sie, dass die Benutzeroberfläche mit dem Medien-Thread kommunizieren kann (z. B. Wiedergabe, Pause und andere Aktionen) und dass der Medien-Thread die Benutzeroberfläche basierend auf bestimmten Ereignissen (z. B. Fehler, Pufferstatus) aktualisiert , etc).

Ein vollständiges Mediaplayer-Beispiel würde den Rahmen dieses Artikels sprengen. Gute Tutorials finden Sie jedoch hier und hier.

Option 1: Verwenden von EventBus

Sie könnten hier EventBus verwenden. Es ist jedoch im Allgemeinen unsicher, ein Ereignis aus dem UI-Thread zu posten und es in einem Dienst zu empfangen. Dies liegt daran, dass Sie nicht wissen können, ob der Dienst ausgeführt wird, wenn Sie die Nachricht gesendet haben.

Option 2: Verwenden von BoundService

Ein BoundService ist ein Service , der an eine Aktivität/ein Fragment gebunden ist. Das bedeutet, dass die Aktivität/das Fragment immer weiß, ob der Dienst läuft oder nicht, und zusätzlich Zugriff auf die öffentlichen Methoden des Dienstes erhält.

Um es zu implementieren, müssen Sie einen benutzerdefinierten Binder innerhalb des Diensts erstellen und eine Methode erstellen, die den Dienst zurückgibt.

 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; } }

Um die Aktivität an den Dienst zu binden, müssen Sie ServiceConnection implementieren, die Klasse, die den Dienststatus überwacht, und die Methode bindService , um die Bindung vorzunehmen:

 // 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; } };

Ein vollständiges Implementierungsbeispiel finden Sie hier.

Um mit dem Dienst zu kommunizieren, wenn der Benutzer auf die Schaltfläche „Wiedergabe“ oder „Pause“ tippt, können Sie sich an den Dienst binden und dann die relevante öffentliche Methode für den Dienst aufrufen.

Wenn es ein Medienereignis gibt und Sie dies zurück an die Aktivität/das Fragment kommunizieren möchten, können Sie eine der früheren Techniken verwenden (z. B. BroadcastReceiver , Handler oder EventBus ).

Anwendungsfall Nr. 6: Aktionen parallel ausführen und Ergebnisse erzielen

Angenommen, Sie erstellen eine Touristen-App und möchten Attraktionen auf einer Karte anzeigen, die aus mehreren Quellen (verschiedenen Datenanbietern) abgerufen wurde. Da möglicherweise nicht alle Quellen zuverlässig sind, sollten Sie die fehlgeschlagenen ignorieren und die Karte trotzdem weiter rendern.

Um den Prozess zu parallelisieren, muss jeder API-Aufruf in einem anderen Thread stattfinden.

Option 1: Verwenden von RxJava

In RxJava können Sie mehrere Observables mit den Operatoren merge() oder concat() zu einer kombinieren. Sie können dann das „zusammengeführte“ Observable abonnieren und auf alle Ergebnisse warten.

Dieser Ansatz wird jedoch nicht wie erwartet funktionieren. Wenn ein API-Aufruf fehlschlägt, meldet das zusammengeführte Observable einen Gesamtfehler.

Option 2: Verwendung nativer Java-Komponenten

Der ExecutorService in Java erstellt eine feste (konfigurierbare) Anzahl von Threads und führt gleichzeitig Aufgaben auf ihnen aus. Der Dienst gibt ein Future -Objekt zurück, das schließlich alle Ergebnisse über die Methode invokeAll() .

Jede Aufgabe, die Sie an den ExecutorService senden, sollte in der Callable -Schnittstelle enthalten sein, die eine Schnittstelle zum Erstellen einer Aufgabe ist, die eine Ausnahme auslösen kann.

Sobald Sie die Ergebnisse von invokeAll() erhalten haben, können Sie jedes Ergebnis überprüfen und entsprechend fortfahren.

Angenommen, Sie haben drei Attraktionstypen, die von drei verschiedenen Endpunkten eingehen, und Sie möchten drei parallele Anrufe tätigen:

 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(); }

Auf diese Weise führen Sie alle Aktionen parallel aus. Sie können daher jede Aktion separat auf Fehler prüfen und gegebenenfalls einzelne Fehler ignorieren.

Dieser Ansatz ist einfacher als die Verwendung von RxJava. Es ist einfacher, kürzer und schlägt aufgrund einer Ausnahme nicht bei allen Aktionen fehl.

Anwendungsfall Nr. 7: Abfragen der lokalen SQLite-Datenbank

Beim Umgang mit einer lokalen SQLite-Datenbank wird empfohlen, die Datenbank von einem Hintergrundthread aus zu verwenden, da Datenbankaufrufe (insbesondere bei großen Datenbanken oder komplexen Abfragen) zeitaufwändig sein können, was zum Einfrieren der Benutzeroberfläche führt.

Wenn Sie SQLite-Daten abfragen, erhalten Sie ein Cursor -Objekt, das dann zum Abrufen der eigentlichen Daten verwendet werden kann.

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

Option 1: Verwenden von RxJava

Sie können RxJava verwenden und die Daten aus der Datenbank abrufen, genau wie wir Daten von unserem Back-End abrufen:

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

Sie können die von getLocalDataObservable() zurückgegebene Observable wie folgt verwenden:

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

Während dies sicherlich ein guter Ansatz ist, gibt es einen noch besseren, da es eine Komponente gibt, die genau für dieses Szenario entwickelt wurde.

Option 2: Verwendung von CursorLoader + ContentProvider

Android stellt CursorLoader , eine native Komponente zum Laden von SQLite-Daten und Verwalten des entsprechenden Threads. Es ist ein Loader , der einen Cursor zurückgibt, mit dem wir die Daten abrufen können, indem wir einfache Methoden wie getString() , getLong() usw. aufrufen.

 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 arbeitet mit der ContentProvider Komponente. Diese Komponente bietet eine Fülle von Echtzeit-Datenbankfunktionen (z. B. Änderungsbenachrichtigungen, Auslöser usw.), mit denen Entwickler viel einfacher eine bessere Benutzererfahrung implementieren können.

Es gibt keine Patentlösung für das Threading in Android

Android bietet viele Möglichkeiten, Threads zu handhaben und zu verwalten, aber keine davon ist eine Wunderwaffe.

Die Wahl des richtigen Threading-Ansatzes kann je nach Anwendungsfall den Unterschied ausmachen, um die Gesamtlösung einfach zu implementieren und zu verstehen. Die nativen Komponenten passen für einige Fälle gut, aber nicht für alle. Gleiches gilt für ausgefallene Lösungen von Drittanbietern.

Ich hoffe, Sie finden diesen Artikel nützlich, wenn Sie an Ihrem nächsten Android-Projekt arbeiten. Teilen Sie uns Ihre Erfahrungen mit dem Threading in Android oder anderen Anwendungsfällen, in denen die oben genannten Lösungen gut funktionieren – oder auch nicht – in den Kommentaren unten mit.