Prozesse und Threads

Quickview

  • Jede Anwendung läuft in einem eigenen Prozess und alle Komponenten der Anwendung laufen standardmäßig in diesem Prozess
  • Alle langsamen, blockierenden Operationen in einer Aktivität sollten in einem neuen Thread ausgeführt werden, um eine Verlangsamung der Benutzeroberfläche zu vermeiden

In diesem Dokument

  1. Prozesse
    1. Prozesslebenszyklus
  2. Threads
    1. Werker-Threads
    2. Thread-safe methods
  3. Interprocess Communication

Wenn eine Anwendungskomponente startet und keine anderen Komponenten in der Anwendung laufen, startet das Android-System einen neuen Linux-Prozess für die Anwendung mit einem einzigen Ausführungsstrang. Standardmäßig werden alle Komponenten einer Anwendung im selben Prozess und Thread (dem sogenannten „Hauptthread“) ausgeführt. Wenn eine Anwendungskomponente gestartet wird und bereits ein Prozess für diese Anwendung existiert (weil eine andere Komponente der Anwendung existiert), dann wird die Komponente innerhalb dieses Prozesses gestartet und verwendet denselben Thread zur Ausführung. Sie können jedoch dafür sorgen, dass verschiedene Komponenten in Ihrer Anwendung in separaten Prozessen ausgeführt werden, und Sie können zusätzliche Threads für jeden Prozess erstellen.

Dieses Dokument beschreibt, wie Prozesse und Threads in einer Android-Anwendung funktionieren.

Prozesse

Standardmäßig laufen alle Komponenten derselben Anwendung im selben Prozess, und die meisten Anwendungen sollten dies nicht ändern. Wenn Sie jedoch feststellen, dass Sie steuern müssen, zu welchem Prozess eine bestimmte Komponente gehört, können Sie dies in der Manifestdatei tun.

Der Manifest-Eintrag für jede Art von Komponentenelement – <activity>, <service>, <receiver> und <provider> – unterstützt ein android:process-Attribut, das einen Prozess angeben kann, in dem diese Komponente laufen soll. Sie können dieses Attribut so setzen, dass jede Komponente in einem eigenen Prozess läuft oder dass einige Komponenten einen Prozess gemeinsam nutzen, während andere dies nicht tun. Sie könnenandroid:process auch so einstellen, dass Komponenten verschiedener Anwendungen im selben Prozess laufen – vorausgesetzt, die Anwendungen haben dieselbe Linux-Benutzer-ID und sind mit denselben Zertifikaten signiert.

Das <application>-Element unterstützt auch ein android:process-Attribut, um einen Standardwert festzulegen, der für alle Komponenten gilt.

Android kann sich dazu entschließen, einen Prozess zu einem bestimmten Zeitpunkt zu beenden, wenn der Speicherplatz knapp ist und von anderen Prozessen benötigt wird, die dem Benutzer unmittelbar dienen. Anwendungskomponenten, die in dem beendeten Prozess laufen, werden folglich zerstört. Für diese Komponenten wird wieder ein Prozess gestartet, wenn es wieder Arbeit für sie gibt.

Bei der Entscheidung, welche Prozesse beendet werden sollen, wägt das Android-System ihre relative Bedeutung für den Benutzer ab. So wird zum Beispiel ein Prozess, der Aktivitäten hostet, die auf dem Bildschirm nicht mehr sichtbar sind, eher beendet als ein Prozess, der sichtbare Aktivitäten hostet. Die Entscheidung, ob ein Prozess beendet werden soll, hängt also vom Zustand der Komponenten ab, die in diesem Prozess laufen. Die Regeln, nach denen entschieden wird, welche Prozesse beendet werden sollen, werden im Folgenden erläutert.

Prozesslebenszyklus

