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.