Android Out of Memory error with Lazy Load images

16,409

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/

Share:
16,409
Brian
Author by

Brian

Updated on June 04, 2022

Comments

  • Brian
    Brian about 2 years

    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();
        }
    
    }