6

I've got memory leak using LazyList. I use one instance of ImageLoader in whole app, I create it in Application.onCreate(), because I need image downloading in several activities: list activity, one activity with gallery widget and full-screen gallery activitiy(all of them use same cache) I modified image loader so it uses SoftReference-based HashMap. Here's code for SoftHashMap:

public class SoftHashMap extends AbstractMap {

    private final Map hash=new HashMap();
    private final int HARD_SIZE;
    private final LinkedList hardCache=new LinkedList();
    private final ReferenceQueue queue=new ReferenceQueue();
    
    public SoftHashMap(){
        this(100);
    }
    
    public SoftHashMap(int hardSize){
        HARD_SIZE=hardSize;
    }
    
    public Object get(Object key){
        Object result=null;
        SoftReference soft_ref=(SoftReference)hash.get(key);
        if(soft_ref!=null){
            result=soft_ref.get();
            if(result==null){
                hash.remove(key);
            }else{
                hardCache.addFirst(result);
                if(hardCache.size()>HARD_SIZE){
                    hardCache.removeLast();
                }
            }
        }
        return result;
    }
    private static class SoftValue extends SoftReference{
        private final Object key;
        public SoftValue(Object k, Object key, ReferenceQueue q) {
            super(k, q);
            this.key=key;
        }
    }
    
    private void processQueue(){
        SoftValue sv;
        while((sv=(SoftValue)queue.poll())!=null){
            hash.remove(sv.key);
       }
    }
    
    public Object put(Object key, Object value){
        processQueue();
        return hash.put(key, new SoftValue(value, key, queue));
    }
    
    public void clear(){
        hardCache.clear();
        processQueue();
        hash.clear();
    }
    
    public int size(){
        processQueue();
        return hash.size();
    }

    public Set entrySet() {
        throw new UnsupportedOperationException();
    }

}

ImageLoader class:

public class ImageLoader {
    

     private SoftHashMap cache=new SoftHashMap(15);
     
     private File cacheDir;
     final int stub_id=R.drawable.stub;
     private int mWidth, mHeight;
     
