Gérer la mémoire bitmap

Remarque : Dans la plupart des cas, nous vous recommandons d'utiliser la bibliothèque Glide pour récupérer, décoder et afficher les bitmaps dans votre application. Glide enlève la plupart de la complexité liée à la gestion de ces tâches et d'autres tâches liées à l'utilisation de bitmaps et d'autres images sur Android. Pour plus d'informations sur l'utilisation et le téléchargement de Glide, consultez le dépôt Glide sur GitHub.

En plus des étapes décrites dans Mise en cache des bitmaps, vous pouvez faciliter la récupération de mémoire et la réutilisation des bitmaps en suivant certaines étapes spécifiques. La stratégie recommandée dépend de la ou des versions d'Android que vous ciblez. L'application exemple BitmapFun utilisée dans cette classe vous montre comment concevoir votre application pour qu'elle fonctionne efficacement avec différentes versions d'Android.

Pour introduire cette leçon, voici comment la gestion de la mémoire bitmap par Android a évolué :

  • Sur Android version 2.2 (niveau 8 d'API) ou antérieure, en cas de récupération de mémoire, les fils de threads de votre application sont arrêtés. Cela entraîne un délai pouvant nuire aux performances. Android 2.3 ajoute une récupération de mémoire simultanée, ce qui signifie que la mémoire est récupérée peu de temps après qu'un bitmap n'est plus référencé.
  • Sur Android version 2.3.3 (niveau 10 d'API) ou antérieure, les données de pixels de sauvegarde d'un bitmap sont stockées en mémoire native. Il est distinct du bitmap lui-même, qui est stocké dans le tas de mémoire Dalvik. Les données de pixels en mémoire native ne sont pas publiées de manière prévisible, ce qui peut entraîner le dépassement pendant une courte durée de la limite de mémoire et le plantage de l'application. D'Android 3.0 (niveau 11 d'API) à Android 7.1 (niveau 25 d'API), les données de pixels sont stockées sur le tas de mémoire Dalvik avec le bitmap associé. Sur Android version 8.0 (niveau 26 d'API) ou ultérieure, les données de pixels bitmap sont stockées sur le tas de mémoire natif.

Les sections suivantes décrivent comment optimiser la gestion de la mémoire bitmap sur différentes versions d'Android.

Gérer la mémoire sur Android version 2.3.3 ou antérieure

Sur Android version 2.3.3 (niveau 10 d'API) et antérieures, il est recommandé d'utiliser recycle(). Si vous affichez de grandes quantités de données bitmap dans votre application, vous risquez de rencontrer des erreurs OutOfMemoryError. La méthode recycle() permet à une application de récupérer de la mémoire dès que possible.

Attention : N'utilisez recycle() que lorsque vous êtes sûr que le bitmap n'est plus utilisé. Si vous appelez recycle(), puis tentez de tracer le bitmap, vous obtiendrez l'erreur suivante : "Canvas: trying to use a recycled bitmap".

L'extrait de code suivant donne un exemple d'appel de recycle(). Il utilise le comptage de référence (dans les variables mDisplayRefCount et mCacheRefCount) pour suivre si un bitmap est actuellement affiché ou se trouve dans le cache. Le code recycle le bitmap lorsque les conditions suivantes sont remplies :

  • Le nombre de références pour mDisplayRefCount et mCacheRefCount est 0.
  • Le bitmap n'est pas null et n'a pas encore été recyclé.

Kotlin

private var cacheRefCount: Int = 0
private var displayRefCount: Int = 0
...
// Notify the drawable that the displayed state has changed.
// Keep a count to determine when the drawable is no longer displayed.
fun setIsDisplayed(isDisplayed: Boolean) {
    synchronized(this) {
        if (isDisplayed) {
            displayRefCount++
            hasBeenDisplayed = true
        } else {
            displayRefCount--
        }
    }
    // Check to see if recycle() can be called.
    checkState()
}

// Notify the drawable that the cache state has changed.
// Keep a count to determine when the drawable is no longer being cached.
fun setIsCached(isCached: Boolean) {
    synchronized(this) {
        if (isCached) {
            cacheRefCount++
        } else {
            cacheRefCount--
        }
    }
    // Check to see if recycle() can be called.
    checkState()
}

@Synchronized
private fun checkState() {
    // If the drawable cache and display ref counts = 0, and this drawable
    // has been displayed, then recycle.
    if (cacheRefCount <= 0
            && displayRefCount <= 0
            && hasBeenDisplayed
            && hasValidBitmap()
    ) {
        getBitmap()?.recycle()
    }
}

@Synchronized
private fun hasValidBitmap(): Boolean =
        getBitmap()?.run {
            !isRecycled
        } ?: false

Java

private int cacheRefCount = 0;
private int displayRefCount = 0;
...
// Notify the drawable that the displayed state has changed.
// Keep a count to determine when the drawable is no longer displayed.
public void setIsDisplayed(boolean isDisplayed) {
    synchronized (this) {
        if (isDisplayed) {
            displayRefCount++;
            hasBeenDisplayed = true;
        } else {
            displayRefCount--;
        }
    }
    // Check to see if recycle() can be called.
    checkState();
}

// Notify the drawable that the cache state has changed.
// Keep a count to determine when the drawable is no longer being cached.
public void setIsCached(boolean isCached) {
    synchronized (this) {
        if (isCached) {
            cacheRefCount++;
        } else {
            cacheRefCount--;
        }
    }
    // Check to see if recycle() can be called.
    checkState();
}

private synchronized void checkState() {
    // If the drawable cache and display ref counts = 0, and this drawable
    // has been displayed, then recycle.
    if (cacheRefCount <= 0 && displayRefCount <= 0 && hasBeenDisplayed
            && hasValidBitmap()) {
        getBitmap().recycle();
    }
}

private synchronized boolean hasValidBitmap() {
    Bitmap bitmap = getBitmap();
    return bitmap != null && !bitmap.isRecycled();
}

Gérer la mémoire sous Android version 3.0 ou ultérieure

Android 3.0 (niveau 11 d'API) introduit le champ BitmapFactory.Options.inBitmap. Si cette option est définie, les méthodes de décodage qui prennent l'objet Options tentent de réutiliser un bitmap existant lors du chargement du contenu. Cela signifie que la mémoire du bitmap est réutilisée, ce qui améliore les performances et supprime à la fois l'allocation et la désallocation de mémoire. Cependant, l'utilisation de inBitmap est soumise à certaines restrictions. En particulier, avant Android 4.4 (niveau 19 d'API), seuls les bitmaps de taille égale sont acceptés. Pour en savoir plus, consultez les documents inBitmap.

Enregistrer un bitmap pour une utilisation ultérieure

L'extrait de code suivant montre comment stocker un bitmap existant en vue d'une utilisation ultérieure dans l'application exemple. Lorsqu'une application s'exécute sous Android version 3.0 ou ultérieure et qu'un bitmap est expulsé du LruCache, une référence légère au bitmap est placée dans un HashSet afin de pouvoir être réutilisée ultérieurement par inBitmap :

Kotlin

var reusableBitmaps: MutableSet<SoftReference<Bitmap>>? = null
private lateinit var memoryCache: LruCache<String, BitmapDrawable>
// If you're running on Honeycomb or newer, create a
// synchronized HashSet of references to reusable bitmaps.
if (Utils.hasHoneycomb()) {
    reusableBitmaps = Collections.synchronizedSet(HashSet<SoftReference<Bitmap>>())
}

memoryCache = object : LruCache<String, BitmapDrawable>(cacheParams.memCacheSize) {

    // Notify the removed entry that is no longer being cached.
    override fun entryRemoved(
            evicted: Boolean,
            key: String,
            oldValue: BitmapDrawable,
            newValue: BitmapDrawable
    ) {
        if (oldValue is RecyclingBitmapDrawable) {
            // The removed entry is a recycling drawable, so notify it
            // that it has been removed from the memory cache.
            oldValue.setIsCached(false)
        } else {
            // The removed entry is a standard BitmapDrawable.
            if (Utils.hasHoneycomb()) {
                // We're running on Honeycomb or later, so add the bitmap
                // to a SoftReference set for possible use with inBitmap later.
                reusableBitmaps?.add(SoftReference(oldValue.bitmap))
            }
        }
    }
}

Java

Set<SoftReference<Bitmap>> reusableBitmaps;
private LruCache<String, BitmapDrawable> memoryCache;

// If you're running on Honeycomb or newer, create a
// synchronized HashSet of references to reusable bitmaps.
if (Utils.hasHoneycomb()) {
    reusableBitmaps =
            Collections.synchronizedSet(new HashSet<SoftReference<Bitmap>>());
}

memoryCache = new LruCache<String, BitmapDrawable>(cacheParams.memCacheSize) {

    // Notify the removed entry that is no longer being cached.
    @Override
    protected void entryRemoved(boolean evicted, String key,
            BitmapDrawable oldValue, BitmapDrawable newValue) {
        if (RecyclingBitmapDrawable.class.isInstance(oldValue)) {
            // The removed entry is a recycling drawable, so notify it
            // that it has been removed from the memory cache.
            ((RecyclingBitmapDrawable) oldValue).setIsCached(false);
        } else {
            // The removed entry is a standard BitmapDrawable.
            if (Utils.hasHoneycomb()) {
                // We're running on Honeycomb or later, so add the bitmap
                // to a SoftReference set for possible use with inBitmap later.
                reusableBitmaps.add
                        (new SoftReference<Bitmap>(oldValue.getBitmap()));
            }
        }
    }
....
}

Utiliser un bitmap existant

Dans l'application en cours d'exécution, les méthodes de décodeur vérifient s'il existe un bitmap existant à utiliser. Par exemple :

Kotlin

fun decodeSampledBitmapFromFile(
        filename: String,
        reqWidth: Int,
        reqHeight: Int,
        cache: ImageCache
): Bitmap {

    val options: BitmapFactory.Options = BitmapFactory.Options()
    ...
    BitmapFactory.decodeFile(filename, options)
    ...

    // If we're running on Honeycomb or newer, try to use inBitmap.
    if (Utils.hasHoneycomb()) {
        addInBitmapOptions(options, cache)
    }
    ...
    return BitmapFactory.decodeFile(filename, options)
}

Java

public static Bitmap decodeSampledBitmapFromFile(String filename,
        int reqWidth, int reqHeight, ImageCache cache) {

    final BitmapFactory.Options options = new BitmapFactory.Options();
    ...
    BitmapFactory.decodeFile(filename, options);
    ...

    // If we're running on Honeycomb or newer, try to use inBitmap.
    if (Utils.hasHoneycomb()) {
        addInBitmapOptions(options, cache);
    }
    ...
    return BitmapFactory.decodeFile(filename, options);
}

L'extrait suivant montre la méthode addInBitmapOptions() appelée dans l'extrait ci-dessus. Elle recherche un bitmap existant à définir comme valeur pour inBitmap. Notez que cette méthode ne définit une valeur pour inBitmap que si elle trouve une correspondance appropriée (votre code ne doit jamais partir du principe qu'une correspondance sera trouvée) :

Kotlin

private fun addInBitmapOptions(options: BitmapFactory.Options, cache: ImageCache?) {
    // inBitmap only works with mutable bitmaps, so force the decoder to
    // return mutable bitmaps.
    options.inMutable = true

    // Try to find a bitmap to use for inBitmap.
    cache?.getBitmapFromReusableSet(options)?.also { inBitmap ->
        // If a suitable bitmap has been found, set it as the value of
        // inBitmap.
        options.inBitmap = inBitmap
    }
}

// This method iterates through the reusable bitmaps, looking for one
// to use for inBitmap:
fun getBitmapFromReusableSet(options: BitmapFactory.Options): Bitmap? {
    mReusableBitmaps?.takeIf { it.isNotEmpty() }?.let { reusableBitmaps ->
        synchronized(reusableBitmaps) {
            val iterator: MutableIterator<SoftReference<Bitmap>> = reusableBitmaps.iterator()
            while (iterator.hasNext()) {
                iterator.next().get()?.let { item ->
                    if (item.isMutable) {
                        // Check to see it the item can be used for inBitmap.
                        if (canUseForInBitmap(item, options)) {
                            // Remove from reusable set so it can't be used again.
                            iterator.remove()
                            return item
                        }
                    } else {
                        // Remove from the set if the reference has been cleared.
                        iterator.remove()
                    }
                }
            }
        }
    }
    return null
}

Java

private static void addInBitmapOptions(BitmapFactory.Options options,
        ImageCache cache) {
    // inBitmap only works with mutable bitmaps, so force the decoder to
    // return mutable bitmaps.
    options.inMutable = true;

    if (cache != null) {
        // Try to find a bitmap to use for inBitmap.
        Bitmap inBitmap = cache.getBitmapFromReusableSet(options);

        if (inBitmap != null) {
            // If a suitable bitmap has been found, set it as the value of
            // inBitmap.
            options.inBitmap = inBitmap;
        }
    }
}

// This method iterates through the reusable bitmaps, looking for one
// to use for inBitmap:
protected Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {
        Bitmap bitmap = null;

    if (reusableBitmaps != null && !reusableBitmaps.isEmpty()) {
        synchronized (reusableBitmaps) {
            final Iterator<SoftReference<Bitmap>> iterator
                    = reusableBitmaps.iterator();
            Bitmap item;

            while (iterator.hasNext()) {
                item = iterator.next().get();

                if (null != item && item.isMutable()) {
                    // Check to see it the item can be used for inBitmap.
                    if (canUseForInBitmap(item, options)) {
                        bitmap = item;

                        // Remove from reusable set so it can't be used again.
                        iterator.remove();
                        break;
                    }
                } else {
                    // Remove from the set if the reference has been cleared.
                    iterator.remove();
                }
            }
        }
    }
    return bitmap;
}

Enfin, cette méthode détermine si un bitmap candidat répond aux critères de taille à utiliser pour inBitmap :

Kotlin

private fun canUseForInBitmap(candidate: Bitmap, targetOptions: BitmapFactory.Options): Boolean {
    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        // From Android 4.4 (KitKat) onward we can re-use if the byte size of
        // the new bitmap is smaller than the reusable bitmap candidate
        // allocation byte count.
        val width: Int = targetOptions.outWidth / targetOptions.inSampleSize
        val height: Int = targetOptions.outHeight / targetOptions.inSampleSize
        val byteCount: Int = width * height * getBytesPerPixel(candidate.config)
        byteCount <= candidate.allocationByteCount
    } else {
        // On earlier versions, the dimensions must match exactly and the inSampleSize must be 1
        candidate.width == targetOptions.outWidth
                && candidate.height == targetOptions.outHeight
                && targetOptions.inSampleSize == 1
    }
}

/**
 * A helper function to return the byte usage per pixel of a bitmap based on its configuration.
 */
private fun getBytesPerPixel(config: Bitmap.Config): Int {
    return when (config) {
        Bitmap.Config.ARGB_8888 -> 4
        Bitmap.Config.RGB_565, Bitmap.Config.ARGB_4444 -> 2
        Bitmap.Config.ALPHA_8 -> 1
        else -> 1
    }
}

Java

static boolean canUseForInBitmap(
        Bitmap candidate, BitmapFactory.Options targetOptions) {

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
        // From Android 4.4 (KitKat) onward we can re-use if the byte size of
        // the new bitmap is smaller than the reusable bitmap candidate
        // allocation byte count.
        int width = targetOptions.outWidth / targetOptions.inSampleSize;
        int height = targetOptions.outHeight / targetOptions.inSampleSize;
        int byteCount = width * height * getBytesPerPixel(candidate.getConfig());
        return byteCount <= candidate.getAllocationByteCount();
    }

    // On earlier versions, the dimensions must match exactly and the inSampleSize must be 1
    return candidate.getWidth() == targetOptions.outWidth
            && candidate.getHeight() == targetOptions.outHeight
            && targetOptions.inSampleSize == 1;
}

/**
 * A helper function to return the byte usage per pixel of a bitmap based on its configuration.
 */
static int getBytesPerPixel(Config config) {
    if (config == Config.ARGB_8888) {
        return 4;
    } else if (config == Config.RGB_565) {
        return 2;
    } else if (config == Config.ARGB_4444) {
        return 2;
    } else if (config == Config.ALPHA_8) {
        return 1;
    }
    return 1;
}