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.
Além das etapas descritas em Como armazenar bitmaps em cache,
você pode fazer coisas específicas para facilitar a coleta de lixo
e a reutilização de bitmaps. A estratégia recomendada depende da versão
do Android para que o app está sendo criado. O app de amostra BitmapFun
incluído
nesta classe mostra como projetar seu app para funcionar de maneira eficiente em
diferentes versões do Android.
Como introdução à lição, veja como o gerenciamento de memória de bitmaps no Android evoluiu:
- No Android 2.2 (API de nível 8) e em versões anteriores, as linhas de execução do app são interrompidas na coleta de lixo. Isso causa um atraso que pode prejudicar o desempenho. O Android 2.3 adiciona a coleta de lixo simultânea, o que significa que a memória é recuperada assim que um bitmap não é referenciado.
- No Android 2.3.3 (API de nível 10) e em versões anteriores, os dados de pixel de backup do bitmap são armazenados na memória nativa. Eles são separadas do bitmap em si, que é armazenado no heap Dalvik. Os dados de pixel da memória nativa não são liberados de maneira previsível, podendo fazer com que um app exceda brevemente aos limites de memória e falhe. Do Android 3.0 (API de nível 11) até o Android 7.1 (API de nível 25), os dados de pixel são armazenados no heap Dalvik com o bitmap associado. No Android 8.0 (API de nível 26) e em versões mais recentes, os dados de pixel do bitmap são armazenados no heap nativo.
As seções a seguir descrevem como otimizar o gerenciamento de memória de bitmap em diferentes versões do Android.
Gerenciar memória no Android 2.3.3 e em versões anteriores
No Android 2.3.3 (API de nível 10) e versões anteriores,
é recomendável usar recycle()
. Se você estiver exibindo grandes quantidades de dados de bitmap no seu app,
provavelmente encontrará
erros OutOfMemoryError
. O
método recycle()
permite que um app
recupere a memória o mais rápido possível.
Cuidado: você só deve usar
recycle()
quando tiver certeza de que o
bitmap não está mais sendo usado. Se você chamar recycle()
e depois tentar desenhar o bitmap, receberá o erro
"Canvas: trying to use a recycled bitmap"
.
O snippet de código a seguir dá um exemplo de chamada
recycle()
. Ele usa a contagem de referência
(nas variáveis mDisplayRefCount
e mCacheRefCount
) para rastrear
se um bitmap está sendo exibido ou está no cache. O
código recicla o bitmap quando as seguintes condições são atendidas:
- A contagem de referência para
mDisplayRefCount
emCacheRefCount
é 0. - O bitmap não é
null
e ainda não foi reciclado.
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(); }
Gerenciar memória no Android 3.0 e em versões mais recentes
O Android 3.0 (API de nível 11) introduz o
campo
BitmapFactory.Options.inBitmap
. Se essa opção estiver definida, os métodos de decodificação que usam o
objeto Options
tentarão reutilizar um bitmap existente ao carregar conteúdo. Isso significa
que a memória de bitmap é reutilizada, resultando em melhor desempenho e
removendo a alocação e a desalocação de memória. No entanto, existem algumas restrições de como
inBitmap
pode ser usado. Especificamente, apenas bitmaps de tamanhos iguais
são compatíveis com versões anteriores ao Android 4.4 (API de nível 19). Para ver detalhes, consulte a
documentação do inBitmap
.
Salvar um bitmap para uso posterior
O snippet a seguir demonstra como um bitmap já existente é armazenado para possível
uso posterior no app de exemplo. Quando um app é executado no Android 3.0 ou versões mais recentes, e
um bitmap é expulso de LruCache
,
uma referência simples ao bitmap é colocada
em HashSet
para possível reutilização posterior com
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())); } } } .... }
Usar um bitmap já existente
No app em execução, os métodos do decodificador verificam se há um bitmap que podem usar. Exemplo:
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); }
O snippet a seguir mostra o método addInBitmapOptions()
que é chamado no
snippet acima. Ele procura um bitmap já existente a ser definido como o valor para
inBitmap
. Observe que este
método só definirá um valor para inBitmap
se encontrar uma correspondência adequada (seu código nunca deve presumir que uma correspondência será encontrada):
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; }
Por fim, este método determina se um bitmap
candidato atende aos critérios de tamanho a serem usados para
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 = ceil((targetOptions.outWidth * 1.0f / targetOptions.inSampleSize).toDouble()).toInt() val height = ceil((targetOptions.outHeight * 1.0f / targetOptions.inSampleSize).toDouble()).toInt() 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 = (int) Math.ceil(targetOptions.outWidth * 1.0f / targetOptions.inSampleSize); int height = (int) Math.ceil(targetOptions.outHeight * 1.0f / 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; }