4

I found Fedor's code here and implemented it into my project. The only difference is that my application does not have a list view, rather, I am accessing 1 image at a time from the server. When the activity launches, i call "DisplayImage(...)" to show the first picture. Then there are 2 buttons (previous/next) that when clicked, they call "DisplayImage(...)".

It works fine for a little while, but then I get an Out of Memory error. At the top of his code, he comments that you may want to use SoftReference. I am assuming that would fix my problem, right? I played around with it a bit but when I tried modifying it to use SoftReference, the images never load. I have never used SoftReference before so I figure I'm just missing something. How would I modify that code (ImageLoader) to fix my OOM error? Is there a better way of caching the pictures as you browse them?

UPDATE: Here is the code in case you don't want to view the other files in the source.

package com.fedorvlasov.lazylist;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.HashMap;
import java.util.Stack;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.widget.ImageView;

public class ImageLoader {

    //the simplest in-memory cache implementation. This should be replaced with something like SoftReference or BitmapOptions.inPurgeable(since 1.6)
    private HashMap<String, Bitmap> cache=new HashMap<String, Bitmap>();

    private File cacheDir;

    public ImageLoader(Context context){
        //Make the background thead low priority. This way it will not affect the UI performance
        photoLoaderThread.setPriority(Thread.NORM_PRIORITY-1);

        //Find the dir to save cached images
        if (android.os.Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED))
            cacheDir=new File(android.os.Environment.getExternalStorageDirectory(),"LazyList");
        else
            cacheDir=context.getCacheDir();
        if(!cacheDir.exists())
            cacheDir.mkdirs();
    }

    final int stub_id=R.drawable.stub;
    public void DisplayImage(String url, Activity activity, ImageView imageView)
    {
        if(cache.containsKey(url))
            imageView.setImageBitmap(cache.get(url));
        else
        {
            queuePhoto(url, activity, imageView);
            imageView.setImageResource(stub_id);
        }    
    }

    private void queuePhoto(String url, Activity activity, ImageView imageView)
    {
        //This ImageView may be used for other images before. So there may be some old tasks in the queue. We need to discard them. 
        photosQueue.Clean(imageView);
        PhotoToLoad p=new PhotoToLoad(url, imageView);
        synchronized(photosQueue.photosToLoad){
            photosQueue.photosToLoad.push(p);
            photosQueue.photosToLoad.notifyAll();
        }

        //start thread if it's not started yet
        if(photoLoaderThread.getState()==Thread.State.NEW)
            photoLoaderThread.start();
    }

    private Bitmap getBitmap(String url) 
    {
        //I identify images by hashcode. Not a perfect solution, good for the demo.
        String filename=String.valueOf(url.hashCode());
        File f=new File(cacheDir, filename);

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

        //from web
        try {
            Bitmap bitmap=null;
            InputStream is=new URL(url).openStream();
            OutputStream os = new FileOutputStream(f);
            Utils.CopyStream(is, os);
            os.close();
            bitmap = decodeFile(f);
            return bitmap;
        } catch (Exception ex){
           ex.printStackTrace();
           return null;
        }
    }

    //decodes image and scales it to reduce memory consumption
    private Bitmap decodeFile(File f){
        try {
            //decode image size
            BitmapFactory.Options o = new BitmapFactory.Options();
            o.inJustDecodeBounds = true;
            BitmapFactory.decodeStream(new FileInputStream(f),null,o);

            //Find the correct scale value. It should be the power of 2.
            final int REQUIRED_SIZE=70;
            int width_tmp=o.outWidth, height_tmp=o.outHeight;
            int scale=1;
            while(true){
                if(width_tmp/2<REQUIRED_SIZE || height_tmp/2<REQUIRED_SIZE)
                    break;
                width_tmp/=2;
                height_tmp/=2;
                scale*=2;
            }

            //decode with inSampleSize
            BitmapFactory.Options o2 = new BitmapFactory.Options();
            o2.inSampleSize=scale;
            return BitmapFactory.decodeStream(new FileInputStream(f), null, o2);
        } catch (FileNotFoundException e) {}
        return null;
    }

    //Task for the queue
    private class PhotoToLoad
    {
        public String url;
        public ImageView imageView;
        public PhotoToLoad(String u, ImageView i){
            url=u; 
            imageView=i;
        }
    }

    PhotosQueue photosQueue=new PhotosQueue();

    public void stopThread()
    {
        photoLoaderThread.interrupt();
    }

    //stores list of photos to download
    class PhotosQueue
    {
        private Stack<PhotoToLoad> photosToLoad=new Stack<PhotoToLoad>();

        //removes all instances of this ImageView
        public void Clean(ImageView image)
        {
            for(int j=0 ;j<photosToLoad.size();){
                if(photosToLoad.get(j).imageView==image)
                    photosToLoad.remove(j);
                else
                    ++j;
            }
        }
    }

    class PhotosLoader extends Thread {
        public void run() {
            try {
                while(true)
                {
                    //thread waits until there are any images to load in the queue
                    if(photosQueue.photosToLoad.size()==0)
                        synchronized(photosQueue.photosToLoad){
                            photosQueue.photosToLoad.wait();
                        }
                    if(photosQueue.photosToLoad.size()!=0)
                    {
                        PhotoToLoad photoToLoad;
                        synchronized(photosQueue.photosToLoad){
                            photoToLoad=photosQueue.photosToLoad.pop();
                        }
                        Bitmap bmp=getBitmap(photoToLoad.url);
                        cache.put(photoToLoad.url, bmp);
                        Object tag=photoToLoad.imageView.getTag();
                        if(tag!=null && ((String)tag).equals(photoToLoad.url)){
                            BitmapDisplayer bd=new BitmapDisplayer(bmp, photoToLoad.imageView);
                            Activity a=(Activity)photoToLoad.imageView.getContext();
                            a.runOnUiThread(bd);
                        }
                    }
                    if(Thread.interrupted())
                        break;
                }
            } catch (InterruptedException e) {
                //allow thread to exit
            }
        }
    }

    PhotosLoader photoLoaderThread=new PhotosLoader();

    //Used to display bitmap in the UI thread
    class BitmapDisplayer implements Runnable
    {
        Bitmap bitmap;
        ImageView imageView;
        public BitmapDisplayer(Bitmap b, ImageView i){bitmap=b;imageView=i;}
        public void run()
        {
            if(bitmap!=null)
                imageView.setImageBitmap(bitmap);
            else
                imageView.setImageResource(stub_id);
        }
    }

    public void clearCache() {
        //clear memory cache
        cache.clear();

        //clear SD cache
        File[] files=cacheDir.listFiles();
        for(File f:files)
            f.delete();
    }

}