Das Android-System versucht, einen Anwendungsprozess so lange wie möglich aufrechtzuerhalten, muss aber irgendwann alte Prozesse entfernen, um Speicher für neue oder wichtigere Prozesse zu gewinnen. Um zu entscheiden, welche Prozesse beibehalten und welche beendet werden sollen, ordnet das System jeden Prozess in eine „Wichtigkeitshierarchie“ ein, die auf den im Prozess laufenden Komponenten und dem Zustand dieser Komponenten basiert. Die Prozesse mit der geringsten Wichtigkeit werden zuerst eliminiert, dann die mit der nächstniedrigeren Wichtigkeit und so weiter, je nach Bedarf, um Systemressourcen wiederherzustellen.

Es gibt fünf Stufen in der Wichtigkeitshierarchie. In der folgenden Liste sind die verschiedenen Prozesstypen in der Reihenfolge ihrer Wichtigkeit aufgeführt (der erste Prozess ist der wichtigste und wird als letzter eliminiert):

  1. Prozess im Vordergrund

    Ein Prozess, der für das, was der Benutzer gerade tut, erforderlich ist. Ein Prozess wird als im Vordergrund befindlich betrachtet, wenn eine der folgenden Bedingungen zutrifft:

    • Er hostet einen Activity, mit dem der Benutzer interagiert (die onResume()-Methode des Activity wurde aufgerufen).
    • Es beherbergt ein Service, das an die Aktivität gebunden ist, mit der der Benutzer interagiert.
    • Es beherbergt ein Service, das „im Vordergrund“ läuft – der Dienst hat startForeground() aufgerufen.
    • Es beherbergt einen Service, der einen seiner Lifecycle-Callbacks (onCreate(), onStart() oder onDestroy()) ausführt.
    • Es beherbergt einen BroadcastReceiver, der seine onReceive()-Methode ausführt.

    Im Allgemeinen gibt es zu einem bestimmten Zeitpunkt nur wenige Prozesse im Vordergrund. Sie werden nur als letzter Ausweg beendet, wenn der Speicher so knapp ist, dass sie nicht alle weiterlaufen können. Im Allgemeinen hat das Gerät zu diesem Zeitpunkt einen Speicherauslagerungszustand erreicht, so dass das Beenden einiger Vordergrundprozesse erforderlich ist, um die Benutzeroberfläche reaktionsfähig zu halten.

  2. Sichtbarer Prozess

    Ein Prozess, der keine Vordergrundkomponenten hat, aber dennoch beeinflussen kann, was der Benutzer auf dem Bildschirm sieht. Ein Prozess gilt als sichtbar, wenn eine der folgenden Bedingungen zutrifft:

    • Er beherbergt ein Activity, das sich nicht im Vordergrund befindet, aber dennoch für den Benutzer sichtbar ist (seine onPause() Methode wurde aufgerufen). Dies kann zum Beispiel der Fall sein, wenn die Vordergrundaktivität einen Dialog gestartet hat, hinter dem die vorherige Aktivität zu sehen ist.
    • Es beherbergt einen Service, der an eine sichtbare (oder Vordergrund-)Aktivität gebunden ist.

    Ein sichtbarer Prozess wird als extrem wichtig angesehen und wird nicht beendet, es sei denn, dies ist erforderlich, um alle Vordergrundprozesse am Laufen zu halten.

  3. Dienstprozess

    Ein Prozess, der einen Dienst ausführt, der mit der Methode startService() gestartet wurde und nicht in eine der beiden höheren Kategorien fällt. Obwohl Service-Prozesse nicht direkt mit etwas verbunden sind, das der Benutzer sieht, tun sie im Allgemeinen Dinge, die den Benutzer interessieren (wie das Abspielen von Musik im Hintergrund oder das Herunterladen von Daten im Netzwerk), so dass das System sie weiterlaufen lässt, es sei denn, es ist nicht genug Speicher vorhanden, um sie zusammen mit allen im Vordergrund befindlichen und sichtbaren Prozessen zu behalten.

  4. Hintergrundprozess

    Ein Prozess, der eine Aktivität ausführt, die derzeit für den Benutzer nicht sichtbar ist (die Methode der AktivitätonStop() wurde aufgerufen). Diese Prozesse haben keine direkte Auswirkung auf das Benutzererlebnis und das System kann sie jederzeit beenden, um Speicher für einen vorhergehenden, sichtbaren oder Dienstprozess zu gewinnen. Normalerweise laufen viele Hintergrundprozesse, so dass sie in einer LRU-Liste (least recently used) geführt werden, um sicherzustellen, dass der Prozess mit der Aktivität, die der Benutzer zuletzt gesehen hat, als letzter beendet wird. Wenn eine Aktivität ihre Lebenszyklusmethoden korrekt implementiert und ihren aktuellen Zustand speichert, hat das Beenden ihres Prozesses keine sichtbaren Auswirkungen auf die Benutzererfahrung, denn wenn der Benutzer zurück zur Aktivität navigiert, stellt die Aktivität ihren gesamten sichtbaren Zustand wieder her. Informationen zum Speichern und Wiederherstellen des Zustands finden Sie im Activitiesdocument.

  5. Leerer Prozess

    Ein Prozess, der keine aktiven Anwendungskomponenten enthält. Der einzige Grund, diese Art von Prozessen am Leben zu erhalten, ist die Zwischenspeicherung, um die Startzeit zu verkürzen, wenn das nächste Mal eine Komponente in diesem Prozess ausgeführt werden muss. Das System tötet diese Prozesse oft, um die gesamten Systemressourcen zwischen den Prozess-Caches und den zugrunde liegenden Kernel-Caches auszugleichen.

