Optimización del rendimiento de Android: gestión de memoria de mapa de bits y carga de imágenes prolongada

1. Cómo calcular la memoria ocupada por el mapa de bits
2. Gestión de la memoria caché en memoria del mapa de bits
3. Puntos a tener en cuenta al cargar imágenes largas

Bitmap es el "gran usuario" del uso de memoria en la Aplicación. Cómo utilizar mejor Bitmap y reducir su uso de la memoria de la Aplicación es un problema inevitable en nuestro desarrollo.

¿Cómo obtener el objeto de mapa de bits?

El mapa de bits es una de las categorías más importantes en el procesamiento de imágenes en el sistema Android. El mapa de bits puede obtener información del archivo de imagen, realizar operaciones como cortar, rotar, hacer zoom y comprimir la imagen, y puede guardar el archivo de imagen en un formato específico.

Hay dos formas de crear un objeto Bitmap: son creadas por Bitmap.createBitmap () y la serie de métodos estáticos de decodificación de BitmapFactory.

A continuación, presentamos principalmente el método de decodificación de BitmapFactory para crear objetos de mapa de bits.

  • decodeFile () se carga desde el sistema de archivos
    • Abrir imágenes o fotos locales a través de Intent
    • Obtener el camino de la imagen según el uri
    • Analizar mapa de bits de acuerdo con la ruta: Bitmap bm = BitmapFactory.decodeFile (ruta);
  • decodeResource () se carga desde recursos locales en forma de R.drawable.xxx
    • Bitmap bm = BitmapFactory.decodeResource (getResources (), R.drawable.icon);
  • decodeStream () carga desde el flujo de entrada
    • Bitmap bm = BitmapFactory.decodeStream (flujo);
  • decodeByteArray () se carga desde la matriz de bytes
    • Bitmap bm = BitmapFactory.decodeByteArray (myByte, 0, myByte.length);

BitmapFactory.Options

  • inSampleSize: frecuencia de muestreo, este es el tamaño de la muestra. Se utiliza para encoger la imagen y cargarla, para no ocupar demasiada memoria, apto para miniaturas.

  • inJustDecodeBounds: cuando inJustDecodeBounds es verdadero, cuando se ejecuta el método decodexxx, BitmapFactory solo analizará la información original de ancho y alto de la imagen, y en realidad no cargará la imagen

  • inPreferredConfig: Se utiliza para configurar el método de decodificación de imágenes, correspondiente al tipo Bitmap.Config. Si no es nulo, se utilizará para decodificar la imagen. El valor predeterminado es Bitmap.Config.ARGB_8888

  • inBitmap: la configuración inBitmap se introdujo en Android 3.0. Al establecer este parámetro, el mapa de bits que se ha creado antes se puede utilizar cuando se carga la imagen para ahorrar memoria y evitar crear un mapa de bits de nuevo. En Android 4.4, se agrega que el tamaño de la imagen establecido por inBitmap es diferente del tamaño de la imagen que debe cargarse, siempre que la imagen de inBitmap sea más grande que la imagen actual que debe cargarse.

  • inDensity
    representa la densidad de píxeles de este mapa de bits, y este valor está relacionado con el directorio dibujable donde se coloca esta imagen.
    Asignación de inDensidad:
    drawable-ldpi 120 drawable
    -mdpi 160
    drawable-hdpi 240
    drawable-xhdpi 320
    drawable-xxhdpi 480

  • inTargetDensity
    representa la densidad de píxeles del objetivo (pantalla) cuando se va a dibujar,
    la forma de obtenerlo en el código: getResources (). getDisplayMetrics (). densidadDpi

  • inScreenDensity

    /**
     * Decode a new Bitmap from an InputStream. This InputStream was obtained from
     * resources, which we pass to be able to scale the bitmap accordingly.
     * @throws IllegalArgumentException if {@link BitmapFactory.Options#inPreferredConfig}
     *         is {@link android.graphics.Bitmap.Config#HARDWARE}
     *         and {@link BitmapFactory.Options#inMutable} is set, if the specified color space
     *         is not {@link ColorSpace.Model#RGB RGB}, or if the specified color space's transfer
     *         function is not an {@link ColorSpace.Rgb.TransferParameters ICC parametric curve}
     */
    @Nullable
    public static Bitmap decodeResourceStream(@Nullable Resources res, @Nullable TypedValue value,
            @Nullable InputStream is, @Nullable Rect pad, @Nullable Options opts) {
    
    
        validate(opts);
        if (opts == null) {
    
    
            opts = new Options();
        }

        if (opts.inDensity == 0 && value != null) {
    
    
        	//value就是读取资源文件时,资源文件的一些数据信息
            final int density = value.density;
            if (density == TypedValue.DENSITY_DEFAULT) {
    
    
                opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
            } else if (density != TypedValue.DENSITY_NONE) {
    
    
            	//inDensity赋值的是资源所在文件夹对应的密度
                opts.inDensity = density;
            }
        }
        
        if (opts.inTargetDensity == 0 && res != null) {
    
    
        	//inTargetDensity赋值的是手机屏幕的像素密度densityDpi
            opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
        }
        
        return decodeStream(is, pad, opts);
    }