     public ImageLoader(Context context, int h, int w){
         mWidth=w;
         mHeight=h;
         
         photoLoaderThread.setPriority(Thread.NORM_PRIORITY);
         if (android.os.Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED))
                cacheDir=new File(android.os.Environment.getExternalStorageDirectory(),"CacheDir");
            else
                cacheDir=context.getCacheDir();
            if(!cacheDir.exists())
                cacheDir.mkdirs();
     }
     public void DisplayImage(String url, Activity activity, ImageView imageView)
        {
           
           
           Log.d("IMAGE LOADER", "getNativeHeapSize()-"+String.valueOf(Debug.getNativeHeapSize()/1024)+" kb");
           Log.d("IMAGE LOADER", "getNativeHeapAllocatedSize()-"+String.valueOf(Debug.getNativeHeapAllocatedSize()/1024)+" kb");
           Log.d("IMAGE LOADER", "getNativeHeapFreeSize()-"+String.valueOf(Debug.getNativeHeapFreeSize()/1024)+" kb");
           if(cache.get(url)!=null){
               imageView.setImageBitmap((Bitmap)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){
            Bitmap b=null;
            try {
                //decode image size
                
                BitmapFactory.Options o = new BitmapFactory.Options();
                o.inJustDecodeBounds = true;
                FileInputStream fis=new FileInputStream(f);
                BitmapFactory.decodeStream(fis,null,o);
                try {
                    fis.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                
                //Find the correct scale value. It should be the power of 2.
                //final int REQUIRED_SIZE=mWidth;
                int width_tmp=o.outWidth, height_tmp=o.outHeight;
                int scale=1;
                
                while(true){
                    if(width_tmp/2<=mWidth || height_tmp/2<=mHeight)
                        break;
                    width_tmp/=2;
                    height_tmp/=2;
                    scale*=2;
                }
                
                //decode with inSampleSize
                BitmapFactory.Options o2 = new BitmapFactory.Options();
                o2.inSampleSize=scale;
                //o2.inPurgeable=true;
                fis=new FileInputStream(f);
                b=BitmapFactory.decodeStream(fis, null, o2);
                try {
                    fis.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                return b;
            } catch (FileNotFoundException e) {}
            return null;
        }
     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();
        }
     class PhotosQueue{
         private Stack<PhotoToLoad> photosToLoad=new Stack<PhotoToLoad>(); 
         
         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();
     
     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();
        }
}

And my Application class, not the best way to do it, though:

public class MyApplication extends Application {
    
    ImageLoader mImageLoader;

    
    @Override 
    public void onCreate(){
    
        int h =((WindowManager)getApplicationContext().getSystemService(WINDOW_SERVICE)).getDefaultDisplay().getHeight();
        
        int w =((WindowManager)getApplicationContext().getSystemService(WINDOW_SERVICE)).getDefaultDisplay().getWidth();
        mImageLoader=new ImageLoader(getApplicationContext(), h, w);
        super.onCreate();
  
    public ImageLoader getImageLoader(){
        return mImageLoader;
    }
    
    @Override
    public void onLowMemory(){
        mImageLoader.clearCache();
        Log.d("MY APP", "ON LOW MEMORY");
        super.onLowMemory();
    }
}

And the worst part: after some time I receive OOM exception when ImageLoader tries to decode another bitmap. I'll appreciate any your help. Thanks.

EDIT I've got rid of hard cache, but i still get this OOM exception. It seems to me that I'm doing smth funny. I don't even know what extra information should I provide... The images which i download from server are pretty big, though. And app fails to allocate appr. 1.5 mb, that's what I see in LogCat. But I just can't figure out why doesn't vm clear my SoftHashMap if there is need for memory...

Community
  • 1
  • 1
  • Can you post a project to play with? If I play with your code maybe I could tell you what the problem is. – Fedor Apr 11 '11 at 13:36
  • Fedor, unfortunately I can't post project. Could all this OOM problems be caused by context leak? Your loader uses Activity object to run BitmapDisplayer on UI thread. But in my app i've got several activities(i.e. user selects list item and I start new Activity) and I've just realized that there could be references to dead activity objects. –  Apr 11 '11 at 19:02
  • 1
    I think that problem is with the size of images. If I have a 5mpx image, BitmapFactory needs a lot of memory to decode it. And GC has no time to clear native heap if there are several big images. So the only solution is to avoid large bitmaps. –  Apr 12 '11 at 07:40

4 Answers4

1
  1. onLowMemory will be of no help to you as it is not generated when your app is running out of memory, it is called when the Android system would like memory for a different application or itself before it kills off processes.
  2. I don't see the need for hard cache - this is preventing drawables from being recycled. Just leave the drawables in the soft cache - the GC won't collect the memory while the drawables have references that are not soft so you don't need to worry about a drawable currently set in an ImageView being recycled.

Also how many images are you displaying on screen at once? How large are they?

Joseph Earl
  • 23,351
  • 11
  • 76
  • 89
  • Thanks for response. The number of drawable varies from one in full-screen gallery to 6-7 in list activity. I receive images from server and don't know their exact size. So i decode them to fit the screen size. but it seems not to be effective to supply such images both for gallery and thumbnails in listview. I didn't know that using hardcache prevents bitmaps from being garbage collected. –  Apr 07 '11 at 17:50
  • Okay so not a lot (I have an app with ~40 small images on screen that works fine). In that case how large the image files you are loading? (also get rid of the hard cache!) – Joseph Earl Apr 07 '11 at 17:55
  • I do not know the exact size of images being loaded from server, approximately from VGA-images to 1600x1200 pixels. I decode them with inSampleSize in order to make them less then screen dimensions. So, images supplied to views are about 70kb. –  Apr 07 '11 at 18:01
  • Also, could you tell more about hard cache? I want to understand why does it prevent bitmaps from being collected by GC. –  Apr 07 '11 at 18:02
  • 1
    A SoftReference doesn't mark something as 'collectable' by the GC, it is a way of keeping a reference to an object without preventing the GC from reclaiming the memory taken up by an object if needed. Normally the GC will only collect memory held by objects which are no longer referenced. For SoftReferences, the GC will only collect objects whose **only** references are soft (or weak etc). So if you have two references to an object, one soft and one not, then GC cannot collect the memory because the object is still 'hard' referenced by one or more things. – Joseph Earl Apr 07 '11 at 18:13
  • Another thing to do would be to use a memory allocation tracker such as Eclipse MAT and see which objects are taking up most of your memory usage. – Joseph Earl Apr 07 '11 at 18:16
  • As far as I know bitmaps are stored in native heap? So is it possible to analize native heap usage with MAT? DDMS shows that my app has ~3mb heap. –  Apr 07 '11 at 19:32
1
  1. Here's an amazing article on analyzing memory leaks. It can definitely help you. http://android-developers.blogspot.com/2011/03/memory-analysis-for-android.html.

  2. Are you absolutely sure your SoftHashMap implementation works fine? Looks rather complicated. You can use debugger to ensure SoftHashMap never holds more than 15 bitmaps. MAT can also help you to identify how many bitmaps are there in memory.
    You can also comment cache.put(photoToLoad.url, bmp) call. This way you'll disable in-memory caching to identify if it's a cause of a problem or not.

  3. Yes it may be an Activity leak. You can identify that. If you just scroll around in the same activity and get OOM it means that something else is leaking not activity. If you stop/start activity several times and get OOM it means activity is leaking. Also you can definitely say is activity leaking or not if you take a look at MAT histogram.

  4. As you use inSampleSize the image size doesn't matter. It should work fine even with 5mpx images.

  5. You could try to replace your SoftHashMap implementation with just HashMap<String, SoftReference<Bitmap>>. Read about SoftReference. It is good for very simple implementation of in-memory cache. It holds objects in memory if there's enough memory. If there's too little memory SoftReference releases objects.

  6. I can also recommend you to use LinkedHashMap for in-memory cache. It has a special constuctor to iterate items in the order in which its entries were last accessed. So when you have more than 15 items in cache you can remove least-recently accessed items. As documentation says:

    This kind of map is well-suited to building LRU caches.

  7. You know my implementation was designed with small images in mind, something like 50*50. If you have larger images you should think how much memory they consume. If they take too much you could just cache them to SD card but not to memory. The performance may be slower but OOM would not be a problem any more.

  8. Not related to OOM. I can see you call clearCache() in onLowMemory(). Not good because clearCache() also removes cache from SD. You should only clear in-memory cache not SD cache.

Fedor
  • 43,261
  • 10
  • 79
  • 89
  • 7. SD-caching only is too slow, but I don't get OOM. 8. onLowMemory is never called. –  Apr 16 '11 at 09:25
  • Also I looked at MAT hist and it showed only 2.5-3mb of head used. So I can assume that I've got native-heap leak. Thanx for response, Fedor. –  Apr 16 '11 at 09:45
  • You're right. Bitmap data is stored in native memory. You should use MAT to identify why your bitmaps are leaking. You should pay attention not to the memory consumed but to the number of objects. When you fix the leak in java heap the native memory leak will also be fixed. Good Luck! – Fedor Apr 16 '11 at 16:15
  • 1
    `Read about WeakReference. It is good for very simple implementation of in-memory cache. It holds objects in memory if there's enough memory...` You are confusing WeakReference with SoftReference here. WR are cleared on next GC (if there are no hard or soft references to the object), while SR are cleared on memory demand. JavaDoc:`Soft reference objects, which are cleared at the discretion of the garbage collector in response to memory demand. Soft references are most often used to implement memory-sensitive caches.` So it should be Map>. – Volo Apr 16 '11 at 21:36
  • Absolutely! Sorry for that. Fixed. – Fedor Apr 17 '11 at 00:31
0

It looks like you're creating bitmaps, but never calling Bitmap.recycle().

EDIT: To elaborate, Bitmap.recycle() frees memory currently being used to store the Bitmap. Since Bitmaps created by the BitmapFactory are immutable, (you can check this with Bitmap.isMutable() on your freshly created bitmaps), simply removing a reference to them from the hashtable and waiting on the garbage collector isn't enough to free up the memory.

Call Bitmap Recycle whenever you're done with a specific bitmap (such as in the "clean" method of your photoQueue, or clearCache()).

Alexander Lucas
  • 22,171
  • 3
  • 46
  • 43
0

I seen that below line is commented in your code, First of All uncomment that line plz.

//o2.inPurgeable=true;

In the NonPurgeable case, an encoded bitstream is decoded to a different Bitmap over and over again until out-of-memory occurs. In Purgeable case, The memory allocated by one image will be shared by any new image whenever required and later any time if the older image reference is activiated in that case the OS will manage the memory reference it self by using a space of another image and vice verce and this way it always avoid the out of memory error.

If even you have purgeable case and still facing that memory leak then now use below try catch blog to trace the error and let me know the Stack Trace Detail.

    try{
    //your code to download or decode image
    }catch(Error e){
    //Print the stack trace
}
Rahul Patel
  • 1,198
  • 7
  • 5