2

I am using Fedor's code(https://github.com/thest1/LazyList) for loading bitmaps. I have modified a few lines of code, according to my requirement. I am getting out of memory error when ever the heap memory crosses the threshold. There are various questions posted on the same topic. Most of them suggesting, to use SoftReference and bitmap recycle(). I am using SoftReference but still I am facing problems. And also I am confused about where to use bitmap recycle method.

MemoryCache.java

public class MemoryCache {

private static final String TAG = "MemoryCache";
private static Map<String, SoftReference<Bitmap>> cache=Collections.synchronizedMap(
        new LinkedHashMap<String, SoftReference<Bitmap>>(16,0.75f,false));//Last argument true for LRU ordering
private long size=0;//current allocated size
private long limit=30000000;//max memory in bytes

public MemoryCache(){

    long cacheSize = Runtime.getRuntime().maxMemory();
    setLimit(cacheSize);
}

public void setLimit(long new_limit){
    limit=new_limit;

}

public Bitmap get(String id){
    try{
        if(!cache.containsKey(id))
            return null;
        //NullPointerException sometimes happen here http://code.google.com/p/osmdroid/issues/detail?id=78 
        return cache.get(id).get();
    }catch(NullPointerException ex){
        ex.printStackTrace();
        return null;
    }
}

public void put(String id, Bitmap bitmap){
    try{
        if(cache.containsKey(id))
            size-=getSizeInBytes(cache.get(id).get());
        cache.put(id, new SoftReference<Bitmap>(bitmap));
        size+=getSizeInBytes(bitmap);
        checkSize();
    }catch(Throwable th){
        th.printStackTrace();
    }
}

private void checkSize() {
    Log.i(TAG, "cache size="+size+" length="+cache.size());

    if(size>limit+5000000){
       cache.clear();
        }
        Log.i(TAG, "Clean cache. New size "+cache.size());

}

public void clear() {
    try{
        //NullPointerException sometimes happen here http://code.google.com/p/osmdroid/issues/detail?id=78 
        cache.clear();
        size=0;
    }catch(NullPointerException ex){
        ex.printStackTrace();
    }
}

long getSizeInBytes(Bitmap bitmap) {
    if(bitmap==null)
        return 0;
    return bitmap.getRowBytes() * bitmap.getHeight();
}
}

ImageLoader.java