A través de estos parámetros de BitmapFactory.Options, podemos cargar la imagen reducida a una determinada frecuencia de muestreo y luego usar la imagen reducida en ImageView. Esto reducirá el uso de memoria, evitará OOM y mejorará el rendimiento de la carga de Bitamp.

En realidad, esto es lo que a menudo llamamos compresión de tamaño de imagen. La compresión de tamaño es el método de cálculo para comprimir los píxeles de una imagen y el tamaño de la memoria de una imagen: el tipo de color del píxel de la imagen * ancho * alto , cambiando los tres valores para reducir la memoria ocupada por la imagen para evitar OOM, por supuesto de esta manera Puede distorsionar la imagen.

Bitmap.Config

public static enum Config {
    
    
    ALPHA_8,//每个像素占用1byte内存
    RGB_565,//每个像素占用2byte内存
    ARGB_4444,//每个像素占用2byte内存
    ARGB_8888;//每个像素占用4byte内存;默认模式
}

Cálculo de uso de memoria de mapa de bits

El mapa de bits, como mapa de bits, necesita leer los datos de la imagen en cada píxel, que ocupa principalmente la memoria, es decir, estos datos de píxeles. El tamaño total de los datos de píxeles de una imagen es el tamaño de píxeles de la imagen * el tamaño en bytes de cada píxel. Normalmente, este valor se puede entender como el tamaño de la memoria ocupada por el objeto Bitmap. El tamaño de píxel de la imagen es valor de píxel horizontal * valor de píxel vertical. Entonces existe la siguiente fórmula:

Memoria de mapa de bits ≈ tamaño total de los datos de píxeles = valor de píxel horizontal * valor de píxel vertical * memoria para cada píxel

Sin tener un objeto de mapa de bits, calcule cuánta memoria ocupa la imagen en el directorio de recursos dibujables después de cargarse como mapa de bits

Cuando BitmapFactory.decodeResource carga una imagen en la memoria, el ancho y alto del mapa de bits generado no son iguales al ancho y alto de la imagen original, pero se escalarán de acuerdo con la densidad de la pantalla actual.

El tamaño del mapa de bits en la memoria depende de:

  • Formato de color, si es ARGB8888, es 4 bytes por píxel, si es RGB565, es 2 bytes
  • Directorio de recursos donde se almacenan los archivos de imágenes originales
  • La densidad de la pantalla de destino (por lo que en las mismas condiciones, la memoria consumida por Redmi en términos de recursos debe ser menor que la de Samsung S6)
int realWidth = (int) (rawWidth * targetDensity / (float) rawDensity + 0.5f)
int realHeight = (int) (rawHeight * targetDensity / (float) rawDensity + 0.5f) 
int memory = realWidth * realHeight * bytes_for_current_colorMode;

rawWidth es el ancho original de la imagen del recurso
targetDensity es la densidad de la pantalla actual (variable inTargetDensity en el código fuente de Android)
rawDensity es la densidad correspondiente a la carpeta de recursos donde se encuentra la imagen del recurso (variable inDensity en el código fuente de Android)
bytes_for_current_colorMode es el formato de color actual El número de bytes correspondientes a cada píxel.

