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)
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
WeakReferenceeverywhere there’s a possibility the object will hang onto a bitmap (loading Tasks, ImageViews, etc). From the documentation: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
SoftReferenceorWeakReferenceobject 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
WeakReferencewill help our timing problem a little bit better than aSoftReference. 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
LruCachethat’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
PhotoToLoadis 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:
bitmap.recycle()on bitmaps that are used in the UI, such as ImageViews.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:
public static ImageFetcher getImageFetcher(final FragmentActivity activity)in UIUtils.javaAnd 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.