Memorizzazione nella cache delle bitmap

Nota: per la maggior parte dei casi, consigliamo che utilizzi Glide libreria per il recupero, la decodifica e la visualizzazione delle bitmap nella tua app. Glide astrae la maggior parte delle a una maggiore complessità nella gestione altre attività relative all'utilizzo di bitmap e altre immagini su Android. Per informazioni sull'utilizzo e sul download di Glide, visita il Repository Glide su GitHub.

Caricare una singola bitmap nell'interfaccia utente (UI) è semplice, ma le cose si complicano se devi caricare contemporaneamente un insieme più grande di immagini. In molti casi (ad esempio con come ListView, GridView o ViewPager), il numero totale di immagini sullo schermo combinato con le immagini che che a breve potrebbero scorrere sullo schermo sono praticamente illimitate.

L'utilizzo della memoria viene ridotto con componenti come questo riciclando le visualizzazioni secondarie durante lo spostamento fuori dallo schermo. Il garbage collector libera anche le bitmap caricate, a condizione che non vengano conservati riferimenti di lunga durata. È tutto a posto, ma per mantenere un'interfaccia utente fluida e che si carica rapidamente è consigliabile evitare l'elaborazione continua di queste immagini ogni volta che appaiono sullo schermo. Spesso può essere utile una cache della memoria e del disco, che consente ai componenti di ricaricare rapidamente le immagini elaborate.

Questa lezione illustra l'uso di una cache bitmap di memoria e disco per migliorare la reattività e fluidità dell'UI durante il caricamento di più bitmap.

Usa una cache di memoria

Una cache in memoria offre accesso rapido alle bitmap, a costo di occupare una preziosa memoria dell'applicazione. Il corso LruCache (disponibile anche nella Libreria di supporto per il riutilizzo al livello API 4) è particolarmente adatta all'attività di memorizzazione nella cache delle bitmap, mantenendo oggetti a cui viene fatto riferimento in un LinkedHashMap con riferimento efficace e la rimozione utilizzato di recente prima che la cache superi la dimensione designata.

Nota: in passato, un'implementazione molto diffusa della cache di memoria era Tuttavia, SoftReference o WeakReference di cache bitmap questa opzione è sconsigliata. A partire da Android 2.3 (livello API 9), il garbage collector è più aggressivi nella raccolta di riferimenti morbidi/deboli, il che li rende piuttosto inefficaci. Inoltre, prima di Android 3.0 (livello API 11), i dati di supporto di una bitmap venivano archiviati nella memoria nativa, non viene rilasciato in modo prevedibile, causando potenzialmente il superamento della soglia limiti di memoria e arresti anomali.

Sono diversi i fattori che determinano la scelta della taglia adatta per un LruCache. prendere in considerazione, ad esempio:

  • Quanto richiede la memoria per il resto dell'attività e/o dell'applicazione?
  • Quante immagini verranno visualizzate sullo schermo contemporaneamente? Quanti devono essere disponibili pronti per l'invio sullo schermo?
  • Quali sono le dimensioni e la densità dello schermo del dispositivo? Un dispositivo con schermo a densità molto elevata (xhdpi) come Galaxy Nexus richiede una cache più grande per contenere lo stesso numero di immagini in memoria rispetto a un dispositivo come Nexus S (hdpi).
  • Quali sono le dimensioni e la configurazione delle bitmap e, di conseguenza, quanta memoria impiegherà ciascuna. in funzione?
  • Con quale frequenza si accede alle immagini? Alcune saranno accessibili più spesso di altre? In questo caso, ti consigliamo di mantenere sempre in memoria determinati elementi o di avere più oggetti LruCache per gruppi diversi di bitmap.
  • Riesci a trovare un equilibrio tra qualità e quantità? A volte può essere più utile archiviare una il numero di bitmap di qualità inferiore, caricando potenzialmente una versione di qualità superiore in un'altra in background.

Non esiste una dimensione o una formula specifica adatta a tutte le applicazioni, spetta a te analizzare all'utilizzo e a trovare una soluzione adeguata. Una cache troppo piccola provoca un overhead aggiuntivo con nessun vantaggio, una cache troppo grande può causare ancora una volta java.lang.OutOfMemory eccezioni e di lasciare poca memoria con cui lavorare al resto dell'app.

Di seguito è riportato un esempio di configurazione di un'LruCache per le bitmap:

Kotlin