Android stuft einen Prozess auf der höchstmöglichen Stufe ein, basierend auf der Wichtigkeit der derzeit im Prozess aktiven Komponenten. Wenn ein Prozess beispielsweise einen Dienst und eine sichtbare Aktivität hostet, wird der Prozess als sichtbarer Prozess und nicht als Dienstprozess eingestuft.

Darüber hinaus kann der Rang eines Prozesses erhöht werden, weil andere Prozesse von ihm abhängig sind – ein Prozess, der einen anderen Prozess bedient, kann niemals niedriger eingestuft werden als der Prozess, den er bedient. Wenn zum Beispiel ein Inhaltsanbieter in Prozess A einen Client in Prozess B bedient oder wenn ein Dienst in Prozess A an eine Komponente in Prozess B gebunden ist, wird Prozess A immer als mindestens so wichtig wie Prozess B angesehen.

Da ein Prozess, der einen Dienst ausführt, höher eingestuft wird als ein Prozess mit Hintergrundaktivitäten, könnte eine Aktivität, die eine lang laufende Operation initiiert, gut daran tun, einen Dienst für diese Operation zu starten, anstatt einfach einen Worker-Thread zu erstellen – insbesondere, wenn die Operation die Aktivität wahrscheinlich überdauern wird.Zum Beispiel sollte eine Aktivität, die ein Bild auf eine Website hochlädt, einen Dienst starten, der den Upload durchführt, so dass der Upload im Hintergrund fortgesetzt werden kann, auch wenn der Benutzer die Aktivität verlässt.Die Verwendung eines Dienstes garantiert, dass die Operation mindestens die Priorität „Dienstprozess“ hat, unabhängig davon, was mit der Aktivität passiert. Dies ist derselbe Grund, warum Broadcast-Empfänger Dienste einsetzen sollten, anstatt zeitaufwendige Operationen einfach in einen Thread zu packen.

Threads

Wenn eine Anwendung gestartet wird, erstellt das System einen Ausführungs-Thread für die Anwendung, genannt „main“. Dieser Thread ist sehr wichtig, da er für die Weiterleitung von Ereignissen an die entsprechenden Widgets der Benutzeroberfläche zuständig ist, einschließlich Zeichenereignissen. Es ist auch der Thread, in dem Ihre Anwendung mit Komponenten aus dem Android UI-Toolkit interagiert (Komponenten aus den Paketen android.widget und android.view). Daher wird der Hauptthread manchmal auch als UI-Thread bezeichnet.

