Maneira mais eficiente de memória para redimensionar bitmaps no Android?

115

Estou construindo um aplicativo social com uso intensivo de imagens, onde as imagens são enviadas do servidor para o dispositivo. Quando o dispositivo tem resoluções de tela menores, preciso redimensionar os bitmaps, no dispositivo, para corresponder aos tamanhos de exibição pretendidos.

O problema é que usar createScaledBitmap faz com que eu tenha muitos erros de falta de memória após redimensionar uma horda de imagens em miniatura.

Qual é a maneira mais eficiente de redimensionar bitmaps no Android?

Colt McAnlis
fonte
7
O seu servidor não pode enviar o tamanho correto e você economiza a RAM e a largura de banda do seu cliente !?
James de
2
Isso só é válido se eu possuir o recurso do servidor, se ele tiver um componente de computação disponível e, em todos os casos, puder prever as dimensões exatas das imagens para as proporções que ainda não viu. Portanto, se você estiver carregando conteúdo de recursos de um CDN de terceiros (como eu), ele não funciona :(
Colt McAnlis

Respostas:

168

Esta resposta é resumida em Carregando bitmaps grandes com eficiência que explica como usar inSampleSize para carregar uma versão de bitmap em escala reduzida.

Em particular , os bitmaps de pré-dimensionamento explicam os detalhes de vários métodos, como combiná-los e quais são os mais eficientes em termos de memória.

Existem três maneiras dominantes de redimensionar um bitmap no Android, que têm diferentes propriedades de memória:

API createScaledBitmap

Esta API pegará um bitmap existente e criará um NOVO bitmap com as dimensões exatas que você selecionou.

No lado positivo, você pode obter exatamente o tamanho de imagem que está procurando (independentemente de sua aparência). Mas a desvantagem é que essa API requer um bitmap existente para funcionar . Significa que a imagem teria que ser carregada, decodificada e um bitmap criado, antes de ser capaz de criar uma nova versão menor. Isso é ideal em termos de obter suas dimensões exatas, mas horrível em termos de sobrecarga de memória adicional. Como tal, isso é uma espécie de quebra de negócio para a maioria dos desenvolvedores de aplicativos que tendem a se preocupar com a memória

Sinalizador inSampleSize

BitmapFactory.Optionstem uma propriedade inSampleSizeque irá redimensionar sua imagem enquanto a decodifica, para evitar a necessidade de decodificar para um bitmap temporário. Este valor inteiro usado aqui carregará uma imagem em tamanho 1 / x reduzido. Por exemplo, definir inSampleSizecomo 2 retorna uma imagem com metade do tamanho, e definir como 4 retorna uma imagem com 1/4 do tamanho. Basicamente, os tamanhos das imagens serão sempre um pouco menores do que o tamanho da fonte.

Do ponto de vista da memória, usar inSampleSizeé uma operação muito rápida. Efetivamente, ele só decodificará cada X pixel de sua imagem em seu bitmap resultante. Existem dois problemas principais inSampleSize:

  • Não fornece resoluções exatas . Ele apenas diminui o tamanho do seu bitmap em uma potência de 2.

  • Não produz o redimensionamento de melhor qualidade . A maioria dos filtros de redimensionamento produz imagens com boa aparência ao ler blocos de pixels e, em seguida, ponderá-los para produzir o pixel redimensionado em questão. inSampleSizeevita tudo isso lendo apenas alguns pixels. O resultado é bastante eficiente e com pouca memória, mas a qualidade é prejudicada.

Se você está lidando apenas com o encolhimento de sua imagem em algum tamanho pow2, e a filtragem não é um problema, então você não encontrará um método mais eficiente em memória (ou desempenho) do que inSampleSize.

Sinalizadores inScaled, inDensity, inTargetDensity

Se você precisa redimensionar uma imagem para uma dimensão que não é igual a uma potência de dois, então você vai precisar do inScaled, inDensitye inTargetDensitybandeiras de BitmapOptions. Quando o inScaledsinalizador for definido, o sistema derivará o valor de escala a ser aplicado ao seu bitmap, dividindo o inTargetDensitypelos inDensityvalores.

mBitmapOptions.inScaled = true;
mBitmapOptions.inDensity = srcWidth;
mBitmapOptions.inTargetDensity =  dstWidth;

// will load & resize the image to be 1/inSampleSize dimensions
mCurrentBitmap = BitmapFactory.decodeResources(getResources(), 
      mImageIDs, mBitmapOptions);

Usar este método redimensionará sua imagem e também aplicará um 'filtro de redimensionamento' a ela, ou seja, o resultado final parecerá melhor porque alguma matemática adicional foi levada em consideração durante a etapa de redimensionamento. Mas esteja avisado: essa etapa de filtro extra, leva mais tempo de processamento e pode aumentar rapidamente para imagens grandes, resultando em redimensionamentos lentos e alocações de memória extra para o próprio filtro.

Geralmente não é uma boa ideia aplicar essa técnica a uma imagem significativamente maior do que o tamanho desejado, devido à sobrecarga de filtragem extra.

Combinação mágica

De uma perspectiva de memória e desempenho, você pode combinar essas opções para obter os melhores resultados. (definindo o inSampleSize, inScaled, inDensitye inTargetDensitybandeiras)

inSampleSizeserá aplicado primeiro à imagem, colocando-a na próxima potência de dois MAIOR do que o tamanho de destino. Em seguida, inDensity& inTargetDensitysão usados ​​para dimensionar o resultado para as dimensões exatas que você deseja, aplicando uma operação de filtro para limpar a imagem.

Combinar esses dois é uma operação muito mais rápida, uma vez que a inSampleSizeetapa reduzirá o número de pixels que a etapa baseada em densidade resultante precisará para aplicar seu filtro de redimensionamento.

mBitmapOptions.inScaled = true;
mBitmapOptions.inSampleSize = 4;
mBitmapOptions.inDensity = srcWidth;
mBitmapOptions.inTargetDensity =  dstWidth * mBitmapOptions.inSampleSize;

// will load & resize the image to be 1/inSampleSize dimensions
mCurrentBitmap = BitmapFactory.decodeFile(fileName, mBitmapOptions);

Se você precisar ajustar uma imagem a dimensões específicas, e alguma filtragem mais agradável, essa técnica é a melhor ponte para obter o tamanho certo, mas feita em uma operação rápida e com pouca memória.

Obtendo as dimensões da imagem

Obtendo o tamanho da imagem sem decodificar a imagem inteira Para redimensionar seu bitmap, você precisará saber as dimensões de entrada. Você pode usar o inJustDecodeBoundssinalizador para ajudá-lo a obter as dimensões da imagem, sem a necessidade de realmente decodificar os dados de pixel.

// Decode just the boundaries
mBitmapOptions.inJustDecodeBounds = true;
BitmapFactory.decodeFile(fileName, mBitmapOptions);
srcWidth = mBitmapOptions.outWidth;
srcHeight = mBitmapOptions.outHeight;


//now go resize the image to the size you want

Você pode usar este sinalizador para decodificar o tamanho primeiro e, em seguida, calcular os valores adequados para dimensionar a sua resolução alvo.

Colt McAnlis
fonte
1
seria ótimo se você pudesse nos dizer o que é dstWidth?
k0sh de
@ k0sh dstWIdth é a largura de ImageView para onde vai, destination widthou seja, ou dstWidth para abreviar
tyczj
@tyczj obrigado pela resposta, eu sei o que é, mas alguns podem não saber e como Colt respondeu a essa pergunta, talvez ele pudesse explicar para que as pessoas não se confundam.
k0sh de
Postagem legal ... não sabia sobre sinalizadores inScaled, inDensity, inTargetDensity anteriormente ...
maveroid
Tenho assistido à série de padrões de desempenho do Android e aprendi muito!
Anis
13

Por melhor (e precisa) que essa resposta seja, também é muito complicada. Em vez de reinventar a roda, considere bibliotecas como Glide , Picasso , UIL , Ion ou qualquer outra que implementa essa lógica complexa e sujeita a erros para você.

O próprio Colt ainda recomenda dar uma olhada em Glide e Picasso no vídeo de padrões de desempenho de bitmaps pré-dimensionamento .

Ao usar bibliotecas, você pode obter toda a eficiência mencionada na resposta da Colt, mas com APIs muito mais simples que funcionam de forma consistente em todas as versões do Android.

Sam Judd
fonte