Wie kann die Bildverarbeitung zur Laufzeit optimiert werden, wenn 6 Bilder erstellt werden müssen, von denen jedes aus 15 bis 16 PNGs besteht, die nacheinander überlagert werden, ohne dass unterwegs eine OutOfMemoryException empfangen wird?

Bei der Entwicklung meiner Haustieranwendung bin ich auf das Problem der Bildverarbeitung gestoßen. Ich konnte Google keine guten Googlecases zur Verfügung stellen, daher musste ich auf meinem Rechen laufen und selbst ein Fahrrad erfinden.
Auch während der Entwicklung gab es eine Migration von Java nach Kotlin, so dass der Code irgendwann übersetzt wird.
Herausforderung
Bewerbung für das Training im Fitnessstudio. Es ist notwendig, eine Karte der Muskelarbeit zu erstellen, die auf den Ergebnissen des Trainings in der Anwendungslaufzeit basiert.
Zwei Geschlechter: M und G. Betrachten Sie Option M, denn für M ist alles gleich.
Es sollten 6 Bilder gleichzeitig erstellt werden: 3 Perioden (eine Trainingseinheit pro Woche und Monat) x 2 Ansichten (vorne, hinten)

Jedes dieser Bilder besteht aus 15 Bildern von Muskelgruppen für eine Vorderansicht und 14 für eine Rückansicht. Plus 1 Bild des Fundaments (Kopf, Hände und Füße). Insgesamt müssen Sie zum Sammeln der Vorderansicht 16 Bilder von hinten überlagern - 15.
Nur 23 Muskelgruppen für beide Seiten (für diejenigen mit 15 + 14! = 23, eine kleine Erklärung - einige Muskeln sind auf beiden Seiten „sichtbar“).
Algorithmus in erster Näherung:
- Basierend auf den Daten aus abgeschlossenen Workouts wird HashMap <String, Float> erstellt, String ist der Name der Muskelgruppe, Float ist der Belastungsgrad von 0 bis 10.
- Jeder der 23 Muskeln ist in der Farbe von 0 (nicht beteiligt) bis 10 (max. Belastung) neu gestrichen.
- Überlagern Sie neu gestrichene Muskelbilder in zwei Bildern (vorne, hinten).
- Wir speichern alle 6 Bilder.

