Comment j'ai accéléré le traitement d'image sur Android 15 fois

Comment optimiser le traitement des images lors de l'exécution lorsqu'il est nécessaire de créer 6 images, chacune composée de 15-16 PNG superposées séquentiellement, sans recevoir en cours de route une OutOfMemoryException?


image


Lors du développement de mon application pour animaux de compagnie, j'ai rencontré le problème du traitement d'image. Je ne pouvais pas fournir de bonnes Googlecases pour Google, j'ai donc dû marcher sur mon râteau et inventer un vélo par moi-même.
Également pendant le développement, il y a eu une migration de Java vers Kotlin, donc le code sera traduit à un moment donné.


Défi


Demande de formation dans le gymnase. Il est nécessaire de construire une carte du travail musculaire basée sur les résultats de l'entraînement au runtime de l'application.
Deux sexes: M et G. Envisagez l'option M, car pour M tout est pareil.
6 images doivent être construites simultanément: 3 périodes (une séance d'entraînement, par semaine, par mois) x 2 vues (avant, arrière)


image


Chacune de ces images consiste en 15 images de groupes musculaires pour une vue de face et 14 pour une vue arrière. Plus 1 image de la fondation (tête, mains et pieds). Au total, pour collecter la vue de face, vous devez superposer 16 images, de l'arrière - 15.


Seulement 23 groupes musculaires des deux côtés (pour ceux avec 15 + 14! = 23, une petite explication - certains muscles sont «visibles» des deux côtés).


Algorithme en première approximation:


  1. Sur la base des données des séances d'entraînement terminées, HashMap <String, Float> est construit, String est le nom du groupe musculaire, Float est le degré de charge de 0 à 10.
  2. Chacun des 23 muscles est repeint en couleur de 0 (non impliqué) à 10 (charge max.).
  3. Superposez les images musculaires repeintes en deux images (avant, arrière).
  4. Nous sauvegardons les 6 images.

image


Pour stocker 31 (16 + 15) images de taille 1500x1500 px avec le mode 24 bits, 31x1500x1500x24bit = 199 Mo de RAM est requis. Lorsque vous dépassez ~ 30 à 40 Mo, vous obtenez une OutOfMemoryException. De même, vous ne pouvez pas charger toutes les images des ressources en même temps, car vous devez libérer des ressources pour ne pas recevoir la réception. Cela signifie que vous devez superposer les images de manière séquentielle. L'algorithme se transforme en ce qui suit:


Sur la base des données des entraînements terminés, HashMap <String, Float>, String - muscle, Float - le niveau de charge de 0 à 10 est construit.


Le cycle pour chacune des 6 images:


  1. Vous avez la ressource BitmapFactory.decodeResource ().
  2. Chacun des 23 muscles est repeint en couleur de 0 (non impliqué) à 10 (charge max.).
  3. Superposez des images musculaires repeintes sur une seule toile.
  4. Bitmap.recycle () a libéré une ressource.

Nous effectuons la tâche dans un thread séparé en utilisant AsyncTask. Dans chaque tâche, deux images sont créées séquentiellement: une vue avant et une vue arrière.


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

La classe auxiliaire DoubleMusclesBitmaps n'est nécessaire que pour renvoyer deux variables Bitmap: vue avant et vue arrière. Pour l'avenir, la classe Java DoubleMusclesBitmaps est remplacée par Pair <Bitmap, Bitmap> dans Kotlin.


Dessin


Couleurs colors.xml dans les ressources de valeurs.


 <?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> 

Créer une vue


 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()); //         color = context.getResources().getColor(context.getResources() .getIdentifier("muscles_color" + color, "color", context.getPackageName())); drawMuscleElement(resultCanvas, entry.getKey(), color); } return musclesBitmap; } 

Superposition musculaire unique


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

Nous commençons la génération de 3 paires d'images.


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

Nous commençons startImageGenerating ():


 > start 1549350950177 > finish 1549350959490 diff=9313 ms 

Il convient de noter que la lecture des ressources prend beaucoup de temps. Pour chaque paire d'images, 29 fichiers PNG des ressources sont décodés. Dans mon cas, sur le coût total de création d'images, la fonction BitmapFactory.decodeResource () passe ~ 75% du temps: ~ 6960 ms.


