Como acelerei o processamento de imagens no Android 15 vezes

Como otimizar o processamento de imagens em tempo de execução quando é necessário criar 6 imagens, cada uma consistindo em 15-16 PNGs sobrepostos sequencialmente, sem receber uma OutOfMemoryException no caminho?


imagem


Ao desenvolver meu aplicativo de estimação, encontrei o problema do processamento de imagens. Como não consegui fornecer bons Googlecases para o Google, tive que andar no meu ancinho e inventar uma bicicleta sozinha.
Também durante o desenvolvimento, houve uma migração do Java para o Kotlin, portanto o código será traduzido em algum momento.


Desafio


Pedido de treinamento na academia. É necessário construir um mapa do trabalho muscular com base nos resultados do treinamento no tempo de execução do aplicativo.
Dois sexos: M e G. Considere a opção M, porque para M tudo é o mesmo.
6 imagens devem ser construídas simultaneamente: 3 períodos (uma sessão de treinamento, por semana, por mês) x 2 visualizações (frente, verso)


imagem


Cada uma dessas imagens consiste em 15 imagens de grupos musculares para uma visão frontal e 14 em uma visão traseira. Mais 1 imagem da fundação (cabeça, mãos e pés). No total, para coletar a vista frontal, você precisa sobrepor 16 imagens, na parte traseira - 15.


Apenas 23 grupos musculares para ambos os lados (para aqueles com 15 + 14! = 23, uma pequena explicação - alguns músculos são "visíveis" em ambos os lados).


Algoritmo na primeira aproximação:


  1. Com base nos dados dos exercícios concluídos, o HashMap <String, Float> é construído, String é o nome do grupo muscular, Float é o grau de carga de 0 a 10.
  2. Cada um dos 23 músculos é repintado em cores de 0 (não envolvido) a 10 (carga máxima).
  3. A sobreposição repintou imagens musculares em duas imagens (frente, verso).
  4. Guardamos todas as 6 imagens.

imagem


Para armazenar 31 (16 + 15) imagens de tamanho 1500x1500 px no modo de 24 bits, é necessário 31x1500x1500x24bit = 199 MB de RAM. Quando você excede ~ 30-40 MB, você obtém uma OutOfMemoryException. Da mesma forma, você não pode carregar todas as imagens dos recursos ao mesmo tempo, porque é necessário liberar recursos para não receber a recepção. Isso significa que você precisa sobrepor imagens sequencialmente. O algoritmo se transforma no seguinte:


Com base nos dados dos exercícios concluídos, o HashMap <String, Float>, String-muscle, Float - nível de carga de 0 a 10 são construídos.


O ciclo para cada uma das 6 imagens:


  1. Obteve o recurso BitmapFactory.decodeResource ().
  2. Cada um dos 23 músculos é repintado em cores de 0 (não envolvido) a 10 (carga máxima).
  3. A sobreposição repintou imagens musculares em uma tela.
  4. Bitmap.recycle () liberou um recurso.

Realizamos a tarefa em um thread separado usando o AsyncTask. Em cada tarefa, duas imagens são criadas seqüencialmente: uma vista frontal e uma traseira.


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

A classe auxiliar DoubleMusclesBitmaps é necessária apenas para retornar duas variáveis ​​de bitmap: vista frontal e traseira. No futuro, a classe Java DoubleMusclesBitmaps é substituída por Pair <Bitmap, Bitmap> no Kotlin.


Desenhando


Colore colors.xml em recursos de valores.


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

Crie uma vista


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

Sobreposição de músculo único


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

Começamos a geração de 3 pares de imagens.


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

Começamos startImageGenerating ():


 > start 1549350950177 > finish 1549350959490 diff=9313 ms 

Note-se que a leitura de recursos leva muito tempo. Para cada par de imagens, 29 arquivos PNG dos recursos são decodificados. No meu caso, fora do custo total da criação de imagens, a função BitmapFactory.decodeResource () passa ~ 75% do tempo: ~ 6960 ms.


Contras:


  1. Recebo uma OutOfMemoryException de tempos em tempos.
  2. O processamento leva mais de 9 segundos e está no emulador (!) No telefone "médio" (mina antiga), chegou a 20 segundos.
  3. AsyncTask com todos os vazamentos resultantes de [memória].

Prós:
Com probabilidade (1-OutOfMemoryException), imagens são desenhadas.


AsyncTask no IntentService


Para sair do AsyncTask, foi decidido mudar para o IntentServie, no qual a tarefa de criar imagens era executada. Após a conclusão do serviço, se o BroadcastReceiver estiver em execução, obtemos o Uri de todas as seis imagens geradas; caso contrário, as imagens foram simplesmente salvas para que, na próxima vez em que o usuário abra o aplicativo, não seja necessário aguardar o processo de criação. Ao mesmo tempo, o tempo de operação não mudou, mas com um menos - vazamentos de memória calculados, houve mais dois menos.


Forçar os usuários a esperar a criação de imagens por esse período de tempo, é claro, é impossível. Precisa otimizar.


Esboço maneiras de otimização:


  1. Processamento de imagem.
  2. Adicionando LruCache.

Processamento de imagem


Todos os recursos PNG de origem têm tamanho de 1500x1500 px. Reduza-os para 1080x1080.
Como você pode ver na segunda foto, todos os códigos-fonte são quadrados, os músculos estão no lugar e os pixels úteis reais ocupam uma área pequena. O fato de todos os grupos musculares já existirem é conveniente para o programador, mas não é racional para o desempenho. Recortamos (cortamos) o excesso em todos os códigos-fonte, registrando a posição (x, y) de cada grupo muscular para aplicá-lo posteriormente no local certo.


Na primeira abordagem, todas as 29 imagens de grupos musculares foram repintadas e sobrepostas na base. A base incluía apenas a cabeça, mãos e partes das pernas. Mudamos a base: agora inclui, além da cabeça, braços e pernas, todos os outros grupos musculares. Pintamos tudo em cinza color_muscle0. Isso permitirá não repintar e não impor os grupos musculares que não estavam envolvidos.


Agora todas as fontes ficam assim:


imagem


Lrucache


Após o processamento adicional das imagens originais, algumas começaram a ocupar um pouco de memória, o que levou ao pensamento de reutilização (não liberando-as após cada sobreposição com o método .recycle ()) usando o LruCache. Criamos uma classe para armazenar imagens de origem, que assume simultaneamente a função de leitura de recursos:


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

As imagens são preparadas, a decodificação de recursos é otimizada.
Não discutiremos a transição suave de Java para Kotlin, mas aconteceu.


Coroutines


O código usando o IntentService funciona, mas a legibilidade do código com retornos de chamada não pode ser considerada agradável.


Adicione um desejo de observar as corotinas de Kotlin em ação. Acrescentamos o entendimento de que, após alguns meses, será mais agradável ler seu código síncrono do que procurar o local para retornar os arquivos Uri das imagens geradas.


Além disso, a aceleração do processamento de imagens levou a ideia de usar o recurso em vários novos locais do aplicativo, principalmente na descrição de exercícios, e não apenas após o treinamento.


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

O pacote padrão de errorHandler, trabalho e escopo é uma rotina de escoteiro com um manipulador de erros, se a corotina quebrar.


uries - HashMap, que armazena 6 imagens em si para saída subsequente para a interface do usuário:
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) } } } 

Medimos o tempo de processamento.


 >start 1549400719844 >finish 1549400720440 diff=596 ms 

De 9313 ms, o processamento diminuiu para 596 ms.


Se você tem idéias para otimização adicional, sinta-se à vontade para comentar.

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


All Articles