Here is the same class after I tried to implement SoftReference. I don't think I did it right because this does not display the picture on the screen after loading.

package com.mycompany.myapp;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.ref.SoftReference;
import java.net.URL;
import java.util.HashMap;
import java.util.Stack;

import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.widget.ImageView;

public class ImageLoader {

    //the simplest in-memory cache implementation. This should be replaced with something like SoftReference or BitmapOptions.inPurgeable(since 1.6)
    private HashMap<String, SoftReference<Bitmap>> cache=new HashMap<String, SoftReference<Bitmap>>();

    private File cacheDir;

    public ImageLoader(Context context){
        //Make the background thread low priority. This way it will not affect the UI performance
        photoLoaderThread.setPriority(Thread.NORM_PRIORITY-1);

        //Find the dir to save cached images
        if (android.os.Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED))
            cacheDir=new File(android.os.Environment.getExternalStorageDirectory(),"MyApp/Temp");
        else
            cacheDir=context.getCacheDir();
        if(!cacheDir.exists())
            cacheDir.mkdirs();
    }

    final int stub_id = R.drawable.loading;
    public void DisplayImage(String url, Activity activity, ImageView imageView)
    {
        if(cache.containsKey(url)){
            imageView.setImageBitmap(null);
            System.gc();
            imageView.setImageBitmap(cache.get(url).get());
        } else {
            queuePhoto(url, activity, imageView);
            imageView.setImageBitmap(null);
            System.gc();
            imageView.setImageResource(stub_id);
        }    
    }

    private void queuePhoto(String url, Activity activity, ImageView imageView)
    {
        //This ImageView may be used for other images before. So there may be some old tasks in the queue. We need to discard them. 
        photosQueue.Clean(imageView);
        PhotoToLoad p=new PhotoToLoad(url, imageView);
        synchronized(photosQueue.photosToLoad){
            photosQueue.photosToLoad.push(p);
            photosQueue.photosToLoad.notifyAll();
        }

        //start thread if it's not started yet
        if(photoLoaderThread.getState()==Thread.State.NEW)
            photoLoaderThread.start();
    }

    private SoftReference<Bitmap> getBitmap(String url) 
    {
        //I identify images by hashcode. Not a perfect solution, good for the demo.
        String filename=String.valueOf(url.hashCode());
        File f=new File(cacheDir, filename);

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

        //from web
        try {
            SoftReference<Bitmap> bitmap=null;
            InputStream is=new URL(url).openStream();
            OutputStream os = new FileOutputStream(f);
            Utils.CopyStream(is, os);
            os.close();
            bitmap = decodeFile(f);
            return bitmap;
        } catch (Exception ex){
           ex.printStackTrace();
           return null;
        }
    }

    //decodes image and scales it to reduce memory consumption
    private SoftReference<Bitmap> decodeFile(File f){
        try {
            //decode image size
            BitmapFactory.Options o = new BitmapFactory.Options();
            o.inJustDecodeBounds = true;
            BitmapFactory.decodeStream(new FileInputStream(f),null,o);

            //Find the correct scale value. It should be the power of 2.
            final int REQUIRED_SIZE=1024;
            int width_tmp=o.outWidth, height_tmp=o.outHeight;
            int scale=1;
            while(true){
                if(width_tmp/2<REQUIRED_SIZE || height_tmp/2<REQUIRED_SIZE)
                    break;
                width_tmp/=2;
                height_tmp/=2;
                scale*=2;
            }

            //decode with inSampleSize
            BitmapFactory.Options o2 = new BitmapFactory.Options();
            o2.inSampleSize=scale;

            return cache.get(BitmapFactory.decodeStream(new FileInputStream(f), null, o2));
        } catch (FileNotFoundException e) {}
        return null;
    }

    //Task for the queue
    private class PhotoToLoad
    {
        public String url;
        public ImageView imageView;
        public PhotoToLoad(String u, ImageView i){
            url=u; 
            imageView=i;
        }
    }

    PhotosQueue photosQueue=new PhotosQueue();

    public void stopThread()
    {
        photoLoaderThread.interrupt();
    }

    //stores list of photos to download
    class PhotosQueue
    {
        private Stack<PhotoToLoad> photosToLoad=new Stack<PhotoToLoad>();

        //removes all instances of this ImageView
        public void Clean(ImageView image)
        {
            for(int j=0 ;j<photosToLoad.size();){
                if(photosToLoad.get(j).imageView==image)
                    photosToLoad.remove(j);
                else
                    ++j;
            }
        }
    }

    class PhotosLoader extends Thread {
        public void run() {
            try {
                while(true)
                {
                    //thread waits until there are any images to load in the queue
                    if(photosQueue.photosToLoad.size()==0)
                        synchronized(photosQueue.photosToLoad){
                            photosQueue.photosToLoad.wait();
                        }
                    if(photosQueue.photosToLoad.size()!=0)
                    {
                        PhotoToLoad photoToLoad;
                        synchronized(photosQueue.photosToLoad){
                            photoToLoad=photosQueue.photosToLoad.pop();
                        }
                        SoftReference<Bitmap> bmp=getBitmap(photoToLoad.url);
                        cache.put(photoToLoad.url, bmp);
                        Object tag=photoToLoad.imageView.getTag();
                        if(tag!=null && ((String)tag).equals(photoToLoad.url)){
                            BitmapDisplayer bd=new BitmapDisplayer(bmp, photoToLoad.imageView);
                            Activity a=(Activity)photoToLoad.imageView.getContext();
                            a.runOnUiThread(bd);
                        }
                    }
                    if(Thread.interrupted())
                        break;
                }
            } catch (InterruptedException e) {
                //allow thread to exit
            }
        }
    }

    PhotosLoader photoLoaderThread=new PhotosLoader();

    //Used to display bitmap in the UI thread
    class BitmapDisplayer implements Runnable
    {
        SoftReference<Bitmap> bitmap;
        ImageView imageView;
        public BitmapDisplayer(SoftReference<Bitmap> bmp, ImageView i){bitmap=bmp;imageView=i;}
        public void run()
        {
            if(bitmap!=null){
                imageView.setImageBitmap(null);
                System.gc();
                imageView.setImageBitmap(bitmap.get());
            } else {
                imageView.setImageBitmap(null);
                System.gc();
                imageView.setImageResource(stub_id);
            }
        }
    }

    public void clearCache() {
        //clear memory cache
        cache.clear();

        //clear SD cache
        File[] files=cacheDir.listFiles();
        for(File f:files)
            f.delete();
    }

}
Community
  • 1
  • 1