private lateinit var memoryCache: LruCache<String, Bitmap>

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    // Get max available VM memory, exceeding this amount will throw an
    // OutOfMemory exception. Stored in kilobytes as LruCache takes an
    // int in its constructor.
    val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()

    // Use 1/8th of the available memory for this memory cache.
    val cacheSize = maxMemory / 8

    memoryCache = object : LruCache<String, Bitmap>(cacheSize) {

        override fun sizeOf(key: String, bitmap: Bitmap): Int {
            // The cache size will be measured in kilobytes rather than
            // number of items.
            return bitmap.byteCount / 1024
        }
    }
    ...
}

Java

private LruCache<String, Bitmap> memoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    // Get max available VM memory, exceeding this amount will throw an
    // OutOfMemory exception. Stored in kilobytes as LruCache takes an
    // int in its constructor.
    final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

    // Use 1/8th of the available memory for this memory cache.
    final int cacheSize = maxMemory / 8;

    memoryCache = new LruCache<String, Bitmap>(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap bitmap) {
            // The cache size will be measured in kilobytes rather than
            // number of items.
            return bitmap.getByteCount() / 1024;
        }
    };
    ...
}

public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
    if (getBitmapFromMemCache(key) == null) {
        memoryCache.put(key, bitmap);
    }
}

public Bitmap getBitmapFromMemCache(String key) {
    return memoryCache.get(key);
}

Nota: in questo esempio, un ottavo della memoria dell'applicazione è allocati per la nostra cache. Su un dispositivo normale/hdpi, è necessario un minimo di circa 4 MB (32/8). Un lo schermo GridView pieno di immagini su un dispositivo con una risoluzione 800 x 480 verrebbe utilizzare circa 1,5 MB (800*480*4 byte), quindi questo permetterebbe di memorizzare nella cache un minimo di circa 2,5 pagine la memoria.

Quando carichi una bitmap in un ImageView, LruCache viene prima selezionata. Se viene trovata una voce, questa viene utilizzata immediatamente per aggiornare ImageView, altrimenti viene generato un thread in background per elaborare l'immagine:

Kotlin

fun loadBitmap(resId: Int, imageView: ImageView) {
    val imageKey: String = resId.toString()

    val bitmap: Bitmap? = getBitmapFromMemCache(imageKey)?.also {
        mImageView.setImageBitmap(it)
    } ?: run {
        mImageView.setImageResource(R.drawable.image_placeholder)
        val task = BitmapWorkerTask()
        task.execute(resId)
        null
    }
}

Java

public void loadBitmap(int resId, ImageView imageView) {
    final String imageKey = String.valueOf(resId);

    final Bitmap bitmap = getBitmapFromMemCache(imageKey);
    if (bitmap != null) {
        mImageView.setImageBitmap(bitmap);
    } else {
        mImageView.setImageResource(R.drawable.image_placeholder);
        BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
        task.execute(resId);
    }
}

Inoltre, la BitmapWorkerTask deve essere aggiornato per aggiungere voci alla cache in memoria:

Kotlin

private inner class BitmapWorkerTask : AsyncTask<Int, Unit, Bitmap>() {
    ...
    // Decode image in background.
    override fun doInBackground(vararg params: Int?): Bitmap? {
        return params[0]?.let { imageId ->
            decodeSampledBitmapFromResource(resources, imageId, 100, 100)?.also { bitmap ->
                addBitmapToMemoryCache(imageId.toString(), bitmap)
            }
        }
    }
    ...
}

Java

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    ...
    // Decode image in background.
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final Bitmap bitmap = decodeSampledBitmapFromResource(
                getResources(), params[0], 100, 100));
        addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
        return bitmap;
    }
    ...
}

Utilizzare una cache su disco

Una cache della memoria è utile per velocizzare l'accesso ai bitmap visualizzati di recente, ma non puoi fare affidamento sulla disponibilità delle immagini in questa cache. Componenti come GridView con più grandi possono riempire facilmente una cache di memoria. L'applicazione potrebbe essere interrotta da un'altra attività, ad esempio una chiamata, e mentre è in background potrebbe essere interrotta e la cache della memoria distrutta. Una volta che l'utente riprende, la tua applicazione deve elaborare nuovamente ogni immagine.

In questi casi è possibile utilizzare una cache del disco per mantenere i bitmap elaborati e contribuire a ridurre i tempi di caricamento quando le immagini non sono più disponibili in una cache della memoria. Naturalmente, il recupero delle immagini dal disco è più lento rispetto al caricamento dalla memoria e dovrebbe essere eseguito in un thread in background, poiché i tempi di lettura del disco possono sono imprevedibili.

Nota: un ContentProvider potrebbe essere un luogo più appropriato per memorizzare le immagini memorizzate nella cache se vengono accedute più di frequente, ad esempio in un'applicazione di galleria di immagini.