Por ejemplo:
formato de color ARGB_8888:
ARGB ocupa 8 bits cada uno, y cada píxel ocupa memoria (8 bits + 8 bits + 8 bits + 8 bits) / 8 = 4 bytes, por lo que la memoria ocupada por Bitmap es realWidth * realHeight * 4
Formato de color RGB_565:
R5 bit, G6 Bit, B5 bit, cada píxel ocupa memoria (5bit + 6bit + 5bit) / 8 = 2Byte, por lo que la memoria ocupada por Bitmap es realWidth * realHeight * 2

Corresponde a las variables del código fuente de Android:

int realWidth = (int) (rawWidth * inTargetDensity / (float) inDensity + 0.5f)
int realHeight = (int) (rawHeight * inTargetDensity / (float) inDensity + 0.5f) 
int memory = realWidth * realHeight * bytes_for_current_colorMode;

Nuestro entendimiento habitual es multiplicar directamente el ancho de la imagen por la altura y luego multiplicarlo por el tamaño de la memoria ocupada por un solo píxel en el formato de mapa de bits actual. Este algoritmo ignora dos puntos:
1. Cuando el dispositivo Android carga Bitmap, escalará las imágenes almacenadas en directorios como drawable-hdpi, drawable-xhdpi, drawable-xxhdpi ..., por lo que aquí debe tomar el ancho original de la imagen. Alto para cálculos de escala.

2. Si se tiene en cuenta el primer punto, el tamaño de memoria calculado del mapa de bits es ligeramente diferente de bitmap.getByteCount (). Esta diferencia se debe a que el resultado de "(rawWidth * inTargetDensity / (float) inDensity + 0.5f)" es un tipo flotante y el número de píxeles en la imagen debe ser un número entero. Entonces hay un proceso de redondeo, el error proviene de aquí.

Después de comprender los principios anteriores, podemos sacar las siguientes conclusiones:
1. En el mismo dispositivo, cuanto menor sea la densidad (dpi) correspondiente al directorio de recursos donde se almacena el archivo de imagen, mayor será el ancho y alto del mapa de bits cargado y la memoria ocupada También más grande. De manera similar, cuanto mayor sea el ppp del directorio de recursos donde se encuentra la imagen, menor será el tamaño del mapa de bits generado.
2. Cuanto mayor sea la densidad de píxeles de la pantalla del dispositivo, mayor
será el tamaño del mapa de bits generado 3. El valor de densidad correspondiente al directorio res / drawable es el mismo que el directorio res / drawable-mdpi, igual a 1, y el valor de ppp es 160.
4. Si la densidad de píxeles del directorio de recursos donde se almacena el archivo de imagen es la misma que la densidad de píxeles de la pantalla del dispositivo, el mapa de bits generado no se escalará y el tamaño es el tamaño original.
Por lo tanto, la fórmula de cálculo anterior de la memoria de mapa de bits se puede convertir en:
memoria de mapa de bits ≈ tamaño total de los datos de píxeles = ancho de píxel de imagen * altura de píxel de imagen * (densidad de píxeles de la pantalla del dispositivo / densidad de píxeles del directorio de recursos donde se almacena el archivo de imagen) ^ 2 * Memoria por píxel = Ancho de píxel de la imagen * Altura de píxel de la imagen * Memoria por píxel

Optimización de la memoria de mapa de bits: de
la fórmula anterior, no es difícil ver que hay tres formas principales de optimizar la memoria de mapa de bits:

  • Al cargar Bitmap, seleccione un parámetro de calidad de color bajo (Bitmap.Config), como RGB_5665, de modo que en comparación con el ARGB_8888 predeterminado, el uso de memoria se reduzca a la mitad. Es adecuado para escenas que requieren una diversidad de color relativamente baja.
  • Coloque las imágenes en un directorio de recursos razonable y manténgalas lo más coherentes posible con la densidad de la pantalla. Pero no los coloque todos en el directorio de recursos con la densidad más alta. La densidad de píxeles del directorio de recursos es mayor que la densidad de la pantalla, y el tamaño del mapa de bits cargado será menor que el tamaño original, o incluso menor que el tamaño del área de visualización, lo que no puede cumplir con algunos requisitos.
  • Según el tamaño del control de destino, al cargar la imagen, se escala el tamaño del mapa de bits. Por ejemplo, en una pantalla con una densidad de píxeles de 480 ppp y un ImageView con un ancho de 300 ppp y una altura de 200 ppp, la resolución de la imagen sin escala que se puede mostrar es 900 * 600. Si la resolución de la imagen es mayor que este tamaño, es necesario considerar reducir la escala al analizar. .