Brian
  • 819
  • 4
  • 20
  • 35
  • It does not help to point to some other code in a zip file. Put a snippet here. – Aliostad Feb 22 '11 at 19:20
  • I updated my post with the ImageLoader class. He created some other classes that are referenced in here, but I guess you can figure out what they do. I just need to modify this code to fix my OOM error. Thanks. – Brian Feb 22 '11 at 19:36
  • @Brian :- could you please help me on this same problem. its urgent. – skygeek Sep 21 '12 at 12:10

5 Answers5

7

Generally, your in-memory image cache declaration should look something like:

private static HashMap<String, SoftReference<Bitmap>> cache = 
    new HashMap<String, SoftReference<Bitmap>>();

Note your OOM problems may not necessarily be related to your Bitmap cache. Always ensure you stop/interrupt the thread spawned by your image loader in your Activity's onDestroy method or in the finalize of any other class that is managing it.

Use the Eclipse Memory Analyzer Tool in conjunction with DDMS to analyze your application's memory usage: http://www.eclipse.org/mat/

Jeff Gilfelt
  • 26,131
  • 7
  • 48
  • 47
  • Thanks for the reply Jeff. I already changed the HashMap code like you posted, which leads to many other changes in the code. When I made the other changes, the pictures never load in the imageView. I updated my original post again to show what my changes look like now. If you could look at it and see what I'm doing wrong I would appreciate it. – Brian Feb 22 '11 at 20:07
  • As far as stopping the thread in the onDestroy method, I do call "imageLoader.stopThread();". But that doesn't help the memory problem when I'm in my Image Viewer Activity. This is the only class where I have any threads or background processing going on. – Brian Feb 22 '11 at 20:13
  • 1
    As a general rule, you will make life a lot easier for yourself if you take the time to understand the code you copy and paste into your own projects. Firstly, cache should be declared static so there is only one instance of it. Secondly, decodeFile and getBitmap methods must return Bitmap, not SoftReference. Declare the soft reference at the point you cache it within PhotoLoader.run(): `Bitmap bmp=getBitmap(photoToLoad.url); cache.put(photoToLoad.url, new SoftReference(bmp));` – Jeff Gilfelt Feb 22 '11 at 20:37
  • Thanks for your help, but I made the changes which fixed the image loading problem. But using SoftResource did not help my OOM problem. I get to about the 4th picture and then I get the OOM error. These pictures are around 1280 x 850 (if that matters). How do I go about fixing this? Is there a better way? This method (without the OOM error) is exactly how I want it to work. As the user goes through the pictures, it stores them temporarily so they can scroll through them faster. Any other ideas? Thanks again. – Brian Feb 22 '11 at 20:59
  • Just to add, DDMS shows my heap size at 3.129mb and 2.421mb is allocated. 724kb are free. I'm not a very experience developer, but does that look pretty typical? – Brian Feb 22 '11 at 21:04
  • 2
    OK, so given the large size of your images, you need to seriously start looking at optimizing the sample size options for your bitmaps as you decode them (in your decodeFile method), and explicitly call the recycle() method when you are done with each one. Memory allocation for bitmaps in Android is a tricky subject, and one that I'm probably not qualified to give advice on. Search this site for similar questions, that should give you some good information. – Jeff Gilfelt Feb 22 '11 at 21:22
  • Yeah I have read that garbage collecting bitmaps is a pain. I did go through and null all of the imageViews before changing the image (someone mentioned that on another thread), but that didn't help. As far as recycling the bitmaps in the ImageLoader class. Where exactly would I do that? Everywhere you see a bitmap, its being returned in the method. – Brian Feb 22 '11 at 21:44
  • 1
    While using SoftReference fixed the OOM error for me, it still had some bugs I couldn't work out. After the system cleaned the SoftReference, it didn't clean the cache. It was clearing the bitmap but not the URL. So I removed my SoftReference and then made it so it only stores a few images in the cache at a time. This seems to be working well and am not getting any OOM errors this way either. Now that I'm typing this, I realize that I probably should have used SoftReference on the String too. That may have fixed the problem I was having. Oh well. Thanks for all your help Jeff. – Brian Feb 23 '11 at 20:36
  • Brian, could you explain the idea of restriction cache size or provide source code? –  Apr 03 '11 at 12:06
