Bessere Leistung durch Threading

Durch den sinnvollen Einsatz von Threads unter Android kannst du die Leistung. Auf dieser Seite werden mehrere Aspekte der Arbeit mit Threads erläutert: mit der Benutzeroberfläche oder dem Hauptthread arbeiten; zwischen App-Lebenszyklus und Thread-Priorität; Methoden, die die Plattform bietet, um Threads zu verwalten und Komplexität. In jedem dieser Bereiche werden auf dieser Seite potenzielle Fallstricke beschrieben und Strategien, um sie zu vermeiden.

Hauptthread

Wenn der Nutzer Ihre App startet, erstellt Android ein neues Linux- zusammen mit einem Ausführungs-Thread. In diesem Hauptthread auch UI-Thread genannt, ist für alles, was passiert, zu präsentieren. Wenn Sie die Funktionsweise verstehen, können Sie Ihre App für die Nutzung der Hauptthread für die bestmögliche Leistung.

Interna

Der Hauptthread ist sehr einfach aufgebaut: Es gibt nur die Aufgabe, aus einer Thread-sicheren Arbeitswarteschlange bis zur Beendigung ihrer Anwendung zu trennen. Die Framework einige dieser Arbeitsblöcke von verschiedenen Stellen aus generiert. Diese z. B. Callbacks, die mit Lebenszyklusinformationen verknüpft sind, oder Ereignisse aus anderen Apps und Prozessen. Außerdem kann die App Blöcke explizit in die Warteschlange zu stellen, ohne das Framework zu verwenden.

Fast alle Codeblock, den Ihre App ausführt, ist mit einem Ereignis-Callback wie einer Eingabe, Layout-Inflation oder Zeichnen. Wenn etwas ein Ereignis auslöst, wird der Thread, in dem das Ereignis passiert, verschiebt das Ereignis aus sich selbst und in die Nachricht des Hauptthreads. in die Warteschlange stellen. Der Hauptthread kann dann das Ereignis verarbeiten.

Während eine Animation oder ein Bildschirmupdate ausgeführt wird, versucht das System, eine der für das Zeichnen des Bildschirms verantwortlich ist, alle 16 ms, um bei 60° flüssig zu rendern, Bilder pro Sekunde. Damit das System dieses Ziel erreichen kann, muss die Hierarchie der Benutzeroberfläche/Ansicht muss im Hauptthread aktualisiert werden. Wenn die Nachrichtenwarteschlange des Hauptthreads enthält Aufgaben, die entweder zu viele oder zu lang sind, sodass der Hauptthread nicht sollte das Update schnell genug abgeschlossen werden, sollte die App diese Arbeit auf einen Worker Diskussions-Thread. Wenn der Hauptthread die Ausführung von Arbeitsblöcken nicht innerhalb von 16 ms fertigstellen kann, kann es zu Verzögerungen, Verzögerungen oder einer mangelnden Reaktionsfähigkeit der UI bei der Eingabe kommen. Wenn der Hauptthread etwa fünf Sekunden lang blockiert wird, zeigt das System die Anwendung Dialogfeld „Keine Reaktion“ (ANR), über das der Nutzer die App direkt schließen kann.

Verschieben zahlreicher oder langer Aufgaben aus dem Hauptthread, damit sie nicht gestört werden mit flüssigem Rendering und schneller Reaktion auf Nutzereingaben. warum Sie Threading in Ihrer App verwenden sollten.

Threads und UI-Objektverweise

Android View-Objekte sind nicht threadsicher. Es wird erwartet, dass eine App UI-Objekte im Hauptthread löschen. Wenn Sie versuchen, oder auf ein UI-Objekt in einem anderen Thread als dem Hauptthread verweisen, können Ausnahmen, Fehler im Hintergrund, Abstürze und anderes undefiniertes Fehlverhalten sein.

Probleme mit Referenzen lassen sich in zwei Kategorien unterteilen: explizite Referenzen. und implizite Verweise.

Explizite Verweise

Viele Aufgaben in Nicht-Haupt-Threads haben letztendlich das Ziel, UI-Objekte zu aktualisieren. Wenn jedoch einer dieser Threads auf ein Objekt in der Ansichtshierarchie zugreift, Anwendungsinstabilität kann auftreten: Wenn ein Worker-Thread die Attribute eines und jeder andere Thread auf das Objekt verweist, sind die Ergebnisse nicht definiert.

Stellen Sie sich zum Beispiel eine App vor, die einen direkten Verweis auf ein UI-Objekt auf einem Worker-Thread. Das Objekt im Worker-Thread kann einen Verweis auf einen View; Aber bevor die Arbeit abgeschlossen ist, wird der View aus der Ansichtshierarchie entfernt. Wenn diese beiden Aktionen gleichzeitig stattfinden, Mit der Referenz wird das View-Objekt im Arbeitsspeicher gespeichert und es werden Attribute dafür festgelegt. Der Nutzer sieht jedoch und die App löscht das Objekt, sobald der Verweis darauf verschwunden ist.