Il codice di esempio di questa classe utilizza un'implementazione DiskLruCache estratta dal Origine Android. Di seguito è riportato un esempio di codice aggiornato che aggiunge una cache su disco oltre a quella esistente:

Kotlin

private const val DISK_CACHE_SIZE = 1024 * 1024 * 10 // 10MB
private const val DISK_CACHE_SUBDIR = "thumbnails"
...
private var diskLruCache: DiskLruCache? = null
private val diskCacheLock = ReentrantLock()
private val diskCacheLockCondition: Condition = diskCacheLock.newCondition()
private var diskCacheStarting = true

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    // Initialize memory cache
    ...
    // Initialize disk cache on background thread
    val cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR)
    InitDiskCacheTask().execute(cacheDir)
    ...
}

internal inner class InitDiskCacheTask : AsyncTask<File, Void, Void>() {
    override fun doInBackground(vararg params: File): Void? {
        diskCacheLock.withLock {
            val cacheDir = params[0]
            diskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE)
            diskCacheStarting = false // Finished initialization
            diskCacheLockCondition.signalAll() // Wake any waiting threads
        }
        return null
    }
}

internal inner class  BitmapWorkerTask : AsyncTask<Int, Unit, Bitmap>() {
    ...

    // Decode image in background.
    override fun doInBackground(vararg params: Int?): Bitmap? {
        val imageKey = params[0].toString()

        // Check disk cache in background thread
        return getBitmapFromDiskCache(imageKey) ?:
                // Not found in disk cache
                decodeSampledBitmapFromResource(resources, params[0], 100, 100)
                        ?.also {
                            // Add final bitmap to caches
                            addBitmapToCache(imageKey, it)
                        }
    }
}

fun addBitmapToCache(key: String, bitmap: Bitmap) {
    // Add to memory cache as before
    if (getBitmapFromMemCache(key) == null) {
        memoryCache.put(key, bitmap)
    }

    // Also add to disk cache
    synchronized(diskCacheLock) {
        diskLruCache?.apply {
            if (!containsKey(key)) {
                put(key, bitmap)
            }
        }
    }
}

fun getBitmapFromDiskCache(key: String): Bitmap? =
        diskCacheLock.withLock {
            // Wait while disk cache is started from background thread
            while (diskCacheStarting) {
                try {
                    diskCacheLockCondition.await()
                } catch (e: InterruptedException) {
                }

            }
            return diskLruCache?.get(key)
        }

// Creates a unique subdirectory of the designated app cache directory. Tries to use external
// but if not mounted, falls back on internal storage.
fun getDiskCacheDir(context: Context, uniqueName: String): File {
    // Check if media is mounted or storage is built-in, if so, try and use external cache dir
    // otherwise use internal cache dir
    val cachePath =
            if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState()
                    || !isExternalStorageRemovable()) {
                context.externalCacheDir.path
            } else {
                context.cacheDir.path
            }

    return File(cachePath + File.separator + uniqueName)
}

Java

private DiskLruCache diskLruCache;
private final Object diskCacheLock = new Object();
private boolean diskCacheStarting = true;
private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
private static final String DISK_CACHE_SUBDIR = "thumbnails";

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    // Initialize memory cache
    ...
    // Initialize disk cache on background thread
    File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR);
    new InitDiskCacheTask().execute(cacheDir);
    ...
}

class InitDiskCacheTask extends AsyncTask<File, Void, Void> {
    @Override
    protected Void doInBackground(File... params) {
        synchronized (diskCacheLock) {
            File cacheDir = params[0];
            diskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);
            diskCacheStarting = false; // Finished initialization
            diskCacheLock.notifyAll(); // Wake any waiting threads
        }
        return null;
    }
}

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    ...
    // Decode image in background.
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final String imageKey = String.valueOf(params[0]);

        // Check disk cache in background thread
        Bitmap bitmap = getBitmapFromDiskCache(imageKey);

        if (bitmap == null) { // Not found in disk cache
            // Process as normal
            final Bitmap bitmap = decodeSampledBitmapFromResource(
                    getResources(), params[0], 100, 100));
        }

        // Add final bitmap to caches
        addBitmapToCache(imageKey, bitmap);

        return bitmap;
    }
    ...
}

public void addBitmapToCache(String key, Bitmap bitmap) {
    // Add to memory cache as before
    if (getBitmapFromMemCache(key) == null) {
        memoryCache.put(key, bitmap);
    }

    // Also add to disk cache
    synchronized (diskCacheLock) {
        if (diskLruCache != null && diskLruCache.get(key) == null) {
            diskLruCache.put(key, bitmap);
        }
    }
}

