¿La forma más eficiente de memoria para cambiar el tamaño de mapas de bits en Android?

115

Estoy creando una aplicación social con uso intensivo de imágenes en la que las imágenes se envían desde el servidor al dispositivo. Cuando el dispositivo tiene resoluciones de pantalla más pequeñas, necesito cambiar el tamaño de los mapas de bits, en el dispositivo, para que coincidan con los tamaños de pantalla previstos.

El problema es que el uso de createScaledBitmap hace que me encuentre con muchos errores de memoria insuficiente después de cambiar el tamaño de una horda de imágenes en miniatura.

¿Cuál es la forma más eficiente de memoria para cambiar el tamaño de mapas de bits en Android?

Colt McAnlis
fuente
7
¿Su servidor no puede enviar el tamaño correcto para que ahorre la RAM y el ancho de banda de su cliente?
James
2
Eso solo es válido si soy el propietario del recurso del servidor, tiene un componente informático disponible y, en todos los casos, puede predecir las dimensiones exactas de las imágenes para las relaciones de aspecto que aún no había visto. Entonces, si está cargando contenido de recursos de un CDN de terceros (como yo), no funciona :(
Colt McAnlis

Respuestas:

168

Esta respuesta se resume en Cargar mapas de bits grandes de forma eficiente, que explica cómo usar inSampleSize para cargar una versión de mapa de bits reducida.

En particular , los mapas de bits de preescalado explican los detalles de varios métodos, cómo combinarlos y cuáles son los más eficientes en memoria.

Hay tres formas dominantes de cambiar el tamaño de un mapa de bits en Android que tienen diferentes propiedades de memoria:

API createScaledBitmap

Esta API tomará un mapa de bits existente y creará un NUEVO mapa de bits con las dimensiones exactas que ha seleccionado.

En el lado positivo, puede obtener exactamente el tamaño de imagen que está buscando (independientemente de cómo se vea). Pero la desventaja es que esta API requiere un mapa de bits existente para funcionar . Lo que significa que la imagen tendría que cargarse, decodificarse y crearse un mapa de bits antes de poder crear una nueva versión más pequeña. Esto es ideal en términos de obtener sus dimensiones exactas, pero horrible en términos de sobrecarga de memoria adicional. Como tal, esto es una especie de factor decisivo para la mayoría de los desarrolladores de aplicaciones que tienden a ser conscientes de la memoria.

inSampleSize bandera

BitmapFactory.Optionstiene una propiedad inSampleSizeque cambiará el tamaño de su imagen mientras la decodifica, para evitar la necesidad de decodificar a un mapa de bits temporal. Este valor entero utilizado aquí cargará una imagen con un tamaño reducido 1 / x. Por ejemplo, establecer inSampleSizeen 2 devuelve una imagen que tiene la mitad del tamaño, y establecerlo en 4 devuelve una imagen que es 1/4 del tamaño. Básicamente, los tamaños de imagen siempre serán una potencia de dos más pequeños que el tamaño de la fuente.

Desde la perspectiva de la memoria, usar inSampleSizees una operación realmente rápida. Efectivamente, solo decodificará cada X píxel de su imagen en su mapa de bits resultante. Sin inSampleSizeembargo, hay dos problemas principales :

  • No te da resoluciones exactas . Solo reduce el tamaño de su mapa de bits en una potencia de 2.

  • No produce un cambio de tamaño de la mejor calidad . La mayoría de los filtros de cambio de tamaño producen imágenes atractivas al leer bloques de píxeles y luego ponderarlos para producir el píxel redimensionado en cuestión. inSampleSizeevita todo esto con solo leer cada pocos píxeles. El resultado es bastante eficiente y con poca memoria, pero la calidad se ve afectada.

Si solo está lidiando con reducir su imagen en un tamaño de pow2 y el filtrado no es un problema, entonces no puede encontrar un método más eficiente en memoria (o rendimiento) que inSampleSize.

Indicadores inScaled, inDensity, inTargetDensity

Si tiene que escalar una imagen a una dimensión que no es igual a una potencia de dos, entonces usted tendrá el inScaled, inDensityy inTargetDensitybanderas de BitmapOptions. Cuando inScaledse ha establecido la bandera, el sistema derivará el valor de escala para aplicar a su mapa de bits dividiendo el inTargetDensitypor los 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);

El uso de este método redimensionará su imagen y también le aplicará un 'filtro de cambio de tamaño', es decir, el resultado final se verá mejor porque se han tenido en cuenta algunas matemáticas adicionales durante el paso de cambio de tamaño. Pero tenga cuidado: ese paso de filtro adicional, requiere tiempo de procesamiento adicional y puede sumarse rápidamente para imágenes grandes, lo que resulta en cambios de tamaño lentos y asignaciones de memoria adicionales para el filtro en sí.

Por lo general, no es una buena idea aplicar esta técnica a una imagen que sea significativamente más grande que el tamaño deseado, debido a la sobrecarga de filtrado adicional.

Combinación mágica

Desde la perspectiva de la memoria y el rendimiento, puede combinar estas opciones para obtener los mejores resultados. (ajuste de la inSampleSize, inScaled, inDensityy inTargetDensitybanderas)

inSampleSizeprimero se aplicará a la imagen, llevándola a la siguiente potencia de dos MÁS GRANDE que su tamaño objetivo. Luego, inDensityy inTargetDensityse utilizan para escalar el resultado a las dimensiones exactas que desee, aplicando una operación de filtro para limpiar la imagen.

Combinar estos dos es una operación mucho más rápida, ya que el inSampleSizepaso reducirá la cantidad de píxeles que el paso basado en Densidad resultante necesitará para aplicar su filtro de cambio de tamaño.

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

Si necesita ajustar una imagen a dimensiones específicas y un filtrado más agradable, esta técnica es el mejor puente para obtener el tamaño correcto, pero se realiza en una operación rápida y con poca memoria.

Obtener las dimensiones de la imagen

Obtener el tamaño de la imagen sin decodificar la imagen completa Para cambiar el tamaño de su mapa de bits, necesitará conocer las dimensiones entrantes. Puede usar la inJustDecodeBoundsbandera para obtener las dimensiones de la imagen, sin necesidad de decodificar los datos de píxeles.

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

Puede usar esta bandera para decodificar el tamaño primero y luego calcular los valores adecuados para escalar a su resolución objetivo.

Colt McAnlis
fuente
1
Sería genial si pudiera decirnos qué es dstWidth.
k0sh
@ k0sh dstWIdth es el ancho de ImageView para dónde va, es decir, destination widtho dstWidth para abreviar
tyczj
@tyczj, gracias por la respuesta, sé qué es, pero es posible que algunos no lo sepan y, dado que Colt, quien realmente respondió esta pregunta, tal vez podría explicarlo para que la gente no se confunda.
k0sh
Buena publicación ... no sabía sobre las banderas inScaled, inDensity, inTargetDensity anteriormente ...
maveroid
He estado viendo la serie de patrones de rendimiento de Android y he aprendido mucho.
Anis
13

Por muy agradable (y precisa) que sea esta respuesta, también es muy complicada. En lugar de reinventar la rueda, considere bibliotecas como Glide , Picasso , UIL , Ion o cualquier otra que implemente esta lógica compleja y propensa a errores.

El mismo Colt incluso recomienda echar un vistazo a Glide y Picasso en el video de patrones de rendimiento de mapas de bits de preescalado .

Al usar bibliotecas, puede obtener toda la eficiencia que se menciona en la respuesta de Colt, pero con API mucho más simples que funcionan de manera consistente en todas las versiones de Android.

Sam Judd
fuente