0

i face the same problem and i use the SoftReference for the bitmap and the url but it seems that the Dalvik VM reclaim SoftReference very quickly and my ListView images keep flickering then i tried inPurgeable= true and i get over OutOfMemoryException and my listview images didn't flicker.

confucius
  • 13,127
  • 10
  • 47
  • 66
0

consider recycling the memory you allocated to the bitmap image

bitmap.recyle(); bitmap=null;

call this function when you no longer require the bitmap image this will free up some memory

0

To fix my OOM from using Lazy Loader, I manually called garbage collection ( System.gc(); ) every time I create or destroy a page (probably doing one or the other would be fine) because Android doesn't always call it as often as it should, and used Fedor's clearCache method:

if (ImageLoader.getImageCache() != null)
    ImageLoader.clearCache();

The function is there, but never gets called by anything unless you make use of it. I do this in my onCreate of the second activity that uses the Lazy Load. You can also do:

@Override
public void onLowMemory() {
    super.onLowMemory();
    ImageLoader.clearCache();
}

This never seems to get called for me, but with how large your images are you might need it. Also consider shrinking the images unless you have a reason to keep them so large.

Cameron
  • 3,098
  • 1
  • 22
  • 40
  • I'm not actually looking to clear the cache. I did add an option to allow the user to do so, but the cache isn't what is creating the OOM. I think its more of a problem with Android not gc appropriately. And I did use the System.gc() method alot throughout the loader itself. But if you read the documentation, that does not cause the system to run the gc, it just sends a hint saying its probably a good time. As far as shrinking the images, I have already shrunk them as far as I want. I want to keep the pics big enough for the tablets that are coming out too. Thanks for your reply. – Brian Feb 22 '11 at 22:05
  • 1
    @CameronW - Here is why onLowMemory doesnt get called: "onLowMemory() is called when the entire system is running out of memory, not when your process is running out of memory. Each app is limited to a fixed amount of RAM (24 MB on a Nexus One for instance).If you use up these 24 MB but the system still has more RAM available, you will get an OutOfMemoryError but not onLowMemory()." - Romain Guy. So, there is no point overriding this method for the app specifically. – Abhijit Nov 03 '11 at 20:20
  • Also, you really shouldn't have to rely on `System.gc();`, and in reality it probably doesn't help you much. Bitmaps aren't stored on the Dalvik (Java) side, they're stored in the native C area of your code, so System.gc() can't touch them. – StackOverflowed Jul 04 '13 at 13:56
-1

You should change the following line in android 2.1 or below.

cacheDir=new File(android.os.Environment.getExternalStorageDirectory(),"MyApp/Temp");

to this

cacheDir = new File(context.getCacheDir(), "MyApp/Temp");
Chrishan
  • 4,076
  • 7
  • 48
  • 67