Das System erstellt nicht für jede Instanz einer Komponente einen eigenen Thread. Alle Komponenten, die im selben Prozess laufen, werden im UI-Thread instanziiert, und Systemaufrufe für jede Komponente werden von diesem Thread aus ausgeführt. Folglich werden Methoden, die auf Systemrückrufe reagieren (z. B. onKeyDown() um Benutzeraktionen oder eine Lebenszyklusrückrufmethode zu melden), immer im UI-Thread des Prozesses ausgeführt.

Wenn der Benutzer beispielsweise eine Schaltfläche auf dem Bildschirm berührt, sendet der UI-Thread Ihrer Anwendung das Berührungsereignis an das Widget, das seinerseits seinen gedrückten Zustand festlegt und eine Ungültigkeitsanforderung an die Ereigniswarteschlange sendet. Der UI-Thread nimmt die Anforderung aus der Warteschlange und benachrichtigt das Widget, dass es sich neu zeichnen soll.

Wenn Ihre Anwendung intensive Arbeit als Reaktion auf Benutzerinteraktionen ausführt, kann dieses Single-Thread-Modell zu schlechter Leistung führen, wenn Sie Ihre Anwendung nicht richtig implementieren. Wenn nämlich alles im UI-Thread passiert, blockiert die Durchführung langer Operationen wie Netzwerkzugriffe oder Datenbankabfragen die gesamte Benutzeroberfläche. Wenn der Thread blockiert ist, können keine Ereignisse versendet werden, auch keine Zeichenereignisse. Aus der Sicht des Benutzers scheint die Anwendung zu hängen. Schlimmer noch, wenn der UI-Thread länger als ein paar Sekunden blockiert ist (derzeit etwa 5 Sekunden), wird dem Benutzer das berüchtigte Dialogfeld „application notresponding“ (ANR) angezeigt. Der Benutzer könnte dann beschließen, Ihre Anwendung zu beenden und zu deinstallieren, wenn er unzufrieden ist.

Außerdem ist das Andoid UI Toolkit nicht thread-sicher. Daher dürfen Sie Ihre Benutzeroberfläche nicht von einem Worker-Thread aus manipulieren – Sie müssen alle Manipulationen an Ihrer Benutzeroberfläche vom UI-Thread aus vornehmen. Daher gibt es einfach zwei Regeln für das Single-Thread-Modell von Android:

  1. Den UI-Thread nicht blockieren
  2. Nicht von außerhalb des UI-Threads auf das Android UI-Toolkit zugreifen

Worker-Threads

Aufgrund des oben beschriebenen Single-Thread-Modells ist es für die Reaktionsfähigkeit der UI Ihrer Anwendung entscheidend, dass Sie den UI-Thread nicht blockieren. Wenn Sie Operationen durchführen müssen, die nicht sofort erfolgen, sollten Sie sicherstellen, dass sie in separaten Threads („Hintergrund-“ oder „Worker“-Threads) ausgeführt werden.

Nachfolgend finden Sie beispielsweise Code für einen Click-Listener, der ein Bild von einem separaten Thread herunterlädt und es in einem ImageView anzeigt:

public void onClick(View v) { new Thread(new Runnable() { public void run() { Bitmap b = loadImageFromNetwork("http://example.com/image.png"); mImageView.setImageBitmap(b); } }).start();}

Auf den ersten Blick scheint dies gut zu funktionieren, da ein neuer Thread erstellt wird, um die Netzwerkoperation zu verarbeiten. Es verstößt jedoch gegen die zweite Regel des Single-Thread-Modells: Greifen Sie nicht von außerhalb des UI-Threads auf das Android UI-Toolkit zu – in diesem Beispiel wird ImageView vom Worker-Thread statt vom UI-Thread aus geändert. Dies kann zu undefiniertem und unerwartetem Verhalten führen, was schwierig und zeitaufwendig zu verfolgen sein kann.