In einem anderen Beispiel enthalten View-Objekte Verweise auf die Aktivität dem sie gehören. Wenn dass die Aktivität zerstört wird, aber es bleibt ein Arbeitsblock mit einem Gewinde, direkt oder indirekt darauf verweist, erfasst die automatische Speicherbereinigung keine bis dieser Arbeitsblock ausgeführt ist.

Dieses Szenario kann in Situationen, in denen Threads mit Threads verbunden sind, zu einem Problem führen. während ein bestimmtes Ereignis im Lebenszyklus der Aktivität auftritt, etwa eine Bildschirmdrehung. Das System kann erst dann eine automatische Speicherbereinigung ausführen, wenn abgeschlossene Arbeit. Daher können zwei Activity-Objekte in bis die automatische Speicherbereinigung stattfinden kann.

Bei solchen Szenarien empfehlen wir, dass Ihre App keine expliziten Verweise auf UI-Objekte in Arbeitsaufgaben mit Threads Die Vermeidung solcher Verweise hilft Ihnen, diese Arten von Speicherlecks vorbeugen, und gleichzeitig Threading-Konflikte vermeiden.

In allen Fällen sollte Ihre Anwendung UI-Objekte nur im Hauptthread aktualisieren. Dieses sollten Sie eine Verhandlungsrichtlinie erstellen, die es mehreren Threads ermöglicht, die Arbeit zurück an den Hauptthread senden, der die höchste Aktivität oder mit der Aktualisierung des eigentlichen UI-Objekts.

Implizite Verweise

Ein häufig auftretender Code-Design-Fehler bei Thread-Objekten ist im Snippet des Code unten:

Kotlin

class MainActivity : Activity() {
    // ...
    inner class MyAsyncTask : AsyncTask<Unit, Unit, String>() {
        override fun doInBackground(vararg params: Unit): String {...}
        override fun onPostExecute(result: String) {...}
    }
}

Java

public class MainActivity extends Activity {
  // ...
  public class MyAsyncTask extends AsyncTask<Void, Void, String>   {
    @Override protected String doInBackground(Void... params) {...}
    @Override protected void onPostExecute(String result) {...}
  }
}

Der Nachteil in diesem Snippet ist, dass der Code das Threading-Objekt deklariert. MyAsyncTask als nicht statische innere Klasse einer Aktivität (oder eine innere Klasse) in Kotlin). Diese Deklaration erstellt einen impliziten Verweis auf das einschließende Activity Instanz. Das Objekt enthält daraufhin so lange einen Verweis auf die Aktivität, bis das Objekt mit Threads zu arbeiten, was zu einer Verzögerung beim Löschen der referenzierten Aktivität führt. Diese Verzögerung übt wiederum mehr Druck auf den Speicher aus.

Eine direkte Lösung für dieses Problem wäre, die überlastete Klasse zu definieren. Instanzen entweder als statische Klassen oder in ihren eigenen Dateien, sodass die implizite Referenz.

Eine weitere Lösung wäre, Hintergrundaufgaben immer in der entsprechenden Activity-Lebenszyklus-Callback, z. B. onDestroy. Dieser Ansatz kann mühsam und fehleranfällig sind. Grundsätzlich sollten Sie keine komplexe Nicht-UI-Logik direkt in Aktivitäten. Außerdem wurde AsyncTask eingestellt und ist nun wird nicht zur Verwendung in neuem Code empfohlen. Siehe Threading unter Android .

Threads und App-Aktivitätslebenszyklen

Der Anwendungslebenszyklus kann sich darauf auswirken, wie Threading in Ihrer Anwendung funktioniert. Möglicherweise müssen Sie entscheiden, ob ein Thread nach einer Aktivitäten vernichtet. Sie sollten auch die Beziehung zwischen der Thread-Priorisierung und ob eine Aktivität im Vordergrund oder Hintergrund.

Persistente Threads

Threads bleiben über die gesamte Lebensdauer der Aktivitäten hinaus bestehen, die sie hervorgebracht haben. Fäden ohne Unterbrechung weiter ausgeführt werden, -Aktivitäten, auch wenn diese zusammen mit dem Bewerbungsprozess beendet werden, sobald keine aktivere Anwendungskomponenten. In einigen Fällen ist diese Persistenz wünschenswert.

Stellen Sie sich einen Fall vor, bei dem eine Aktivität eine Reihe von Arbeitsblöcken mit Threads erzeugt. wird dann gelöscht, bevor ein Worker-Thread die Blöcke ausführen kann. Was sollte der mit den aktiven Blocks zu tun?