Objeto de mapa de bits existente, calcular el tamaño de memoria ocupado por mapa de bits

Simplemente llame al método getByteCount () de Bitmap.

getByteCount (): devuelve el número mínimo de bytes que se pueden utilizar para almacenar este píxel de mapa de bits. Su método de cálculo interno: tamaño en bytes de cada fila * número total de filas (es decir, altura)

    /**
     * Returns the minimum number of bytes that can be used to store this bitmap's pixels.
     *
     * <p>As of {@link android.os.Build.VERSION_CODES#KITKAT}, the result of this method can
     * no longer be used to determine memory usage of a bitmap. See {@link
     * #getAllocationByteCount()}.</p>
     */
    public final int getByteCount() {
    
    
        if (mRecycled) {
    
    
            Log.w(TAG, "Called getByteCount() on a recycle()'d bitmap! "
                    + "This is undefined behavior!");
            return 0;
        }
        // int result permits bitmaps up to 46,340 x 46,340
        return getRowBytes() * getHeight();
    }

Compresión de memoria de mapa de bits

¿Las imágenes del mismo ancho y alto en diferentes formatos tienen el mismo uso de memoria?

Cuando usamos imágenes, elegimos jpg, png o webp, ¿afectará la memoria?
Siempre que el ancho y el alto originales de la imagen sean los mismos y estén colocados en la misma carpeta, la memoria que ocupa Bitmap después de la carga es la misma. Por ejemplo: R.drawable.icon_mv_jpg, R.drawable.icon_mv_png, R.drawable.icon_mv_webp tienen el mismo ancho y alto de las tres imágenes, y están ubicadas en la misma carpeta, por lo que
BitmapFactory.decodeResource carga las tres imágenes en la misma pantalla móvil El ancho y el alto son los mismos, y el parámetro de calidad de color predeterminado de BitmapFactory.La configuración de opciones es ARGB_8888,
por lo que la memoria que ocupan estas tres imágenes es la misma cuando se cargan en la memoria, aunque el disco que ocupan estas tres imágenes El tamaño es diferente.

Realización de compresión de imágenes

Los parámetros de BitmapFactory.options utilizados principalmente: cuando
inJustDecodeBounds
es verdadero, el decodificador devolverá nulo, pero se analizará el campo outxxx

inPreferredConfig
establece el formato de píxeles de la imagen después de la decodificación, como ARGB_8888 / RGB_565

inSampleSize
establece la relación de zoom de decodificación de la imagen, si el valor es 2, el ancho y el alto de la imagen cargada es la mitad del original y
el tamaño de memoria de toda la imagen es 1/4 de la imagen original

/**
 * 图片压缩
 */
public class ImageResize {
    
    


    /**
     * 返回压缩图片
     *
     * @param context
     * @param id
     * @param maxW
     * @param maxH
     * @param hasAlpha
     * @return
     */
    public static Bitmap resizeBitmap(Context context, int id, int maxW, int maxH, boolean hasAlpha, Bitmap reusable) {
    
    

        Resources resources = context.getResources();

        BitmapFactory.Options options = new BitmapFactory.Options();
        // 设置为true后,再去解析,就只解析 out 参数
        options.inJustDecodeBounds = true;

        BitmapFactory.decodeResource(resources, id, options);

        int w = options.outWidth;
        int h = options.outHeight;


        options.inSampleSize = calcuteInSampleSize(w, h, maxW, maxH);

        if (!hasAlpha) {
    
    
            options.inPreferredConfig = Bitmap.Config.RGB_565;
        }

        options.inJustDecodeBounds = false;

        // 复用, inMutable 为true 表示易变
        options.inMutable = true;
        options.inBitmap = reusable;


        return BitmapFactory.decodeResource(resources, id, options);

    }