Um dieses Problem zu beheben, bietet Android mehrere Möglichkeiten, auf den UI-Thread von anderen Threads aus zuzugreifen. Hier ist eine Liste von Methoden, die helfen können:

  • Activity.runOnUiThread(Runnable)
  • View.post(Runnable)
  • View.postDelayed(Runnable,long)

Zum Beispiel können Sie den obigen Code mit der Methode View.post(Runnable) beheben:

public void onClick(View v) { new Thread(new Runnable() { public void run() { final Bitmap bitmap = loadImageFromNetwork("http://example.com/image.png"); mImageView.post(new Runnable() { public void run() { mImageView.setImageBitmap(bitmap); } }); } }).start();}

Diese Implementierung ist nun thread-sicher: Die Netzwerkoperation wird von einem separaten Thread ausgeführt, während ImageView vom UI-Thread aus bearbeitet wird.

Wenn jedoch die Komplexität des Vorgangs zunimmt, kann diese Art von Code kompliziert und schwer zu warten werden. Um komplexere Interaktionen mit einem Worker-Thread zu handhaben, könnten Sie in Erwägung ziehen, einen Handler in Ihrem Worker-Thread zu verwenden, um die vom UI-Thread gelieferten Nachrichten zu verarbeiten. Die vielleicht beste Lösung ist jedoch, die Klasse AsyncTask zu erweitern, die die Ausführung von Worker-Thread-Aufgaben vereinfacht, die mit der Benutzeroberfläche interagieren müssen.

Die Verwendung von AsyncTask

AsyncTask ermöglicht es Ihnen, asynchrone Arbeiten an Ihrer Benutzeroberfläche durchzuführen. Es führt die blockierenden Operationen in einem Worker-Thread aus und veröffentlicht dann die Ergebnisse auf dem UI-Thread, ohne dass Sie selbst Threads und/oder Handler handhaben müssen.

Um es zu verwenden, müssen Sie die Unterklasse AsyncTask und die Callback-Methode doInBackground() implementieren, die in einem Pool von Hintergrund-Threads läuft. Um Ihre Benutzeroberfläche zu aktualisieren, sollten Sie onPostExecute() implementieren, die das Ergebnis von doInBackground() liefert und im UI-Thread ausgeführt wird, sodass Sie Ihre Benutzeroberfläche sicher aktualisieren können. Sie können die Aufgabe dann durch den Aufruf von execute()vom UI-Thread aus ausführen.

Sie können zum Beispiel das vorherige Beispiel mit AsyncTask auf diese Weise implementieren:

public void onClick(View v) { new DownloadImageTask().execute("http://example.com/image.png");}private class DownloadImageTask extends AsyncTask<String, Void, Bitmap> { /** The system calls this to perform work in a worker thread and * delivers it the parameters given to AsyncTask.execute() */ protected Bitmap doInBackground(String... urls) { return loadImageFromNetwork(urls); } /** The system calls this to perform work in the UI thread and delivers * the result from doInBackground() */ protected void onPostExecute(Bitmap result) { mImageView.setImageBitmap(result); }}

Jetzt ist die UI sicher und der Code ist einfacher, weil er die Arbeit in den Teil, der auf einem Worker-Thread ausgeführt werden sollte, und den Teil, der auf dem UI-Thread ausgeführt werden sollte, trennt.

Für ein vollständiges Verständnis der Verwendung dieser Klasse sollten Sie die AsyncTaskReferenz lesen, aber hier ist ein kurzer Überblick darüber, wie sie funktioniert:

  • Sie können den Typ der Parameter, die Fortschrittswerte und den Endwert der Aufgabe mithilfe von Generics angeben
  • Die Methode doInBackground() wird automatisch auf einem Worker-Thread ausgeführt
  • onPreExecute(), onPostExecute(), und onProgressUpdate() werden alle auf dem UI-Thread aufgerufen
  • Der von doInBackground() zurückgegebene Wert wird an onPostExecute()
  • Sie können publishProgress() jederzeit in doInBackground() aufrufen, um onProgressUpdate() auf dem UI-Thread auszuführen
  • Sie können die Aufgabe jederzeit von jedem Thread aus abbrechen