Es gibt keinen Grund, wenn die Blockierungen eine Benutzeroberfläche aktualisieren, die nicht mehr existiert. damit die Arbeit fortgesetzt werden kann. Wenn es z. B. darum geht, Nutzerinformationen aus einer Datenbank löschen und dann Ansichten aktualisieren, wird der Thread nicht mehr benötigt.

Im Gegensatz dazu haben die Arbeitspakete möglicherweise einen Vorteil, der nicht ausschließlich mit dem UI. In diesem Fall sollten Sie den Thread dauerhaft speichern. Zum Beispiel können die Pakete warten darauf, ein Image herunterzuladen, es im Cache auf der Festplatte zu speichern und die zugehörigen View-Objekt. Obwohl das Objekt nicht mehr existiert, Das Speichern des Bildes im Cache kann trotzdem hilfreich sein, falls der Nutzer zur zerstörte Aktivitäten.

Das manuelle Verwalten von Lebenszyklusantworten für alle Threading-Objekte kann zu extrem komplex. Wenn Sie sie nicht richtig verwalten, kann Ihre App Speicherkonflikte und Leistungsprobleme. Wird kombiniert ViewModel mit LiveData ermöglicht Ihnen, um Daten zu laden und bei Änderungen ohne sich Gedanken über den Lebenszyklus machen zu müssen. ViewModel-Objekte sind eine Lösung für dieses Problem. ViewModels werden über alle Konfigurationsänderungen hinweg bietet eine einfache Möglichkeit, Ihre Ansichtsdaten zu speichern. Weitere Informationen zu ViewModels finden Sie in der ViewModel-Anleitung und weitere Informationen zu LiveData finden Sie im LiveData-Leitfaden. Wenn Sie auch mehr über die Anwendungsarchitektur, Leitfaden zur Anwendungsarchitektur.

Threadpriorität

