Como carregar bitmaps grandes de maneira eficiente

Observação: várias bibliotecas seguem as práticas recomendadas para carregar imagens. Você pode usar essas bibliotecas no seu app para carregar imagens de maneira mais otimizada. Recomendamos a biblioteca Glide, que carrega e exibe imagens da forma mais rápida e simples possível. Outras bibliotecas populares de carregamento de imagens incluem Picasso, do Square, Coil, do Instacart e Fresco, do Facebook. Essas bibliotecas simplificam a maioria das tarefas complexas associadas a bitmaps e outros tipos de imagens no Android.

Existem imagens de todos os formatos e tamanhos. Em muitos casos, elas são maiores que o necessário para uma interface de usuário (IU) típica de um app. Por exemplo, o app do sistema Galeria exibe fotos tiradas com a câmera dos seus dispositivos Android, que costumam ter uma resolução muito maior que a densidade de tela do dispositivo.

Como você está trabalhando com memória limitada, o ideal é carregar somente uma versão de resolução mais baixa na memória. A versão de resolução mais baixa precisa corresponder ao tamanho do componente de IU que a exibe. Uma imagem com uma resolução mais alta não fornece benefícios visíveis, mas ainda ocupa um espaço precioso na memória e causa sobrecarga adicional de desempenho devido ao dimensionamento adicional em tempo real.

Esta lição orienta você durante a decodificação de bitmaps grandes sem exceder ao limite de memória por app ao carregar uma versão menor subamostrada na memória.

Ler dimensões e tipo de bitmap

A classe BitmapFactory fornece vários métodos de decodificação (decodeByteArray(), decodeFile(), decodeResource() etc.) para criar um Bitmap a partir de várias fontes. Escolha o método de decodificação mais apropriado com base na sua fonte de dados de imagem. Esses métodos tentam alocar memória para o bitmap construído e, portanto, podem facilmente resultar em uma exceção OutOfMemory. Cada tipo de método de decodificação tem assinaturas adicionais que permitem especificar opções de decodificação por meio da classe BitmapFactory.Options. Definir a propriedade inJustDecodeBounds como true durante a decodificação evita a alocação de memória, retornando null para o objeto bitmap, mas definindo outWidth, outHeight e outMimeType. Essa técnica permite que você leia as dimensões e o tipo dos dados de imagem antes da construção (e alocação de memória) do bitmap.

Kotlin

val options = BitmapFactory.Options().apply {
    inJustDecodeBounds = true
}
BitmapFactory.decodeResource(resources, R.id.myimage, options)
val imageHeight: Int = options.outHeight
val imageWidth: Int = options.outWidth
val imageType: String = options.outMimeType

Java

BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;

Para evitar exceções java.lang.OutOfMemory, verifique as dimensões de um bitmap antes de decodificá-lo, a menos que você confie totalmente na fonte para fornecer dados de imagem de tamanho previsível que se encaixam perfeitamente na memória disponível.

Carregar uma versão reduzida na memória

Agora que as dimensões de imagem são conhecidas, elas podem ser usadas para decidir se a imagem carregada na memória será completa ou subamostrada. Veja alguns fatores a serem considerados:

  • Estimativa de uso de memória ao carregar a imagem inteira.
  • Quantidade de memória que você pretende comprometer para carregar essa imagem, considerando quaisquer outros requisitos de memória do seu app.
  • Dimensões do ImageView ou componente de IU de destino em que a imagem será carregada.
  • Tamanho da tela e densidade do dispositivo atual.

Por exemplo, não vale a pena carregar uma imagem de 1024x768 pixels na memória se ela é exibida em miniatura de 128x96 pixels em ImageView.

Para solicitar que o decodificador faça uma subamostra da imagem, carregando uma versão menor na memória, defina inSampleSize como true no seu objeto BitmapFactory.Options. Por exemplo, uma imagem com resolução 2048x1536 decodificada com inSampleSize de 4 produz um bitmap de aproximadamente 512x384. Carregar isso na memória usa 0,75 MB em vez de 12MB para a imagem completa (supondo uma configuração de bitmap de ARGB_8888). Veja um método para calcular um valor de tamanho de amostra que é uma potência de dois com base na largura e na altura de um alvo:

Kotlin

fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
    // Raw height and width of image
    val (height: Int, width: Int) = options.run { outHeight to outWidth }
    var inSampleSize = 1

    if (height > reqHeight || width > reqWidth) {

        val halfHeight: Int = height / 2
        val halfWidth: Int = width / 2

        // Calculate the largest inSampleSize value that is a power of 2 and keeps both
        // height and width larger than the requested height and width.
        while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
            inSampleSize *= 2
        }
    }

    return inSampleSize
}

Java

public static int calculateInSampleSize(
            BitmapFactory.Options options, int reqWidth, int reqHeight) {
    // Raw height and width of image
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;

    if (height > reqHeight || width > reqWidth) {

        final int halfHeight = height / 2;
        final int halfWidth = width / 2;

        // Calculate the largest inSampleSize value that is a power of 2 and keeps both
        // height and width larger than the requested height and width.
        while ((halfHeight / inSampleSize) >= reqHeight
                && (halfWidth / inSampleSize) >= reqWidth) {
            inSampleSize *= 2;
        }
    }

    return inSampleSize;
}

Observação: uma potência de dois valores é calculada porque o decodificador usa um valor final arredondando para baixo para a potência mais próxima de dois, conforme a documentação de inSampleSize.

Para usar esse método, primeiro decodifique com inJustDecodeBounds definido como true, transmita as opções e decodifique novamente usando o novo valor inSampleSize e inJustDecodeBounds definido como false:

Kotlin

fun decodeSampledBitmapFromResource(
        res: Resources,
        resId: Int,
        reqWidth: Int,
        reqHeight: Int
): Bitmap {
    // First decode with inJustDecodeBounds=true to check dimensions
    return BitmapFactory.Options().run {
        inJustDecodeBounds = true
        BitmapFactory.decodeResource(res, resId, this)

        // Calculate inSampleSize
        inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight)

        // Decode bitmap with inSampleSize set
        inJustDecodeBounds = false

        BitmapFactory.decodeResource(res, resId, this)
    }
}

Java

public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
        int reqWidth, int reqHeight) {

    // First decode with inJustDecodeBounds=true to check dimensions
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);

    // Calculate inSampleSize
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

    // Decode bitmap with inSampleSize set
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}

Esse método facilita o carregamento de um bitmap de tamanho arbitrariamente grande em um ImageView que exibe uma miniatura de 100x100 pixels, conforme mostrado no seguinte código de exemplo:

Kotlin

imageView.setImageBitmap(
        decodeSampledBitmapFromResource(resources, R.id.myimage, 100, 100)
)

Java

imageView.setImageBitmap(
    decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));

É possível seguir um processo semelhante para decodificar bitmaps de outras fontes, substituindo o método BitmapFactory.decode* apropriado conforme necessário.