Inconvénients:


  1. Je reçois de temps en temps une OutOfMemoryException.
  2. Le traitement prend plus de 9 secondes, et c'est sur l'émulateur (!) Dans le téléphone "moyen" (ancienne mine), il a atteint 20 secondes.
  3. AsyncTask avec toutes les fuites [mémoire] résultantes.

Avantages:
Avec probabilité (1-OutOfMemoryException), des images sont dessinées.


AsyncTask dans IntentService


Pour quitter AsyncTask, il a été décidé de passer à IntentServie, dans lequel la tâche de création d'images a été effectuée. Une fois le service terminé, si BroadcastReceiver est en cours d'exécution, nous obtenons l'URI des six images générées, sinon les images ont simplement été enregistrées afin que la prochaine fois que l'utilisateur ouvre l'application, il ne soit pas nécessaire d'attendre le processus de création. Dans le même temps, la durée de fonctionnement n'a pas changé, mais avec un inconvénient - des fuites de mémoire ont été détectées, il y avait deux autres inconvénients.


Forcer les utilisateurs à s’attendre à la création d’images pendant une telle durée est bien sûr impossible. Besoin d'optimiser.


Décrire les moyens d'optimisation:


  1. Traitement d'image.
  2. Ajout de LruCache.

Traitement d'image


Toutes les ressources PNG source ont une taille de 1500x1500 px. Réduisez-les à 1080x1080.
Comme vous pouvez le voir sur la deuxième photo, tous les codes sources sont carrés, les muscles sont en place et les vrais pixels utiles occupent une petite zone. Le fait que tous les groupes musculaires soient déjà en place est pratique pour le programmeur, mais pas rationnel pour les performances. Nous recadrons (coupons) l'excédent dans tous les codes sources, enregistrant la position (x, y) de chaque groupe musculaire afin de l'appliquer ensuite au bon endroit.


Dans la première approche, les 29 images de groupes musculaires ont été repeintes et superposées à la base. La base ne comprenait que la tête, les mains et des parties des jambes. Nous changeons de base: maintenant, il comprend, en plus de la tête, des bras et des jambes, tous les autres groupes musculaires. Nous peignons tout en gris color_muscle0. Cela permettra de ne pas repeindre et de ne pas imposer les groupes musculaires qui n'étaient pas impliqués.


Maintenant, toutes les sources ressemblent Ă  ceci:


image


Lrucache


Après un traitement supplémentaire des images originales, certains ont commencé à occuper un peu de mémoire, ce qui a conduit à penser à les réutiliser (pas à les libérer après chaque superposition avec la méthode .recycle ()) à l'aide de LruCache. Nous créons une classe pour stocker les images sources, qui assume simultanément la fonction de lecture à partir des ressources:


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

Les images sont préparées, le décodage des ressources est optimisé.
Nous ne discuterons pas de la transition en douceur de Java à Kotlin, mais c'est arrivé.


Coroutines


Le code utilisant IntentService fonctionne, mais la lisibilité du code avec des rappels ne peut pas être qualifiée d'agréable.


Ajoutez une envie de regarder les coroutines de Kotlin au travail. Nous ajoutons la compréhension qu'après quelques mois, il sera plus agréable de lire votre code synchrone que de rechercher l'endroit où retourner les fichiers Uri des images générées.


De plus, l'accélération du traitement d'image a incité l'idée d'utiliser la fonctionnalité dans plusieurs nouveaux endroits de l'application, en particulier dans la description des exercices, et pas seulement après la formation.


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

Le bundle standard de errorHandler, job et scope est scout coroutine avec un gestionnaire d'erreurs en cas de rupture de coroutine.


uries - HashMap, qui stocke 6 images en soi pour une sortie ultérieure vers l'interface utilisateur:
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?>() ... //    continuation.resume(resultUries) } } } 

Nous mesurons le temps de traitement.


 >start 1549400719844 >finish 1549400720440 diff=596 ms 

De 9313 ms, le traitement a diminué à 596 ms.


Si vous avez des idées d'optimisation supplémentaire, n'hésitez pas à commenter.

Source: https://habr.com/ru/post/fr439596/


All Articles