Zum Speichern von 31 (16 + 15) Bildern mit einer Größe von 1500 x 1500 Pixel im 24-Bit-Modus sind 31 x 1500 x 1500 x 24 Bit = 199 MB RAM erforderlich. Wenn Sie ~ 30-40 MB überschreiten, erhalten Sie eine OutOfMemoryException. Dementsprechend können Sie nicht alle Bilder von Ressourcen gleichzeitig laden, da Sie Ressourcen freigeben müssen, um den Empfang nicht zu erhalten. Dies bedeutet, dass Sie Bilder nacheinander überlagern müssen. Der Algorithmus wandelt sich in Folgendes um:
Basierend auf den Daten aus abgeschlossenen Workouts werden HashMap <String, Float>, String - Muskel, Float - Last von 0 bis 10 erstellt.
Der Zyklus für jedes der 6 Bilder:
- Ich habe die Ressource BitmapFactory.decodeResource () erhalten.
- Jeder der 23 Muskeln ist in der Farbe von 0 (nicht beteiligt) bis 10 (max. Belastung) neu gestrichen.
- Überlagern Sie neu gestrichene Muskelbilder auf einer Leinwand.
- Bitmap.recycle () hat eine Ressource freigegeben.
Wir führen die Aufgabe in einem separaten Thread mit AsyncTask aus. In jeder Aufgabe werden nacheinander zwei Bilder erstellt: eine Vorderansicht und eine Rückansicht.
private class BitmapMusclesTask extends AsyncTask<Void, Void, DoubleMusclesBitmaps> { private final WeakReference<HashMap<String, Float>> musclesMap; BitmapMusclesTask(HashMap<String, Float> musclesMap) { this.musclesMap = new WeakReference<>(musclesMap); } @Override protected DoubleMusclesBitmaps doInBackground(Void... voids) { DoubleMusclesBitmaps bitmaps = new DoubleMusclesBitmaps(); bitmaps.bitmapBack = createBitmapMuscles(musclesMap.get(), false); bitmaps.bitmapFront = createBitmapMuscles(musclesMap.get(), true); return bitmaps; } @Override protected void onPostExecute(DoubleMusclesBitmaps bitmaps) { super.onPostExecute(bitmaps); Uri uriBack = saveBitmap(bitmaps.bitmapBack); Uri uriFront = saveBitmap(bitmaps.bitmapFront); bitmaps.bitmapBack.recycle(); bitmaps.bitmapFront.recycle(); if (listener != null) listener.onUpdate(uriFront, uriBack); } } public class DoubleMusclesBitmaps { public Bitmap bitmapFront; public Bitmap bitmapBack; }
Die Hilfsklasse DoubleMusclesBitmaps wird nur benötigt, um zwei Bitmap-Variablen zurückzugeben: Vorderansicht und Rückansicht. Mit Blick auf die Zukunft wird die Java-Klasse DoubleMusclesBitmaps in Kotlin durch Pair <Bitmap, Bitmap> ersetzt.
Zeichnen
Farben color.xml in Wertressourcen.
<?xml version="1.0" encoding="utf-8"?> <resources> <color name="muscles_color0">#BBBBBB</color> <color name="muscles_color1">#ffb5cf</color> <color name="muscles_color2">#fda9c6</color> <color name="muscles_color3">#fa9cbe</color> <color name="muscles_color4">#f890b5</color> <color name="muscles_color5">#f583ac</color> <color name="muscles_color6">#f377a4</color> <color name="muscles_color7">#f06a9b</color> <color name="muscles_color8">#ee5e92</color> <color name="muscles_color9">#eb518a</color> <color name="muscles_color10">#e94581</color> </resources>
Erstellen Sie eine Ansicht
public Bitmap createBitmapMuscles(HashMap<String, Float> musclesMap, Boolean isFront) { Bitmap musclesBitmap = Bitmap.createBitmap(1500, 1500, Bitmap.Config.ARGB_8888); Canvas resultCanvas = new Canvas(musclesBitmap); for (HashMap.Entry entry : musclesMap.entrySet()) { int color = Math.round((float) entry.getValue());
Einzelne Muskelauflage
private void drawMuscleElement(Canvas resultCanvas, String drawableName, @ColorInt int color) { PorterDuff.Mode mode = PorterDuff.Mode.SRC_IN; Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); Bitmap bitmapDst = BitmapFactory.decodeResource(context.getResources(), context.getResources().getIdentifier(drawableName, "drawable", context.getPackageName())); bitmapDst = Bitmap.createScaledBitmap(bitmapDst, 1500, 1500, true); paint.setColorFilter(new PorterDuffColorFilter(color, mode)); resultCanvas.drawBitmap(bitmapDst, 0, 0, paint); bitmapDst.recycle();
Wir beginnen mit der Erzeugung von 3 Bildpaaren.
private BitmapMusclesTask taskLast; private BitmapMusclesTask taskWeek; private BitmapMusclesTask taskMonth; private void startImageGenerating(){ taskLast = new BitmapMusclesTask(mapLast); taskLast.execute(); taskWeek = new BitmapMusclesTask(mapWeek); taskWeek.execute(); taskMonth = new BitmapMusclesTask(mapMonth); taskMonth.execute(); }
Wir starten startImageGenerating ():
> start 1549350950177 > finish 1549350959490 diff=9313 ms
Es ist zu beachten, dass das Lesen von Ressourcen viel Zeit in Anspruch nimmt. Für jedes Bildpaar werden 29 PNG-Dateien aus Ressourcen dekodiert. In meinem Fall verbringt die Funktion BitmapFactory.decodeResource () von den Gesamtkosten für die Erstellung von Bildern ~ 75% der Zeit: ~ 6960 ms.
Nachteile:
- Ich bekomme von Zeit zu Zeit eine OutOfMemoryException.
- Die Verarbeitung dauert mehr als 9 Sekunden und befindet sich auf dem Emulator (!). Im "durchschnittlichen" Telefon (alte Mine) wurden 20 Sekunden erreicht.
- AsyncTask mit allen resultierenden [Speicher-] Lecks.
Vorteile:
Mit Wahrscheinlichkeit (1-OutOfMemoryException) werden Bilder gezeichnet.
AsyncTask in IntentService
Um AsyncTask zu verlassen, wurde beschlossen, zu IntentServie zu wechseln, in dem die Aufgabe zum Erstellen von Bildern ausgeführt wurde. Nach Abschluss des Dienstes erhalten wir, wenn BroadcastReceiver ausgeführt wird, den Uri aller sechs generierten Bilder. Andernfalls wurden die Bilder einfach gespeichert, sodass der Benutzer beim nächsten Öffnen der Anwendung nicht auf den Erstellungsprozess warten muss. Zur gleichen Zeit änderte sich die Betriebszeit nicht, aber mit einem Minus - Speicherlecks herausgefunden, gab es zwei weitere Minuspunkte.
Es ist natürlich unmöglich, Benutzer dazu zu zwingen, die Erstellung von Bildern für einen solchen Zeitraum zu erwarten. Muss optimiert werden.
Möglichkeiten zur Optimierung skizzieren:
- Bildverarbeitung.
- LruCache hinzufügen.
Bildverarbeitung
Alle Quell-PNG-Ressourcen sind 1500 x 1500 Pixel groß. Reduzieren Sie sie auf 1080x1080.
Wie Sie auf dem zweiten Foto sehen können, sind alle Quellcodes quadratisch, die Muskeln sind vorhanden und die wirklich nützlichen Pixel nehmen einen kleinen Bereich ein. Die Tatsache, dass alle Muskelgruppen bereits vorhanden sind, ist für den Programmierer praktisch, für die Leistung jedoch nicht rational. Wir beschneiden (schneiden) den Überschuss in allen Quellcodes und zeichnen die Position (x, y) jeder Muskelgruppe auf, um sie anschließend an der richtigen Stelle anzuwenden.
Beim ersten Ansatz wurden alle 29 Bilder von Muskelgruppen neu gestrichen und auf die Basis gelegt. Die Basis umfasste nur den Kopf, die Hände und Teile der Beine. Wir ändern die Basis: Jetzt umfasst sie neben Kopf, Armen und Beinen alle anderen Muskelgruppen. Wir malen alles in grau color_muscle0. Dies ermöglicht es, die nicht beteiligten Muskelgruppen nicht neu zu streichen und nicht aufzuerlegen.
Jetzt sehen alle Quellen so aus:

Lrucache
Nach der zusätzlichen Verarbeitung der Originalbilder nahmen einige etwas Speicherplatz in Anspruch, was zu dem Gedanken führte, LruCache wiederzuverwenden (nicht nach jeder Überlagerung mit der .recycle () -Methode freizugeben). Wir erstellen eine Klasse zum Speichern von Quellbildern, die gleichzeitig die Funktion des Lesens aus Ressourcen übernimmt:
class LruCacheBitmap(val context: Context) { private val lruCache: LruCache<String, Bitmap> init { val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt() val cacheSize = maxMemory / 4 lruCache = object : LruCache<String, Bitmap>(cacheSize) { override fun sizeOf(key: String, bitmap: Bitmap): Int { return bitmap.byteCount } } } fun getBitmap(drawableName: String): Bitmap? { return if (lruCache.get(drawableName) != null) lruCache.get(drawableName) else decodeMuscleFile(drawableName) } fun clearAll() { lruCache.evictAll() } private fun decodeMuscleFile(drawableName: String): Bitmap? { val bitmap = BitmapFactory.decodeResource(context.resources, context.resources.getIdentifier(drawableName, "drawable", context.packageName)) if (bitmap != null) { lruCache.put(drawableName, bitmap) } return bitmap } }
Bilder werden vorbereitet, die Ressourcendecodierung wird optimiert.
Wir werden den reibungslosen Übergang von Java zu Kotlin nicht diskutieren, aber es ist passiert.
Coroutinen
Der Code, der IntentService verwendet, funktioniert, aber die Lesbarkeit des Codes mit Rückrufen kann nicht als angenehm bezeichnet werden.
Fügen Sie den Wunsch hinzu, Kotlins Coroutinen in Arbeit zu betrachten. Wir fügen das Verständnis hinzu, dass es nach ein paar Monaten angenehmer sein wird, Ihren synchronen Code zu lesen, als nach dem Ort zu suchen, an dem die Uri-Dateien der generierten Bilder zurückgegeben werden können.
Die Beschleunigung der Bildverarbeitung veranlasste die Idee, die Funktion an mehreren neuen Stellen in der Anwendung zu verwenden, insbesondere bei der Beschreibung von Übungen, und zwar nicht nur nach dem Training.
private val errorHandler = CoroutineExceptionHandler { _, e -> e.printStackTrace()} private val job = SupervisorJob() private val scope = CoroutineScope(Dispatchers.Main + job + errorHandler) private var uries: HashMap<String, Uri?> = HashMap() fun startImageGenerating() = scope.launch { ... val imgMuscle = ImgMuscle() uries = withContext(Dispatchers.IO) { imgMuscle.createMuscleImages() } ... }
Das Standardpaket aus errorHandler, Job und Scope ist Scout Coroutine mit einem Fehlerhandler, wenn Coroutine kaputt geht.
uries - HashMap, die 6 Bilder für die spätere Ausgabe an die Benutzeroberfläche in sich speichert:
uries ["last_back"] = Uri?
uries ["last_front"] = Uri?
uries ["week_back"] = Uri?
uries ["week_front"] = Uri?
uries ["month_back"] = Uri?
uries ["month_front"] = Uri?
class ImgMuscle { val lruBitmap: LruCacheBitmap suspend fun createMuscleImages(): HashMap<String, Uri?> { return suspendCoroutine { continuation -> val resultUries = HashMap<String, Uri?>() ...
Wir messen die Bearbeitungszeit.
>start 1549400719844 >finish 1549400720440 diff=596 ms
Ab 9313 ms verringerte sich die Verarbeitung auf 596 ms.
Wenn Sie Ideen für zusätzliche Optimierungen haben, können Sie diese gerne kommentieren.