I have a memory leak using LazyList . I use one instance of ImageLoader in the whole application, I create it in Application.onCreate (), because I need to load images into several types of activity: list activity, one action with gallery widgets and activitiy full-screen gallery (they all use the same cache) I modified the image downloader, so it uses HashMap based on SoftReference. Here is the 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 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 this:
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 a while, I get an OOM exception when ImageLoader tries to decode another bitmap. I would be grateful for any of your help. Thanks.
EDIT I got rid of the hard cache, but I still get this OOM exception. It seems to me that I am doing something funny. I donโt even know what additional information I should provide ... The images that I download from the server are quite large. And the application cannot allocate appr. 1.5 mb what I see in LogCat. But I just canโt understand why my SoftHashMap doesnโt clear if there is a need for memory ...