    /**
     * 计算 缩放系数
     *
     * @param w
     * @param h
     * @param maxW
     * @param maxH
     * @return
     */
    private static int calcuteInSampleSize(int w, int h, int maxW, int maxH) {
    
    

        int inSampleSize = 1;

        if (w > maxW && h > maxH) {
    
    
            inSampleSize = 2;

            while (w / inSampleSize > maxW && h / inSampleSize > maxH) {
    
    
                inSampleSize *= 2;
            }

        }

        return inSampleSize;
    }
}

Optimización de memoria de mapa de bits-reutilización de memoria

El mecanismo de reutilización de memoria inBitmap que viene con Bitmap se refiere principalmente a la reutilización de bloques de memoria. No es necesario volver a solicitar una nueva memoria para este mapa de bits, lo que evita una asignación y reciclaje de memoria, evitando así OOM y jitter de memoria, y mejora eficiencia operativa.

InBitmap es similar al principio técnico del grupo de objetos, evitando la pérdida de rendimiento causada por la frecuente creación y destrucción de memoria. El uso de inBitmap puede mejorar la eficiencia del ciclo del mapa de bits.

En la optimización del rendimiento del segundo trimestre lanzada por Google, se menciona la tecnología inBitmap https://www.youtube.com/watch?v=_ioFW3cyRV0&index=17&list=PLWz5rJ2EKKc9CBxr3BVjPTPoDPLdPIFCE (requiere sobre la pared)

Antes de usar inBitmap, cada mapa de bits creado requiere un bloque de memoria exclusivo. Después de
Inserte la descripción de la imagen aquí
usar inBitmap, varios mapas de bits reutilizarán el mismo bloque de memoria.
Inserte la descripción de la imagen aquí
Por lo tanto, usar inBitmap puede mejorar en gran medida la eficiencia de la utilización de la memoria, pero también tiene varias restricciones:
1. inBitmap solo se puede usar en Úselo después del SDK 11 (Android 3.0). En Android 2.3, los datos del mapa de bits se almacenan en el área de la memoria nativa, no en el montón de memoria de Dalvik.
2. Entre SDK 11 -> 18, el tamaño del mapa de bits reutilizado debe ser el mismo. Por ejemplo, el tamaño de la imagen asignada a inBitmap es 100-100, luego el mapa de bits recién aplicado también debe ser 100-100 antes de que pueda reutilizarse.
A partir del SDK 19, el tamaño del mapa de bits recién aplicado debe ser menor o igual que el tamaño del mapa de bits que ya se ha asignado.
3. El mapa de bits recién aplicado y el mapa de bits antiguo deben tener el mismo formato de decodificación. Por ejemplo, todos son ARGB_8888. Si el mapa de bits anterior es ARGB_8888, los mapas de bits de los formatos ARGB_4444 y RGB_565 no se admiten, pero puede crear un mapa de bits que contenga más Un grupo de objetos de mapa de bits reutilizable típico, de modo que la creación posterior del mapa de bits pueda encontrar una "plantilla" adecuada para su reutilización.

La mejor manera aquí es usar LRUCache para almacenar en caché el mapa de bits.Cuando almacena en caché un nuevo mapa de bits más tarde, puede encontrar el mapa de bits más adecuado para reutilizarlo de la caché de acuerdo con la versión de la API para reutilizar su área de memoria.

tutorial oficial relacionado con mapas de bits de Google:
http://developer.android.com/training/displaying-bitmaps/manage-memory.html
http://developer.android.com/training/displaying-bitmaps/index.html

Los mapas de bits que requieren la reutilización de la memoria no pueden llamar a recycle () para recuperar memoria

Como se muestra abajo:

Inserte la descripción de la imagen aquí

