Observação: na maioria dos casos, recomendamos que você use a biblioteca Glide para buscar, decodificar e exibir bitmaps no seu app. A Glide abstrai a maior parte da complexidade para processar essas e outras tarefas relacionadas ao trabalho com bitmaps e outras imagens no Android. Para saber mais sobre como usar e fazer o download do Glide, visite o repositório Glide no GitHub.
Carregar um único bitmap na sua interface do usuário (IU) é simples. No entanto, as coisas ficam mais
complicadas se você precisa carregar um conjunto maior de imagens de uma só vez. Em muitos casos (como em componentes como
ListView
, GridView
ou ViewPager
), o número total de imagens na tela, combinado com imagens que
podem em breve rolar para a tela, é essencialmente ilimitado.
Com componentes como esse, o uso de memória é mantido baixo por meio de reciclagem de exibições filhas à medida que saem da tela. O coletor de lixo também libera os bitmaps carregados e supõe que você não tenha mantido referências de longa duração. Tudo isso é muito bom, mas, para manter um carregamento de IU fluido e rápido, é melhor evitar o processamento contínuo dessas imagens toda vez que voltam à tela. Os caches de disco e memória podem ajudar com isso, permitindo que componentes recarreguem rapidamente as imagens processadas.
Esta lição mostra o uso de um cache de bitmap de memória e de disco para melhorar a capacidade de resposta e a fluidez da IU ao carregar vários bitmaps.
Usar um cache de memória
Um cache de memória oferece acesso rápido a bitmaps, mas ocupa um espaço valioso na memória
do app. A classe LruCache
(também disponível no Biblioteca de Suporte para uso
na API de nível 4) é particularmente adequada para a tarefa de armazenamento em cache de bitmaps, mantendo os objetos referenciados recentemente
em uma referência LinkedHashMap
e removendo o membro usado menos
recentemente antes que o cache exceda o tamanho designado.
Observação: no passado, uma implementação de cache de memória conhecida era um cache de bitmap
SoftReference
ou WeakReference
. No entanto,
isso não é recomendado. A partir do Android 2.3 (API de nível 9), o coletor de lixo é mais
agressivo na coleta de referências suaves/fracas, o que as torna bem ineficazes. Além disso,
antes do Android 3.0 (API de nível 11), os dados de backup de um bitmap eram armazenados na memória nativa, que
não é liberada de forma previsível e pode fazer com que um app exceda temporariamente
os limites da memória e falhe.
Para escolher um tamanho adequado para um LruCache
, é necessário considerar
vários fatores, por exemplo:
- Qual é a intensidade do resto da sua atividade e/ou do seu app em relação à memória?
- Quantas imagens aparecerão na tela ao mesmo tempo? Quantas precisarão estar disponíveis e prontas para serem exibidas?
- Qual é o tamanho e a densidade da tela do dispositivo? Um dispositivo com tela de densidade extra-alta (xhdpi), como o Galaxy Nexus, precisará de um cache maior para armazenar o mesmo número de imagens que um dispositivo como o Nexus S (hdpi).
- Quais são as dimensões e configurações dos bitmaps e, portanto, quanta memória cada um ocupa?
- Com que frequência as imagens serão acessadas? Algumas serão acessadas com mais frequência do que outras?
Nesse caso, talvez você queira manter determinados itens sempre na memória ou até ter vários objetos
LruCache
para diferentes grupos de bitmaps. - Você pode equilibrar a qualidade e a quantidade? Às vezes, pode ser mais útil armazenar um número maior de bitmaps de baixa qualidade, carregando uma versão de qualidade superior em outra tarefa em segundo plano.
Não existe tamanho ou fórmula que atenda a todos os apps. Cabe a você analisar o
uso e encontrar uma solução adequada. Um cache muito pequeno causa sobrecarga adicional sem
nenhum benefício; um cache muito grande pode mais uma vez causar exceções java.lang.OutOfMemory
e deixar pouca memória para o restante do seu app funcionar.
Veja um exemplo de como configurar um LruCache
para bitmaps:
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); }
Observação: nesse exemplo, um oitavo da memória do app está
alocado para nosso cache. Em um dispositivo normal/hdpi, esse é um mínimo de cerca de 4 MB (32/8). Uma tela
inteira GridView
preenchida com imagens em um dispositivo com resolução 800x480
usaria cerca de 1,5 MB (800*480*4 bytes), armazenando em cache um mínimo de cerca de 2,5 páginas de imagens na
memória.
Ao carregar um bitmap em ImageView
, o LruCache
é verificado primeiro. Se uma entrada for encontrada, ela será usada imediatamente para atualizar o ImageView
. Caso contrário, uma linha de execução em segundo plano será gerada para processar a imagem:
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); } }
O BitmapWorkerTask
também precisa ser
atualizado para adicionar entradas ao cache de memória:
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; } ... }
Usar um cache de disco
Um cache de memória é útil para acelerar o acesso a bitmaps Acesso recente. No entanto,
não espere que as imagens estejam disponíveis nesse cache. Componentes como GridView
com
conjuntos de dados maiores podem facilmente preencher um cache de memória. Seu app pode ser interrompido por outra
tarefa, como uma ligação telefônica, e, no segundo plano, ele pode ser interrompido e o cache de memória pode ser
destruído. Quando for retomado, seu app precisará processar cada imagem novamente.
Um cache de disco pode ser usado nesses casos para manter os bitmaps processados e ajudar a diminuir tempos de carregamento em que imagens não estão mais disponíveis em um cache de memória. Obviamente, buscar imagens no disco leva mais tempo do que carregá-las da memória e isso só deve ser feito em uma linha de execução em segundo plano, já que os tempos de leitura do disco podem ser imprevisíveis.
Observação: um ContentProvider
pode ser um local mais
apropriado para armazenar imagens em cache se elas forem acessadas com mais frequência, por exemplo, em um
app de galeria de imagens.
O código de exemplo desta classe usa uma implementação de DiskLruCache
extraída da
origem do Android.
Veja um código de exemplo atualizado que adiciona um cache de disco ao cache de memória existente:
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); }
Observação: até mesmo a inicialização do cache de disco precisa de operações de disco e, portanto, não deve ocorrer na linha de execução principal. No entanto, isso significa que há uma chance de o cache ser acessado antes da inicialização. Para resolver isso, um objeto de bloqueio na implementação acima garante que o app não seja lido do cache de disco até que o cache tenha sido inicializado.
Enquanto o cache de memória é verificado na linha de execução de interface, o cache em disco é verificado na linha de execução em segundo plano. Operações de disco nunca devem ocorrer na linha de execução de interface. Quando o processamento de imagem for concluído, o bitmap final será adicionado aos caches de disco e memória para uso posterior.
Processar mudanças de configuração
Alterações na configuração no momento da execução, como uma mudança na orientação da tela, fazem com que o Android destrua e reinicie a atividade com a nova configuração. Para saber mais sobre esse comportamento, consulte Como gerenciar alterações no momento da execução (link em inglês). Evitar o processamento de todas as suas imagens de novo garante uma experiência simples e rápida para o usuário durante uma mudança na configuração.
Felizmente, você tem um bom cache de memória de bitmaps criado na seção Usar um cache de memória. Esse cache pode ser transmitido para a nova
instância de atividade usando um Fragment
, que é preservado chamando setRetainInstance(true)
. Depois que a atividade for
recriada, o Fragment
retido será reanexado e você terá acesso ao
objeto de cache já existente, permitindo que as imagens sejam rapidamente buscadas e preenchidas novamente em objetos ImageView
.
Veja um exemplo de como reter um objeto LruCache
nas mudanças de
configuração usando um 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); } }
Para testar isso, tente girar um dispositivo com e sem Fragment
. Quando retiver o cache, você observará pouco ou nenhum atraso no preenchimento quase
instantâneo das imagens da memória. Espera-se que as imagens que não forem encontradas
no cache de memória estejam disponíveis no cache de disco. Se não estiverem, elas serão processadas como de costume.