public Bitmap getBitmapFromDiskCache(String key) {
    synchronized (diskCacheLock) {
        // Wait while disk cache is started from background thread
        while (diskCacheStarting) {
            try {
                diskCacheLock.wait();
            } catch (InterruptedException e) {}
        }
        if (diskLruCache != null) {
            return diskLruCache.get(key);
        }
    }
    return null;
}

// Creates a unique subdirectory of the designated app cache directory. Tries to use external
// but if not mounted, falls back on internal storage.
public static File getDiskCacheDir(Context context, String uniqueName) {
    // Check if media is mounted or storage is built-in, if so, try and use external cache dir
    // otherwise use internal cache dir
    final String cachePath =
            Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
                    !isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
                            context.getCacheDir().getPath();

    return new File(cachePath + File.separator + uniqueName);
}

Nota:anche l'inizializzazione della cache su disco richiede operazioni su disco e di conseguenza non devono svolgersi sul thread principale. Tuttavia, c'è la possibilità accedi alla cache prima dell'inizializzazione. Per risolvere questo problema, nell'implementazione precedente, un blocco assicura che l'app non legga dalla cache su disco finché la cache inizializzato.

Mentre la cache della memoria viene controllata nel thread dell'interfaccia utente, la cache del disco viene controllata nel thread in background. Le operazioni sul disco non dovrebbero mai avvenire sul thread dell'interfaccia utente. Quando l'elaborazione delle immagini viene completata, la bitmap finale viene aggiunta alla cache di memoria e su disco per un uso futuro.

Gestire le modifiche alla configurazione

Le modifiche alla configurazione del runtime, ad esempio una modifica dell'orientamento dello schermo, causano l'eliminazione e l'eliminazione di Android riavvia l'attività in esecuzione con la nuova configurazione. Per ulteriori informazioni su questo comportamento, consulta Gestione delle modifiche di runtime). Vuoi evitare di dover elaborare nuovamente tutte le immagini, in modo che l'utente abbia una velocità quando si apporta una modifica alla configurazione.

Fortunatamente, disponi di una buona cache di memoria di bitmap che hai creato nella sezione Utilizza una cache di memoria. Questa cache può essere trasmessa alla nuova istanza di attività utilizzando un Fragment che viene preservato chiamando setRetainInstance(true). Dopo che l'attività è stata ricreato, il Fragment conservato viene ricollegato e puoi accedere esistente, che consente di recuperare e compilare rapidamente le immagini negli oggetti ImageView.

Ecco un esempio di conservazione di un oggetto LruCache in tutta la configurazione modifiche utilizzando un Fragment:

Kotlin

private const val TAG = "RetainFragment"
...
private lateinit var mMemoryCache: LruCache<String, Bitmap>

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    val retainFragment = RetainFragment.findOrCreateRetainFragment(supportFragmentManager)
    mMemoryCache = retainFragment.retainedCache ?: run {
        LruCache<String, Bitmap>(cacheSize).also { memoryCache ->
            ... // Initialize cache here as usual
            retainFragment.retainedCache = memoryCache
        }
    }
    ...
}

class RetainFragment : Fragment() {
    var retainedCache: LruCache<String, Bitmap>? = null

    companion object {
        fun findOrCreateRetainFragment(fm: FragmentManager): RetainFragment {
            return (fm.findFragmentByTag(TAG) as? RetainFragment) ?: run {
                RetainFragment().also {
                    fm.beginTransaction().add(it, TAG).commit()
                }
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        retainInstance = true
    }
}

Java

private LruCache<String, Bitmap> memoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    RetainFragment retainFragment =
            RetainFragment.findOrCreateRetainFragment(getFragmentManager());
    memoryCache = retainFragment.retainedCache;
    if (memoryCache == null) {
        memoryCache = new LruCache<String, Bitmap>(cacheSize) {
            ... // Initialize cache here as usual
        }
        retainFragment.retainedCache = memoryCache;
    }
    ...
}

class RetainFragment extends Fragment {
    private static final String TAG = "RetainFragment";
    public LruCache<String, Bitmap> retainedCache;

    public RetainFragment() {}

    public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
        RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);
        if (fragment == null) {
            fragment = new RetainFragment();
            fm.beginTransaction().add(fragment, TAG).commit();
        }
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
    }
}

Per verificarlo, prova a ruotare un dispositivo con e senza conservare Fragment. Dovresti notare un ritardo minimo o nullo man mano che le immagini completano l'attività quasi. all'istante dalla memoria quando conserva la cache. Le immagini non trovate nella cache della memoria dovrebbero essere disponibili nella cache del disco, altrimenti vengono elaborate come di consueto.