Chargement efficace de bitmaps volumineux

Remarque : Plusieurs bibliothèques suivent les bonnes pratiques de chargement d'images. Vous pouvez utiliser ces bibliothèques dans votre application pour charger des images de la manière la plus optimisée possible. Nous vous recommandons la bibliothèque Glide, qui permet de charger et d'afficher des images aussi rapidement et facilement que possible. Parmi les autres bibliothèques d'images populaires figurent Picasso de Square, Coil d'Instacart et Fresco de Facebook. Ces bibliothèques simplifient la plupart des tâches complexes associées aux bitmaps et aux autres types d'images sur Android.

Les images sont de toutes formes et de toutes tailles. Bien souvent, elles sont plus volumineuses que ce qui est nécessaire pour une interface utilisateur (UI) d'application standard. Par exemple, l'application système Galerie affiche les photos prises avec l'appareil photo de votre appareil Android, dont la résolution est généralement beaucoup plus haute que la densité d'écran de votre appareil.

Étant donné que vous travaillez avec une mémoire limitée, idéalement, vous ne devez charger qu'une version basse résolution en mémoire. La version basse résolution doit correspondre à la taille du composant d'interface utilisateur qui l'affiche. Une image avec une résolution plus élevée ne présente aucun avantage apparent, mais elle utilise beaucoup de mémoire et ralentit les performances en raison de mises à l'échelle ajoutées à la volée.

Cette leçon explique comment décoder des bitmaps volumineux sans dépasser la limite de mémoire par application en chargeant une plus petite version en sous-échantillon en mémoire.

Lire les dimensions et le type de bitmap

La classe BitmapFactory fournit plusieurs méthodes de décodage (decodeByteArray(), decodeFile(), decodeResource(), etc.) pour créer un Bitmap à partir de différentes sources. Choisissez la méthode de décodage la plus appropriée en fonction de votre source de données d'images. Ces méthodes tentent d'allouer de la mémoire pour le bitmap construit et peuvent donc facilement entraîner une exception OutOfMemory. Chaque type de méthode de décodage comporte des signatures supplémentaires qui vous permettent de spécifier des options de décodage via la classe BitmapFactory.Options. Définir la propriété inJustDecodeBounds sur true lors du décodage évite l'allocation de mémoire, en affichant null pour l'objet bitmap, mais en définissant outWidth, outHeight et outMimeType. Cette technique vous permet de lire les dimensions et le type des données d'image avant la construction (et l'allocation de mémoire) du 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;

Pour éviter les exceptions java.lang.OutOfMemory, vérifiez les dimensions d'un bitmap avant de le décoder, sauf si vous avez entièrement confiance en la source qui vous fournit des données d'image de taille prévisible, qui sont adaptées à la mémoire disponible.

Charger une version réduite dans la mémoire

Maintenant que vous connaissez les dimensions de l'image, vous pouvez décider si l'image entière doit être chargée en mémoire ou si une version sous-échantillonnée doit être chargée à la place. Voici quelques facteurs à prendre en compte :

  • Estimation de l'utilisation de la mémoire pour charger l'image entière en mémoire.
  • Quantité de mémoire que vous souhaitez consacrer au chargement de cette image en fonction des autres exigences de mémoire de votre application.
  • Dimensions de l'ImageView cible ou du composant d'interface utilisateur dans lequel l'image doit être chargée.
  • Taille et densité d'écran de l'appareil actuel.

Par exemple, il n'est pas intéressant de charger une image de 1 024 x 768 pixels dans la mémoire si elle s'affiche au final dans une vignette de 128 x 96 pixels dans une ImageView.

Pour indiquer au décodeur de sous-échantillonner l'image en chargeant une version plus petite dans la mémoire, définissez inSampleSize sur true dans votre objet BitmapFactory.Options. Par exemple, une image qui mesure 2 048 x 1 536 pixels décodée avec une inSampleSize de 4 génère un bitmap d'environ 512 x 384 pixels. Le chargement en mémoire utilise 0,75 Mo au lieu de 12 Mo pour l'image complète (à condition d'utiliser une configuration bitmap de ARGB_8888). Voici une méthode pour calculer un exemple de valeur de taille qui est une puissance de deux en fonction d'une largeur et d'une hauteur cibles :

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

Remarque : Une valeur est calculée via une puissance de deux, car le décodeur utilise une valeur finale en arrondissant à la puissance de deux la plus proche, conformément à la documentation inSampleSize.

Pour utiliser cette méthode, vous devez tout d'abord effectuer le décodage avec un inJustDecodeBounds défini sur true, transmettre les options, puis procéder à un nouveau décodage à l'aide de la nouvelle valeur inSampleSize et d'un inJustDecodeBounds défini sur 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);
}

Cette méthode permet de charger facilement un bitmap de taille arbitraire dans une ImageView affichant une vignette de 100 x 100 pixels, comme illustré dans l'exemple de code suivant :

Kotlin

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

Java

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

Vous pouvez adopter un processus similaire pour décoder des bitmaps provenant d'autres sources en utilisant la méthode BitmapFactory.decode* appropriée aux besoins particuliers.