public class ImageLoader {

MemoryCache memoryCache = new MemoryCache();
FileCache fileCache;
private Map<ImageView, String> imageViews = Collections
        .synchronizedMap(new WeakHashMap<ImageView, String>());
ExecutorService executorService;
Handler handler = new Handler();
Context con;
ProgressBar pb;

public ImageLoader(Context context) {
    fileCache = new FileCache(context);
    this.con = context;
    executorService = Executors.newFixedThreadPool(5);
}

final int stub_id = R.drawable.icon_loading;

public void DisplayImage(String url, ImageView imageView, ProgressBar pb) {
    this.pb = pb;

    imageViews.put(imageView, url);
    Bitmap bitmap = memoryCache.get(url);
    if (bitmap != null) {
        pb.setVisibility(View.GONE);
        imageView.setImageBitmap(bitmap);

    } else {
        queuePhoto(url, imageView);


    }
}

private void queuePhoto(String url, ImageView imageView) {
    PhotoToLoad p = new PhotoToLoad(url, imageView);
    executorService.submit(new PhotosLoader(p));
}

private Bitmap getBitmap(String url) {
    Bitmap result = null;

    File f = fileCache.getFile(url);

    // from SD cache
    Bitmap b = decodeFile(f);
    if (b != null)
        return b;

    // from web
    try {
        Bitmap bitmap = null;
        URL imageUrl = new URL(url);
        HttpURLConnection conn = (HttpURLConnection) imageUrl
                .openConnection();
        conn.setInstanceFollowRedirects(true);
        InputStream is = conn.getInputStream();
        OutputStream os = new FileOutputStream(f);
        Utils.CopyStream(is, os);
        os.close();
        is.close();
        conn.disconnect();
        bitmap = decodeFile(f);
        //Log.v("bitmap size", bitmap.getByteCount() + "");
        //bitmap.recycle();
        return bitmap;
    } catch (Throwable ex) {
        ex.printStackTrace();
        if (ex instanceof OutOfMemoryError)
            memoryCache.clear();
        return null;
    }


}



// decodes image and scales it to reduce memory consumption
private Bitmap decodeFile(File f) {
    try {


        BitmapFactory.Options o = new BitmapFactory.Options();
        o.inJustDecodeBounds = true;
        FileInputStream stream1 = new FileInputStream(f);
        BitmapFactory.decodeStream(stream1, null, o);
        stream1.close();


        final int REQUIRED_SIZE = 70;
        int width_tmp = o.outWidth, height_tmp = o.outHeight;
        int scale = 1;

        BitmapFactory.Options o2 = new BitmapFactory.Options();

        if (f.length() > 300000) {

            o2.inSampleSize = 4;
        } else if (f.length() > 200000) {

            o2.inSampleSize = 2;
        } else {

            o2.inSampleSize = 1;
        }
        FileInputStream stream2 = new FileInputStream(f);
        Bitmap bitmap = BitmapFactory.decodeStream(stream2, null, o2);
        stream2.close();
        return bitmap;
    } catch (FileNotFoundException e) {
    } catch (IOException e) {
        memoryCache.clear();
        e.printStackTrace();
    }
    return null;
}

// Task for the queue
private class PhotoToLoad {
    public String logo_url;
    public ImageView imageView;

    public PhotoToLoad(String u, ImageView i) {
        logo_url = u;
        imageView = i;
    }
}

class PhotosLoader implements Runnable {
    PhotoToLoad photoToLoad;

    PhotosLoader(PhotoToLoad photoToLoad) {
        this.photoToLoad = photoToLoad;
    }

    @Override
    public void run() {
        if (imageViewReused(photoToLoad))
            return;
        Bitmap bmp = getBitmap(photoToLoad.logo_url);
        // Log.v("bitmap size",bmp.getByteCount()+"");


        memoryCache.put(photoToLoad.logo_url, bmp);

        if (imageViewReused(photoToLoad))
            return;
        BitmapDisplayer bd = new BitmapDisplayer(bmp, photoToLoad);
        //bmp.recycle();
        // Activity a=(Activity)photoToLoad.imageView.getContext();
        // a.runOnUiThread(bd);
        handler.post(bd);
    }
}

boolean imageViewReused(PhotoToLoad photoToLoad) {
    String tag = imageViews.get(photoToLoad.imageView);
    if (tag == null || !tag.equals(photoToLoad.logo_url))
        return true;
    return false;
}

// Used to display bitmap in the UI thread
class BitmapDisplayer implements Runnable {
    Bitmap bitmap;
    PhotoToLoad photoToLoad;

    public BitmapDisplayer(Bitmap b, PhotoToLoad p) {
        bitmap = b;
        photoToLoad = p;
    }

    public void run() {
        if (imageViewReused(photoToLoad))
            return;
        if (bitmap != null) {
            //bitmap.recycle();
            // pb.setVisibility(View.GONE);
            photoToLoad.imageView.setImageBitmap(bitmap);
            //bitmap.recycle();
        } else {
            // photoToLoad.imageView.setImageResource(stub_id);
            // pb.setVisibility(View.VISIBLE);
        }
    }
}

public void clearCache() {
    memoryCache.clear();
    fileCache.clear();
}

}

Attached the Logcat output:

01-21 16:54:47.348: D/skia(20335): --- decoder->decode returned false
01-21 16:54:47.408: I/dalvikvm-heap(20335): Clamp target GC heap from 69.438MB to 64.000MB
01-21 16:54:47.408: D/dalvikvm(20335): GC_FOR_ALLOC freed 67K, 5% free 62767K/65416K, paused 54ms, total 54ms
01-21 16:54:47.408: I/dalvikvm-heap(20335): Forcing collection of SoftReferences for 228816-byte allocation
01-21 16:54:47.468: I/dalvikvm-heap(20335): Clamp target GC heap from 69.438MB to 64.000MB
01-21 16:54:47.468: D/dalvikvm(20335): GC_BEFORE_OOM freed <1K, 5% free 62767K/65416K, paused 64ms, total 64ms
01-21 16:54:47.468: E/dalvikvm-heap(20335): Out of memory on a 228816-byte allocation.
01-21 16:54:47.468: I/dalvikvm(20335): "pool-21-thread-4" prio=5 tid=63 RUNNABLE
01-21 16:54:47.468: I/dalvikvm(20335):   | group="main" sCount=0 dsCount=0 obj=0x42e4e878 self=0x67693b00
01-21 16:54:47.468: I/dalvikvm(20335):   | sysTid=20520 nice=0 sched=0/0 cgrp=apps handle=1735190240
01-21 16:54:47.468: I/dalvikvm(20335):   | state=R schedstat=( 2851815000 268321000 1461 ) utm=276 stm=9 core=0
01-21 16:54:47.468: I/dalvikvm(20335):   at android.graphics.BitmapFactory.nativeDecodeStream(Native Method)
01-21 16:54:47.468: I/dalvikvm(20335):   at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:529)
01-21 16:54:47.468: I/dalvikvm(20335):   at com.main.util.ImageLoader.decodeFile(ImageLoader.java:211)
01-21 16:54:47.468: I/dalvikvm(20335):   at com.main.util.ImageLoader.getBitmap(ImageLoader.java:85)
01-21 16:54:47.468: I/dalvikvm(20335):   at com.main.util.ImageLoader.access$1(ImageLoader.java:79)
01-21 16:54:47.468: I/dalvikvm(20335):   at com.main.util.ImageLoader$PhotosLoader.run(ImageLoader.java:244)
01-21 16:54:47.468: I/dalvikvm(20335):   at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:390)
01-21 16:54:47.468: I/dalvikvm(20335):   at java.util.concurrent.FutureTask.run(FutureTask.java:234)
01-21 16:54:47.468: I/dalvikvm(20335):   at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1080)
01-21 16:54:47.468: I/dalvikvm(20335):   at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:573)
01-21 16:54:47.468: I/dalvikvm(20335):   at java.lang.Thread.run(Thread.java:856)
intrepidkarthi
  • 3,104
  • 8
  • 42
  • 75

2 Answers2

6

Two (big) parts to my answer. The first is aimed more directly at your question, and the second part takes a step back as I share how I learned to implement my own solution that is aimed more at people running into this for the first time.

Don't use bitmap.recycle(), because you really shouldn't have to. While this clears the memory being used for that bitmap, you'll probably run into issues with the bitmap still being used somewhere.

You should also use WeakReference everywhere there's a possibility the object will hang onto a bitmap (loading Tasks, ImageViews, etc). From the documentation:

Weak references are useful for mappings that should have their entries removed automatically once they are not referenced any more (from outside). The difference between a SoftReference and a WeakReference is the point of time at which the decision is made to clear and enqueue the reference:
  • A SoftReference should be cleared and enqueued as late as possible, that is, in case the VM is in danger of running out of memory.
  • A WeakReference may be cleared and enqueued as soon as is known to be weakly-referenced.

Both should theoretically work, but we have a little problem: Java finalizers. They aren't guaranteed to run in time, and unfortunately, that's where our little friend Bitmap is clearing it's memory. If the bitmaps in question are created slowly enough, the GC probably has enough time to recognize our SoftReference or WeakReference object and clear it from memory, but in practice that's not the case.

