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.

Il caricamento di una singola bitmap nell'interfaccia utente (UI) è semplice, ma le cose diventano di più complicata se devi caricare contemporaneamente un insieme più ampio 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, sempre che tu non ne conservi 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. Un ricordo e la cache su disco sono spesso utili in questo caso, consentendo 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 di memoria offre un accesso rapido alle bitmap al costo di consumare applicazioni preziose la memoria. 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 ad altissima densità (xhdpi) come Galaxy Nexus avrà bisogno di un 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 tal caso, potresti voler tenere sempre in memoria determinati elementi o persino avere più oggetti LruCache per diversi gruppi 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, questo valore è di almeno 4 MB (32/8). Un lo schermo GridView pieno di immagini su un dispositivo con una risoluzione 800 x 480 sarebbe 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;
    }
    ...
}

Utilizza una cache su disco

Una cache di memoria è utile per velocizzare l'accesso alle bitmap visualizzate di recente, ma non puoi dipende dalla disponibilità delle immagini in questa cache. Componenti come GridView con più grandi possono riempire facilmente una cache di memoria. La tua applicazione potrebbe essere interrotta da un altro un'attività come una telefonata e mentre in background potrebbe interrompersi e la memoria cache distrutte. Una volta che l'utente riprende, la tua applicazione deve elaborare nuovamente ogni immagine.

In questi casi è possibile utilizzare una cache su disco per mantenere le bitmap elaborate e ridurre il caricamento in cui le immagini non sono più disponibili in una cache di 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: ContentProvider potrebbe essere un valore maggiore posto appropriato in cui archiviare le immagini memorizzate nella cache se vi si accede più di frequente, ad esempio in una 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 in memoria viene controllata nel thread dell'interfaccia utente, la cache su disco viene controllata 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.

Modifiche alla configurazione degli handle

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 al nuovo di attività utilizzando un Fragment che viene conservato 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 in memoria vengono che si spera siano disponibili nella cache del disco; in caso contrario, vengono elaborati come di consueto.