Achtung: Ein weiteres Problem, auf das Sie bei der Verwendung eines Workerthreads stoßen können, sind unerwartete Neustarts Ihrer Aktivität aufgrund einer Änderung der Laufzeitkonfiguration (z. B. wenn der Benutzer die Bildschirmausrichtung ändert), die Ihren Workerthread zerstören können. Wie Sie Ihre Aufgabe während eines dieser Neustarts aufrechterhalten können und wie Sie die Aufgabe ordnungsgemäß abbrechen, wenn die Aktivität zerstört wird, sehen Sie im Quellcode der Beispielanwendung Regale.

Thread-sichere Methoden

In einigen Situationen können die von Ihnen implementierten Methoden von mehr als einem Thread aufgerufen werden und müssen daher thread-sicher geschrieben werden.

Dies gilt vor allem für Methoden, die aus der Ferne aufgerufen werden können, wie z.B. Methoden in einem gebundenen Dienst. Wenn ein Aufruf einer Methode, die in einem IBinder implementiert ist, aus demselben Prozess stammt, in dem der IBinder läuft, wird die Methode im Thread des Aufrufers ausgeführt. Wenn der Aufruf jedoch aus einem anderen Prozess stammt, wird die Methode in einem Thread ausgeführt, der aus einem Pool von Threads ausgewählt wird, den das System im selben Prozess wie den IBinder unterhält (sie wird nicht im UI-Thread des Prozesses ausgeführt). Während zum Beispiel die Methode onBind() eines Dienstes vom UI-Thread des Prozesses des Dienstes aufgerufen würde, würden Methoden, die in dem Objekt implementiert sind, das onBind() zurückgibt (zum Beispiel eine Unterklasse, die RPC-Methoden implementiert), von Threads im Pool aufgerufen werden. Da ein Dienst mehr als einen Client haben kann, kann mehr als ein Pool-Thread dieselbe IBinder-Methode gleichzeitig aufrufen. IBinder-Methoden müssen daher thread-sicher implementiert werden.

In ähnlicher Weise kann ein Inhaltsanbieter Datenanforderungen erhalten, die von anderen Prozessen stammen.Obwohl die ContentResolver– und ContentProvider-Klassen die Details der Verwaltung der Interprozesskommunikation verbergen, werden die ContentProvider-Methoden, die auf diese Anforderungen reagieren – die Methoden query(), insert(), delete(), update() und getType() – von einem Pool von Threads im Prozess des Inhaltsanbieters aufgerufen, nicht vom UI-Thread des Prozesses. Da diese Methoden von einer beliebigen Anzahl von Threads gleichzeitig aufgerufen werden können, müssen auch sie thread-sicher implementiert werden.

Interprozesskommunikation

Android bietet einen Mechanismus für Interprozesskommunikation (IPC) unter Verwendung von Remote Procedure Calls (RPCs), bei dem eine Methode von einer Aktivität oder einer anderen Anwendungskomponente aufgerufen, aber remote (in einem anderen Prozess) ausgeführt wird, wobei das Ergebnis an den Aufrufer zurückgegeben wird. Dazu werden ein Methodenaufruf und seine Daten auf eine für das Betriebssystem verständliche Ebene zerlegt, vom lokalen Prozess und Adressraum an den entfernten Prozess und Adressraum übertragen und dort wieder zusammengesetzt und erneut ausgeführt. Die Rückgabewerte werden dann in umgekehrter Richtung übertragen. Android stellt den gesamten Code zur Durchführung dieser IPC-Transaktionen bereit, sodass Sie sich auf die Definition und Implementierung der RPC-Programmierschnittstelle konzentrieren können.

Um IPC durchzuführen, muss Ihre Anwendung mit bindService() an einen Dienst gebunden werden. Weitere Informationen finden Sie im Services-Entwicklerhandbuch.

Schreibe einen Kommentar