The short of it is that it's extremely easy to outpace the Garbage Collector when working with objects that use finalizers like Bitmaps (I think some IO classes use them too). A WeakReference will help our timing problem a little bit better than a SoftReference. Yeah, it'd be nice if we could hold a bunch of images in memory for insane performance, but many Android devices simply don't have the memory to do this, and I've found that no matter how large the cache is you'll still run into this problem if you aren't clearing references as soon as humanly possible.

As far as your caching goes, the first change I'd make is to ditch your own memory cache class and just use the LruCache that's found in the Android compatibility library. Not that your cache has issues or anything, but it removes another point of headaches, it's already done for you, and you won't have to maintain it.

Otherwise, the biggest problem I see with what you have is that PhotoToLoad is holding a strong reference to an ImageView, but more of this whole class could use some tweaking.

A short but nicely-written blog post explaining a great method for holding references to correct ImageViews while downloading images can be found on Android's blog, Multithreading for Performance. You can also see this sort of practice in-use on Google's I/O app, whose source code is available. I expand on this a little in the second part.

Anyway, instead of trying to map the URLs being loaded to the ImageView it's intended for with a Collection as you're doing, following what's done on the blog post above is an elegant way to reference back to the ImageView in question while avoiding using a recycled ImageView by mistake. And of course, it's a good example of how the ImageViews are all weakly referenced, which means our Garbage Collector is allowed to free up that memory faster.

OK. Now the second part.

Before I continue on the issue more in general and get even more long-winded I'll say that you're on the right track, and that the rest of my answer probably treads on a lot of ground you already covered and know about, but I'm hoping it will also benefit someone newer at this so bear with me.

As you already know, this is a very common problem on Android with a fairly long explanation that's been covered before (shakes fist at finalizers). After banging my own head against the wall for hours on end, trying various implementations of loaders and cachers, watching the "heap growth/cleanup race" in logs endlessly, and profiling memory usage and tracing objects with various implementations until my eyes would bleed, a few things have become clear to me:

  1. If you find yourself trying to tell the GC when to fire, you're going down the wrong path.
  2. You're in for a world of pain if you try to call bitmap.recycle() on bitmaps that are used in the UI, such as ImageViews.
  3. A major reason this is such a headache is because there's way too much misinformation out there on this topic on how to solve the problem. So many of the tutorials or examples out there look good in theory, but in practice are absolute trash (confirmed by all the profiling and tracing I mentioned above). What a maze to navigate!

You have two options. The first is to use a well-known and tested library. The second is to learn the right way to accomplish this task and gain some insightful knowledge along the way. For some libraries you can do both options.

If you look at this question, you'll find a few libraries that will accomplish what you are trying to do. There's also a couple of great answers that point to very useful learning resources.

The route I took myself was the more difficult one, but I'm obsessed with understanding solutions and not simply just using them. If you want to go the same route (it's worth it), you should first follow Google's tutorial "Displaying Bitmaps Efficiently".

If that didn't take, or you want to study a solution used in practice by Google themselves, check out the utility classes that handle bitmap loading and caching in their I/O 2012 app. In particular, study the following classes:

And of course, study some of the Activities to see how they use these classes. Between the official Android tutorial and the I/O 2012 app, I was able to successfully roll my own to fit what I was doing more specifically and know what was actually happening. You can always study some of the libraries I mentioned in the question I linked to above as well to see a couple of slightly different takes.

Community
  • 1
  • 1
mkuech
  • 411
  • 1
  • 4
  • 13
  • I should also mention that the `Bitmap` cleanup issue is only relevant in Android versions prior to 3.0, because after 3.0 the memory is allocated inside the VM (rather than in native memory, thus the need for `finalize()`), and so the Garbage Collector can reclaim memory on time like with other Java objects. – mkuech Feb 04 '13 at 04:16
0

recycle the bitmap after you have used it. you can do it by

bitmap.recycle();
Nirav Tukadiya
  • 3,367
  • 1
  • 18
  • 36