Wie unter Prozesse beschrieben und dem Anwendungslebenszyklus, also der Priorität, die die Threads Ihrer App erhalten hängt teilweise davon ab, wo sich die App im App-Lebenszyklus befindet. Wenn Sie beim Erstellen und Threads in Ihrer Anwendung verwalten möchten, ist es wichtig, deren Priorität festzulegen, die richtigen Threads zur richtigen Zeit die richtigen Prioritäten erhalten. Ist der Wert zu hoch, kann Ihr Thread den UI-Thread und RenderThread unterbrechen, wodurch Ihre App Frames entfernen. Ist dieser Wert zu niedrig, können Sie die asynchronen Aufgaben (z. B. langsamer laden, als sie sein müssen.

Jedes Mal, wenn Sie einen Thread erstellen, sollten Sie Folgendes aufrufen: setThreadPriority() Der Thread des Systems Planer bevorzugt Threads mit hohen Prioritäten und gleicht diese aus. und Prioritäten setzen, um letztendlich die gesamte Arbeit zu erledigen. Im Allgemeinen sind Threads im Vordergrund der Gruppe erhalten etwa 95% der gesamten Ausführungszeit vom Gerät, einer Hintergrundgruppe bei etwa 5%.

Außerdem weist das System jedem Thread einen eigenen Prioritätswert zu. Dabei werden die Parameter Klasse Process.

Standardmäßig legt das System für die Priorität eines Threads dieselbe Priorität und Gruppe fest Mitgliedschaften als hervorbringender Thread. Ihre Anwendung kann jedoch explizit die Thread-Priorität mithilfe von setThreadPriority()

Das Process -Klasse reduziert die Komplexität bei der Zuweisung von Prioritätswerten, indem eine eine Reihe von Konstanten, mit denen Ihre Anwendung Thread-Prioritäten festlegen kann. Beispiel: THREAD_PRIORITY_DEFAULT stellt den Standardwert für einen Thread dar. Ihre App sollte die Priorität des Threads auf THREAD_PRIORITY_BACKGROUND für Threads, die weniger dringende Arbeit ausführen.

Deine App kann die THREAD_PRIORITY_LESS_FAVORABLE verwenden und THREAD_PRIORITY_MORE_FAVORABLE als Inkrementierer verwenden, um relative Prioritäten festzulegen. Eine Liste mit Thread-Prioritäten finden Sie in der THREAD_PRIORITY Konstanten in die Klasse Process.

Weitere Informationen zu finden Sie in der Referenzdokumentation zu Klassen Thread und Process.

Hilfsklassen für Threading

Entwicklern, die Kotlin als Hauptsprache verwenden, empfehlen wir die Verwendung von Coroutinen. Koroutinen bieten eine Reihe von Vorteilen, darunter das Schreiben von asynchronem Code ohne Callbacks sowie strukturierte Nebenläufigkeit für den Umfang, die Stornierung und die Fehlerbehandlung

Das Framework bietet auch dieselben Java-Klassen und -Primitive, Threading, z. B. Thread, Runnable , und Executors Klassen, sowie zusätzliche Optionen wie HandlerThread. Weitere Informationen finden Sie unter Threading unter Android.

Die HandlerThread-Klasse

Ein Handler-Thread ist im Grunde ein lang andauernder Thread, der Arbeit aus einer Warteschlange holt und .

Eine häufige Herausforderung beim Abrufen von Vorschau-Frames Camera-Objekt. Wenn Sie die Kameravorschau-Frames registrieren, erhalten Sie sie im onPreviewFrame() Callback, das im Ereignis-Thread aufgerufen wird, aus dem er aufgerufen wurde. Wenn dieses im Benutzeroberflächen-Thread aufgerufen wurden, der Aufgabe, dass Arrays die Rendering- und Ereignisverarbeitung beeinträchtigen würden.

Wenn Ihre App in diesem Beispiel den Befehl Camera.open() an einen des Handler-Threads, der zugehörigen onPreviewFrame() Rückruf landet im Handler-Thread statt im UI-Thread. Wenn Sie also eine lang andauernde an den Pixeln arbeiten, könnte dies eine bessere Lösung für Sie sein.

Wenn deine App einen Thread mit HandlerThread erstellt, solltest du Folgendes nicht tun: nicht vergessen, die <ph type="x-smartling-placeholder"></ph> je nach Art der Arbeit, die sie leisten. CPUs können nur und eine kleine Anzahl von Threads gleichzeitig zu bearbeiten. Das Festlegen der Priorität hilft, Das System weiß, wie diese Arbeit geplant werden kann, wenn alle anderen Threads die um Aufmerksamkeit kämpfen.

Die ThreadPoolExecutor-Klasse

Es gibt bestimmte Arten von Arbeiten, die auf sehr parallele Arbeiten reduziert werden können, für verteilte Aufgaben. Dazu gehört beispielsweise das Berechnen eines Filters für 8-x-8-Block eines 8-Megapixel-Bildes Bei der schieren Menge an Arbeitspaketen erstellt wird, ist HandlerThread nicht die zu verwendende Klasse.

ThreadPoolExecutor ist eine Hilfsklasse, die erstellt werden kann diesen Prozess zu vereinfachen. Diese Klasse verwaltet das Erstellen einer Gruppe von Threads, Sets Prioritäten zu setzen und die Verteilung der Arbeit zwischen diesen Threads zu verwalten. Wenn die Arbeitslast zu- oder abnimmt, startet die Klasse oder zerstört mehr Threads an die Arbeitsbelastung anzupassen.

Diese Klasse trägt auch dazu bei, dass Ihre App eine optimale Anzahl von Threads erzeugt. Wenn es erstellt eine ThreadPoolExecutor legt die App einen Mindest- und Höchstwert fest, Anzahl der Threads. Da die Arbeitslast ThreadPoolExecutor steigt an, übernimmt die Klasse die initialisierte minimale und maximale Thread-Anzahl und überlegen Sie, wie viel Arbeit noch erledigt werden muss. Basierend auf diesen berücksichtigt, entscheidet ThreadPoolExecutor, wie viele sollten immer aktiv sein.

Wie viele Threads sollten Sie erstellen?

Auf Softwareebene kann Ihr Code jedoch Hunderte von Threads führt dies zu Leistungsproblemen. Deine App nutzt eingeschränkte CPU-Leistung mit Hintergrunddiensten, dem Renderer, der Audio-Engine, Networking und vieles mehr. CPUs haben wirklich nur die die Fähigkeit, eine kleine Anzahl von Threads parallel zu verarbeiten; und alle darüber liegenden Elemente Prioritäts- und Zeitplanproblem einordnen. Daher ist es wichtig, nur eine so viele Threads wie Ihre Arbeitslast benötigt.

In der Praxis sind dafür verschiedene Variablen verantwortlich, einen Wert auswählen (z. B. 4, zu Beginn) und testen ihn mit Systrace ist wie jede andere eine solide Strategie. Durch Ausprobieren können Sie herausfinden, Mindestanzahl von Threads verwenden, die ohne Probleme verwendet werden können.

Ein weiterer Aspekt bei der Entscheidung, wie viele Threads Sie haben sollten, sind nicht kostenlos: Sie verbrauchen viel Speicherplatz. Jeder Thread kostet mindestens 64.000 Arbeitsspeicher. Dies summiert sich schnell bei den vielen auf einem Gerät installierten Apps, insbesondere in in denen die Aufrufstacks erheblich zunehmen.

Viele Systemprozesse und Bibliotheken von Drittanbietern starten oft ihre eigenen Threadpools. Wenn deine App einen vorhandenen Threadpool wiederverwenden kann, kann diese Wiederverwendung helfen durch weniger Speicher- und Verarbeitungsressourcen.