El método entryRemoved se vuelve a llamar cuando LruCache elimina la imagen. En este método, debemos tratarlo por separado:

  1. Cuando se puede reutilizar (oldValue.isMutable () es para juzgar si se puede reutilizar), lo reutilizamos a través del grupo de reutilización
  2. Si no se puede reutilizar, llamaremos directamente a recycling () para reciclar

La imagen de arriba se procesa así, es decir, no importa cuál sea la situación, se llamará a recycling () para reciclar nuestro mapa de bits y liberar la memoria ocupada. Esto hará que luego saquemos el mapa de bits del grupo de reutilización para su reutilización y encontremos que la memoria del mapa de bits ha sido Se recicló, por lo que se informó un error.

Entonces, lo anterior debe cambiarse por lo siguiente:

            @Override
            protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
    
    
				if (oldValue.isMutable()) {
    
    
				    // < 3.0  bitmap 内存在 native
				    // >= 3.0 在 java
				    // >= 8.0 又变为 native
				    // 如果从内存缓存中移除,将其放入复用池
				    reusablePool.add(new WeakReference<Bitmap>(oldValue, getReferenceQueue()));
				} else {
    
    
				    oldValue.recycle();
				}
			}

Caché de memoria de mapa de bits

Utilice la clase de herramienta de caché LruCache: android.util.LruCache

lru (menos utilizado recientemente), es decir, el menos utilizado recientemente, la idea central es: es más probable que los datos utilizados recientemente se utilicen en el futuro.

Método de implementación:
use la lista vinculada + HashMap (es decir, LinkedHashMap). Cuando es necesario insertar un nuevo elemento de datos, si el nuevo elemento de datos existe en la lista vinculada (generalmente llamado un acierto), el nodo se mueve al encabezado de la lista vinculada; si no existe, se crea un nuevo nodo y se coloca al principio de la lista vinculada. Si la caché está llena, elimine el último nodo de la lista vinculada. Al acceder a los datos, si el elemento de datos existe en la lista vinculada, mueva el nodo al encabezado de la lista vinculada; de lo contrario, devuelva -1. De esta manera, el nodo al final de la lista vinculada es el elemento de datos al que no se ha accedido más recientemente.

Inserte la descripción de la imagen aquí

Caché de disco de mapa de bits

Utilice DiskLruCache
https://github.com/JakeWharton/DiskLruCache

Carga de imágenes largas de mapa de bits

使用 : android.graphics.BitmapRegionDecoder

Cómo utilizar:
InputStream is = getAssets (). Open ("big.png");

// La imagen larga se carga usando BigmapRegionDecoder
// verdadero: el flujo de entrada se comparte y el flujo de entrada no se usará si el flujo de entrada está cerrado, por lo que este lugar usa falso
BigmapRegionDecoder decoder = BigmapRegionDecoder.newInstance (es, falso);

// Obtener la imagen del área de Rect especificada, que es equivalente a tomar una captura de pantalla de la imagen grande
// Las opciones se establecen en nulo
Bitmap bitmap = decoder.decodeRegion (Rect, null);

Por ejemplo: Cómo cargar la imagen larga de la derecha en el teléfono.
Inserte la descripción de la imagen aquí
Si la pones directamente, el contenido no quedará claro:
Inserte la descripción de la imagen aquí

La solución correcta:
amplíe la imagen larga al mismo ancho que la pantalla y luego escale la longitud para mostrar un área de la imagen larga y luego mueva continuamente el área de la imagen larga al deslizarla.
Inserte la descripción de la imagen aquí

Referencia:
[Optimización de memoria de Android] Cálculo de uso de memoria de mapa de bits (análisis de uso de memoria de imagen de mapa de bits | Cálculo de uso de memoria de mapa de bits | Conversión de mapa de bits entre diferentes densidades de píxeles)
Optimización del rendimiento de Android: explicación detallada de mapa de bits y cuánta memoria ocupa su mapa de bits?
Carga de imágenes y optimización de memoria de mapa de
bits Fórmula de cálculo precisa para el tamaño de memoria ocupado por mapa de bits

Supongo que te gusta

Origin blog.csdn.net/yzpbright/article/details/109209028
Recomendado
Clasificación