2012-05-24 8 views
51

Sto utilizzando un LRUCache per memorizzare nella cache i bitmap che sono memorizzati nel file system. Ho creato la cache basandomi sugli esempi qui: http://developer.android.com/training/displaying-bitmaps/cache-bitmap.htmlQuando è necessario riciclare una bitmap utilizzando LRUCache?

Il problema è che sto vedendo spesso Arresto anomalo di OutOfMemory durante l'utilizzo dell'app. Credo che quando LRUCache elabora un'immagine per far spazio ad un'altra, la memoria non viene liberata.

ho aggiunto una chiamata a Bitmap.recycle() quando l'immagine viene sfrattato:

// use 1/8 of the available memory for this memory cache 
    final int cacheSize = 1024 * 1024 * memClass/8; 
       mImageCache = new LruCache<String, Bitmap>(cacheSize) { 
       @Override 
       protected int sizeOf(String key, Bitmap bitmap) { 
        return bitmap.getByteCount(); 
       } 

       @Override 
       protected void entryRemoved(boolean evicted, String key, Bitmap oldBitmap, Bitmap newBitmap) { 
        oldBitmap.recycle(); 
        oldBitmap = null; 
       } 
      }; 

questo risolve il crash, ma si traduce anche in immagini volte che non figurano in app (solo un nero spazio in cui dovrebbe essere l'immagine). Ogni volta che si verifica, vedo questo messaggio nel mio logcat: Cannot generate texture from bitmap.

Una rapida ricerca su google rivela che questo sta accadendo perché l'immagine che sta visualizzando è stata riciclata.

Quindi cosa sta succedendo qui? Perché le immagini riciclate sono ancora nel LRUCache se le sto riciclando solo dopo che sono state rimosse? Qual è l'alternativa all'implementazione di una cache? I documenti Android indicano chiaramente che LRUCache è la strada da percorrere, ma non menzionano la necessità di riciclare le bitmap o come farlo.

DELIBERATO: Nel caso in cui il suo utile a chiunque altro, la soluzione a questo problema come suggerito dalla risposta accettata è quella di NON fare quello che ho fatto l'esempio di codice di cui sopra (non riciclare le bitmap in la chiamata entryRemoved()).

Invece, quando hai finito con un ImageView (come onPause() in un'attività, o quando la vista è riciclato in un adattatore) verificare se il bitmap è ancora nella cache (ho aggiunto un metodo isImageInCache() per la cache classe) e, se non lo è, quindi riciclare la bitmap. Altrimenti, lascia stare. Ciò risolveva le mie eccezioni OutOfMemory e impediva il riciclaggio delle bitmap che erano ancora in uso.

+9

Come stai controllando se il bitmap è ancora nella cache ? – LuxuryMode

+0

È possibile verificare se l'immagine è memorizzata nella cache utilizzando ImageLoader come imageLoader.isCached (url, maxWidth, maxHeight); maxWidth e maxHeight possono essere 0 ... – 66CLSjY

risposta

38

Credo che quando LRUCache elimini un'immagine per far spazio ad un'altra, la memoria non viene liberata.

Non lo sarà, fino a quando lo Bitmap non viene riciclato o eliminato.

Una rapida ricerca su google rivela che questo sta accadendo perché l'immagine che sta visualizzando è stata riciclata.

Ecco perché non si dovrebbe riciclare lì.

Perché le immagini riciclate sono ancora nel LRUCache se le sto riciclando solo dopo che sono state rimosse?

Presumibilmente, non sono nello LRUCache. Si trovano in un ImageView o qualcos'altro che utilizza ancora lo Bitmap.

Qual è l'alternativa per l'implementazione di una cache?

Per amor di discussione, supponiamo che si sta utilizzando gli oggetti in BitmapImageView widget, come ad esempio in file di un ListView.

Quando si termina con un Bitmap (ad esempio, una riga in un ListView viene riciclata), si controlla se è ancora nella cache. Se lo è, lo lasci da solo. Se non lo è, è recycle() it.

La cache ti indica semplicemente quali oggetti Bitmap valgono. La cache non ha modo di sapere se lo Bitmap è ancora in uso da qualche parte.

BTW, se sei al livello API 11+, considera l'utilizzo di inBitmap. OutOMemoryErrors vengono attivati ​​quando un'assegnazione non può essere soddisfatta. Ultimo ho controllato, Android non ha un garbage collector compattante, quindi è possibile ottenere un OutOfMemoryError a causa della frammentazione (si vuole allocare qualcosa di più grande del più grande singolo blocco disponibile).

+2

Grazie per i commenti penetranti. La chiave era "La cache ti sta semplicemente permettendo di sapere su quali oggetti Bitmap valga la pena tenere. La cache non ha modo di sapere se Bitmap è ancora in uso da qualche parte." Stavo pensando a come LRUCache funziona in modo sbagliato. PS Amo i tuoi libri! – howettl

+1

@howettl: sono contento che ti piacciano! – CommonsWare

+7

@CommonsWare: che dire della situazione quando si controlla che l'elemento di ListView riciclato e la sua bitmap siano ancora in LRUCache? Eventualmente verrà rimosso da LRUCache ma poiché non è legato a nessun articolo, non verrà chiamato recycle(). – Digger

15

Di fronte lo stesso e grazie a @CommonsWare per la discussione. Pubblicare qui la soluzione completa in modo che aiuti più persone a venire qui per lo stesso problema. Le modifiche e i commenti sono benvenuti. Acclamazioni

When should I recycle a bitmap using LRUCache? 
  • Proprio quando il bitmap non è né nella cache e non sempre si fa riferimento da qualsiasi ImageView.

  • Per mantenere il conteggio dei riferimenti della bitmap, è necessario estendere la classe BitmapDrawable e aggiungere ad essi gli attributi di riferimento .

  • Questo campione Android ha la risposta esattamente. DisplayingBitmaps.zip

otterremo al dettaglio e il codice di seguito.

(don't recycle the bitmaps in the entryRemoved() call). 

Non esattamente.

  • Nella voce delegato rimosso verificare se Bitmap è ancora referenziato da qualsiasi ImageView. In caso contrario. Riciclalo lì da solo.

  • E viceversa, menzionato nella risposta accettata che quando la vista sta per essere riutilizzata o viene scaricata, controllare la sua bitmap (la bitmap precedente se la vista viene riutilizzata) è nella cache. Se è lì, lascialo da solo, altrimenti riciclalo.

  • La chiave qui è che è necessario verificare in entrambe le posizioni se è possibile riciclare o meno bitmap.

spiegherò il mio caso specifico in cui sto usando LRUCache di tenere bitmap per me. E visualizzandoli in ListView. E chiamando riciclare su bitmap quando non sono più in uso.

RecyclingBitmapDrawable.java e RecyclingImageView.java del campione di cui sopra sono i pezzi di nucleo dobbiamo qui. Stanno gestendo le cose magnificamente. Loro setIsCached e setIsDisplayed i metodi stanno facendo ciò di cui abbiamo bisogno.

Il codice può essere trovato nel link di esempio menzionato sopra. Ma anche la pubblicazione del codice completo del file nella parte inferiore della risposta nel caso in cui in futuro il collegamento si interrompe o venga modificato. È stata eseguita una piccola modifica per sovrascrivere setImageResource anche per verificare lo stato della bitmap precedente.

--- Qui va il codice per voi ---

Così il responsabile LRUCache dovrebbe essere simile a questa.

LruCacheManager.java

package com.example.cache; 

import android.os.Build; 
import android.support.v4.util.LruCache; 

public class LruCacheManager { 

    private LruCache<String, RecyclingBitmapDrawable> mMemoryCache; 

    private static LruCacheManager instance; 

    public static LruCacheManager getInstance() { 
     if(instance == null) { 
      instance = new LruCacheManager(); 
      instance.init(); 
     } 

     return instance; 
    } 

    private void init() { 

     // We are declaring a cache of 6Mb for our use. 
     // You need to calculate this on the basis of your need 
     mMemoryCache = new LruCache<String, RecyclingBitmapDrawable>(6 * 1024 * 1024) { 
      @Override 
      protected int sizeOf(String key, RecyclingBitmapDrawable bitmapDrawable) { 
       // The cache size will be measured in kilobytes rather than 
       // number of items. 
       if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) { 
        return bitmapDrawable.getBitmap().getByteCount() ; 
       } else { 
        return bitmapDrawable.getBitmap().getRowBytes() * bitmapDrawable.getBitmap().getHeight(); 
       } 
      } 

      @Override 
      protected void entryRemoved(boolean evicted, String key, RecyclingBitmapDrawable oldValue, RecyclingBitmapDrawable newValue) { 
       super.entryRemoved(evicted, key, oldValue, newValue); 
       oldValue.setIsCached(false); 
      } 
     }; 

    } 

    public void addBitmapToMemoryCache(String key, RecyclingBitmapDrawable bitmapDrawable) { 
     if (getBitmapFromMemCache(key) == null) { 
      // The removed entry is a recycling drawable, so notify it 
      // that it has been added into the memory cache 
      bitmapDrawable.setIsCached(true); 
      mMemoryCache.put(key, bitmapDrawable); 
     } 
    } 

    public RecyclingBitmapDrawable getBitmapFromMemCache(String key) { 
     return mMemoryCache.get(key); 
    } 

    public void clear() { 
     mMemoryCache.evictAll(); 
    } 
} 


E il vostro GetView() di ListView/adattatore GridView dovrebbe essere normale come al solito. Come quando si imposta una nuova immagine su ImageView usando il metodo setImageDrawable. Controlla internamente il conteggio dei riferimenti sulla bitmap precedente e chiamerà il riciclo su di esso internamente se non in lrucache.

@Override 
    public View getView(int position, View convertView, ViewGroup parent) { 
     RecyclingImageView imageView; 
     if (convertView == null) { // if it's not recycled, initialize some attributes 
      imageView = new RecyclingImageView(getActivity()); 
      imageView.setLayoutParams(new GridView.LayoutParams(
        GridView.LayoutParams.WRAP_CONTENT, 
        GridView.LayoutParams.WRAP_CONTENT)); 
      imageView.setScaleType(ImageView.ScaleType.FIT_CENTER); 
      imageView.setPadding(5, 5, 5, 5); 

     } else { 
      imageView = (RecyclingImageView) convertView; 
     } 

     MyDataObject dataItem = (MyDataObject) getItem(position); 
     RecyclingBitmapDrawable image = lruCacheManager.getBitmapFromMemCache(dataItem.getId()); 

     if(image != null) { 
      // This internally is checking reference count on previous bitmap it used. 
      imageView.setImageDrawable(image); 
     } else { 
      // You have to implement this method as per your code structure. 
      // But it basically doing is preparing bitmap in the background 
      // and adding that to LruCache. 
      // Also it is setting the empty view till bitmap gets loaded. 
      // once loaded it just need to call notifyDataSetChanged of adapter. 
      loadImage(dataItem.getId(), R.drawable.empty_view); 
     } 

     return imageView; 

    } 

Ecco il vostro RecyclingImageView.java

/* 
* Copyright (C) 2013 The Android Open Source Project 
* 
* Licensed under the Apache License, Version 2.0 (the "License"); 
* you may not use this file except in compliance with the License. 
* You may obtain a copy of the License at 
* 
*  http://www.apache.org/licenses/LICENSE-2.0 
* 
* Unless required by applicable law or agreed to in writing, software 
* distributed under the License is distributed on an "AS IS" BASIS, 
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
* See the License for the specific language governing permissions and 
* limitations under the License. 
*/ 

package com.example.cache; 

import android.content.Context; 
import android.graphics.drawable.Drawable; 
import android.graphics.drawable.LayerDrawable; 
import android.util.AttributeSet; 
import android.widget.ImageView; 


/** 
* Sub-class of ImageView which automatically notifies the drawable when it is 
* being displayed. 
*/ 
public class RecyclingImageView extends ImageView { 

    public RecyclingImageView(Context context) { 
     super(context); 
    } 

    public RecyclingImageView(Context context, AttributeSet attrs) { 
     super(context, attrs); 
    } 

    /** 
    * @see android.widget.ImageView#onDetachedFromWindow() 
    */ 
    @Override 
    protected void onDetachedFromWindow() { 
     // This has been detached from Window, so clear the drawable 
     setImageDrawable(null); 

     super.onDetachedFromWindow(); 
    } 

    /** 
    * @see android.widget.ImageView#setImageDrawable(android.graphics.drawable.Drawable) 
    */ 
    @Override 
    public void setImageDrawable(Drawable drawable) { 
     // Keep hold of previous Drawable 
     final Drawable previousDrawable = getDrawable(); 

     // Call super to set new Drawable 
     super.setImageDrawable(drawable); 

     // Notify new Drawable that it is being displayed 
     notifyDrawable(drawable, true); 

     // Notify old Drawable so it is no longer being displayed 
     notifyDrawable(previousDrawable, false); 
    } 

    /** 
    * @see android.widget.ImageView#setImageResource(android.graphics.drawable.Drawable) 
    */ 
    @Override 
    public void setImageResource(int resId) { 
     // Keep hold of previous Drawable 
     final Drawable previousDrawable = getDrawable(); 

     // Call super to set new Drawable 
     super.setImageResource(resId); 

     // Notify old Drawable so it is no longer being displayed 
     notifyDrawable(previousDrawable, false); 
    } 


    /** 
    * Notifies the drawable that it's displayed state has changed. 
    * 
    * @param drawable 
    * @param isDisplayed 
    */ 
    private static void notifyDrawable(Drawable drawable, final boolean isDisplayed) { 
     if (drawable instanceof RecyclingBitmapDrawable) { 
      // The drawable is a CountingBitmapDrawable, so notify it 
      ((RecyclingBitmapDrawable) drawable).setIsDisplayed(isDisplayed); 
     } else if (drawable instanceof LayerDrawable) { 
      // The drawable is a LayerDrawable, so recurse on each layer 
      LayerDrawable layerDrawable = (LayerDrawable) drawable; 
      for (int i = 0, z = layerDrawable.getNumberOfLayers(); i < z; i++) { 
       notifyDrawable(layerDrawable.getDrawable(i), isDisplayed); 
      } 
     } 
    } 

} 

Qui è la vostra RecyclingBitmapDrawable.java

/* 
* Copyright (C) 2013 The Android Open Source Project 
* 
* Licensed under the Apache License, Version 2.0 (the "License"); 
* you may not use this file except in compliance with the License. 
* You may obtain a copy of the License at 
* 
*  http://www.apache.org/licenses/LICENSE-2.0 
* 
* Unless required by applicable law or agreed to in writing, software 
* distributed under the License is distributed on an "AS IS" BASIS, 
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
* See the License for the specific language governing permissions and 
* limitations under the License. 
*/ 

package com.example.cache; 

import android.content.res.Resources; 
import android.graphics.Bitmap; 
import android.graphics.drawable.BitmapDrawable; 

import android.util.Log; 

/** 
* A BitmapDrawable that keeps track of whether it is being displayed or cached. 
* When the drawable is no longer being displayed or cached, 
* {@link android.graphics.Bitmap#recycle() recycle()} will be called on this drawable's bitmap. 
*/ 
public class RecyclingBitmapDrawable extends BitmapDrawable { 

    static final String TAG = "CountingBitmapDrawable"; 

    private int mCacheRefCount = 0; 
    private int mDisplayRefCount = 0; 

    private boolean mHasBeenDisplayed; 

    public RecyclingBitmapDrawable(Resources res, Bitmap bitmap) { 
     super(res, bitmap); 
    } 

    /** 
    * Notify the drawable that the displayed state has changed. Internally a 
    * count is kept so that the drawable knows when it is no longer being 
    * displayed. 
    * 
    * @param isDisplayed - Whether the drawable is being displayed or not 
    */ 
    public void setIsDisplayed(boolean isDisplayed) { 
     //BEGIN_INCLUDE(set_is_displayed) 
     synchronized (this) { 
      if (isDisplayed) { 
       mDisplayRefCount++; 
       mHasBeenDisplayed = true; 
      } else { 
       mDisplayRefCount--; 
      } 
     } 

     // Check to see if recycle() can be called 
     checkState(); 
     //END_INCLUDE(set_is_displayed) 
    } 

    /** 
    * Notify the drawable that the cache state has changed. Internally a count 
    * is kept so that the drawable knows when it is no longer being cached. 
    * 
    * @param isCached - Whether the drawable is being cached or not 
    */ 
    public void setIsCached(boolean isCached) { 
     //BEGIN_INCLUDE(set_is_cached) 
     synchronized (this) { 
      if (isCached) { 
       mCacheRefCount++; 
      } else { 
       mCacheRefCount--; 
      } 
     } 

     // Check to see if recycle() can be called 
     checkState(); 
     //END_INCLUDE(set_is_cached) 
    } 

    private synchronized void checkState() { 
     //BEGIN_INCLUDE(check_state) 
     // If the drawable cache and display ref counts = 0, and this drawable 
     // has been displayed, then recycle 
     if (mCacheRefCount <= 0 && mDisplayRefCount <= 0 && mHasBeenDisplayed 
       && hasValidBitmap()) { 

      Log.d(TAG, "No longer being used or cached so recycling. " 
         + toString()); 

     getBitmap().recycle(); 
    } 
     //END_INCLUDE(check_state) 
    } 

    private synchronized boolean hasValidBitmap() { 
     Bitmap bitmap = getBitmap(); 
     return bitmap != null && !bitmap.isRecycled(); 
    } 

} 
+0

Wow ho passato ore a cercare di capire come risolvere il mio OOME. Grazie per un'ottima spiegazione !! QUESTO FUNZIONA – galhe2

Problemi correlati