Android - Bitmap Cache Takes a Lot of Memory

Android - Bitmap cache takes a lot of memory

Images are also scaled according to the density so they can use a lot of memory.

For example, if the image file is in the drawable folder (which is mdpi density) and you run it on an xhdpi device, both the width and the height would double. Maybe this link could help you, or this one.

So in your example the bytes the image file would take are :

(1024*2)*(800*2)*4 = 13,107,200 bytes.

It would be even worse if you ran it on an xxhdpi device (like the HTC one and Galaxy S4) .

What can you do? Either put the image file in the correct density folder (drawable-xhdpi or drawable-xxhdpi) or put it in drawable-nodpi (or in the assets folder) and downscale the image according to your needs.

BTW you don't have to set options.inJustDecodeBounds = false since it's the default behavior. In fact you can set null for the bitmap options.

About down scaling you can use either google's way or my way each has its own advantages and disadvantages.

About caching there are many ways to do it. The most common one is LRU cache. There is also an alternative I've created recently (link here or here) that allows you to cache a lot more images and avoid having OOM but it gives you a lot of responsibility.

How to cache bitmaps into native memory

explanation

the sample code shows how to store 2 different bitmaps (small ones, but it's just a demo), recycle the original java ones, and later restore them to java instances and use them.

as you might guess, the layout has 2 imageViews. i didn't include it in the code since it's quite obvious.

do remember to change the code to your own package if you need, otherwise things won't work.

MainActivity.java - how to use:

package com.example.jnibitmapstoragetest;
...
public class MainActivity extends Activity
{
@Override
protected void onCreate(final Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
//
Bitmap bitmap=BitmapFactory.decodeResource(getResources(),R.drawable.ic_launcher);
final JniBitmapHolder bitmapHolder=new JniBitmapHolder(bitmap);
bitmap.recycle();
//
Bitmap bitmap2=BitmapFactory.decodeResource(getResources(),android.R.drawable.sym_action_call);
final JniBitmapHolder bitmapHolder2=new JniBitmapHolder(bitmap2);
bitmap2.recycle();
//
setContentView(R.layout.activity_main);
{
bitmap=bitmapHolder.getBitmapAndFree();
final ImageView imageView=(ImageView)findViewById(R.id.imageView1);
imageView.setImageBitmap(bitmap);
}
{
bitmap2=bitmapHolder2.getBitmapAndFree();
final ImageView imageView=(ImageView)findViewById(R.id.imageView2);
imageView.setImageBitmap(bitmap2);
}
}
}

JniBitmapHolder.java - the "bridge" between JNI and JAVA :

package com.example.jnibitmapstoragetest;
...
public class JniBitmapHolder
{
ByteBuffer _handler =null;
static
{
System.loadLibrary("JniBitmapStorageTest");
}

private native ByteBuffer jniStoreBitmapData(Bitmap bitmap);

private native Bitmap jniGetBitmapFromStoredBitmapData(ByteBuffer handler);

private native void jniFreeBitmapData(ByteBuffer handler);

public JniBitmapHolder()
{}

public JniBitmapHolder(final Bitmap bitmap)
{
storeBitmap(bitmap);
}

public void storeBitmap(final Bitmap bitmap)
{
if(_handler!=null)
freeBitmap();
_handler=jniStoreBitmapData(bitmap);
}

public Bitmap getBitmap()
{
if(_handler==null)
return null;
return jniGetBitmapFromStoredBitmapData(_handler);
}

public Bitmap getBitmapAndFree()
{
final Bitmap bitmap=getBitmap();
freeBitmap();
return bitmap;
}

public void freeBitmap()
{
if(_handler==null)
return;
jniFreeBitmapData(_handler);
_handler=null;
}

@Override
protected void finalize() throws Throwable
{
super.finalize();
if(_handler==null)
return;
Log.w("DEBUG","JNI bitmap wasn't freed nicely.please rememeber to free the bitmap as soon as you can");
freeBitmap();
}
}

Android.mk - the properties file of JNI:

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := JniBitmapStorageTest
LOCAL_SRC_FILES := JniBitmapStorageTest.cpp
LOCAL_LDLIBS := -llog
LOCAL_LDFLAGS += -ljnigraphics

include $(BUILD_SHARED_LIBRARY)
APP_OPTIM := debug
LOCAL_CFLAGS := -g

JniBitmapStorageTest.cpp - the "magical" stuff goes here :

#include <jni.h>
#include <jni.h>
#include <android/log.h>
#include <stdio.h>
#include <android/bitmap.h>
#include <cstring>
#include <unistd.h>

#define LOG_TAG "DEBUG"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)

extern "C"
{
JNIEXPORT jobject JNICALL Java_com_example_jnibitmapstoragetest_JniBitmapHolder_jniStoreBitmapData(JNIEnv * env, jobject obj, jobject bitmap);
JNIEXPORT jobject JNICALL Java_com_example_jnibitmapstoragetest_JniBitmapHolder_jniGetBitmapFromStoredBitmapData(JNIEnv * env, jobject obj, jobject handle);
JNIEXPORT void JNICALL Java_com_example_jnibitmapstoragetest_JniBitmapHolder_jniFreeBitmapData(JNIEnv * env, jobject obj, jobject handle);
}

class JniBitmap
{
public:
uint32_t* _storedBitmapPixels;
AndroidBitmapInfo _bitmapInfo;
JniBitmap()
{
_storedBitmapPixels = NULL;
}
};

JNIEXPORT void JNICALL Java_com_example_jnibitmapstoragetest_JniBitmapHolder_jniFreeBitmapData(JNIEnv * env, jobject obj, jobject handle)
{
JniBitmap* jniBitmap = (JniBitmap*) env->GetDirectBufferAddress(handle);
if (jniBitmap->_storedBitmapPixels == NULL)
return;
delete[] jniBitmap->_storedBitmapPixels;
jniBitmap->_storedBitmapPixels = NULL;
delete jniBitmap;
}

JNIEXPORT jobject JNICALL Java_com_example_jnibitmapstoragetest_JniBitmapHolder_jniGetBitmapFromStoredBitmapData(JNIEnv * env, jobject obj, jobject handle)
{
JniBitmap* jniBitmap = (JniBitmap*) env->GetDirectBufferAddress(handle);
if (jniBitmap->_storedBitmapPixels == NULL)
{
LOGD("no bitmap data was stored. returning null...");
return NULL;
}
//
//creating a new bitmap to put the pixels into it - using Bitmap Bitmap.createBitmap (int width, int height, Bitmap.Config config) :
//
//LOGD("creating new bitmap...");
jclass bitmapCls = env->FindClass("android/graphics/Bitmap");
jmethodID createBitmapFunction = env->GetStaticMethodID(bitmapCls, "createBitmap", "(IILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;");
jstring configName = env->NewStringUTF("ARGB_8888");
jclass bitmapConfigClass = env->FindClass("android/graphics/Bitmap$Config");
jmethodID valueOfBitmapConfigFunction = env->GetStaticMethodID(bitmapConfigClass, "valueOf", "(Ljava/lang/String;)Landroid/graphics/Bitmap$Config;");
jobject bitmapConfig = env->CallStaticObjectMethod(bitmapConfigClass, valueOfBitmapConfigFunction, configName);
jobject newBitmap = env->CallStaticObjectMethod(bitmapCls, createBitmapFunction, jniBitmap->_bitmapInfo.height, jniBitmap->_bitmapInfo.width, bitmapConfig);
//
// putting the pixels into the new bitmap:
//
int ret;
void* bitmapPixels;
if ((ret = AndroidBitmap_lockPixels(env, newBitmap, &bitmapPixels)) < 0)
{
LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret);
return NULL;
}
uint32_t* newBitmapPixels = (uint32_t*) bitmapPixels;
int pixelsCount = jniBitmap->_bitmapInfo.height * jniBitmap->_bitmapInfo.width;
memcpy(newBitmapPixels, jniBitmap->_storedBitmapPixels, sizeof(uint32_t) * pixelsCount);
AndroidBitmap_unlockPixels(env, newBitmap);
//LOGD("returning the new bitmap");
return newBitmap;
}

JNIEXPORT jobject JNICALL Java_com_example_jnibitmapstoragetest_JniBitmapHolder_jniStoreBitmapData(JNIEnv * env, jobject obj, jobject bitmap)
{
AndroidBitmapInfo bitmapInfo;
uint32_t* storedBitmapPixels = NULL;
//LOGD("reading bitmap info...");
int ret;
if ((ret = AndroidBitmap_getInfo(env, bitmap, &bitmapInfo)) < 0)
{
LOGE("AndroidBitmap_getInfo() failed ! error=%d", ret);
return NULL;
}
LOGD("width:%d height:%d stride:%d", bitmapInfo.width, bitmapInfo.height, bitmapInfo.stride);
if (bitmapInfo.format != ANDROID_BITMAP_FORMAT_RGBA_8888)
{
LOGE("Bitmap format is not RGBA_8888!");
return NULL;
}
//
//read pixels of bitmap into native memory :
//
//LOGD("reading bitmap pixels...");
void* bitmapPixels;
if ((ret = AndroidBitmap_lockPixels(env, bitmap, &bitmapPixels)) < 0)
{
LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret);
return NULL;
}
uint32_t* src = (uint32_t*) bitmapPixels;
storedBitmapPixels = new uint32_t[bitmapInfo.height * bitmapInfo.width];
int pixelsCount = bitmapInfo.height * bitmapInfo.width;
memcpy(storedBitmapPixels, src, sizeof(uint32_t) * pixelsCount);
AndroidBitmap_unlockPixels(env, bitmap);
JniBitmap *jniBitmap = new JniBitmap();
jniBitmap->_bitmapInfo = bitmapInfo;
jniBitmap->_storedBitmapPixels = storedBitmapPixels;
return env->NewDirectByteBuffer(jniBitmap, 0);
}

Android bitmap caching

There's no sense in caching both image sizes, it takes too much memory.

Best practices would be (to my humble opinion):

  1. Make sure your cache uses SoftReferences, this way you can make sure that you don't run out of memory, and can always load new bitmaps on the "expense" of losing old ones.
  2. Use the Canvas' drawBitmap methods to draw the large-scale bitmaps smaller.
  3. Make sure you guard against OutOfMemoryError, and notice that it's a subclass of Throwable, and not a subclass of Exception, so a catch(Exception e) clause will not catch it.

Out of memory issue while loading bitmaps in Android

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 WeakReference everywhere there's a possibility the object will hang onto a bitmap (loading Tasks, ImageViews, etc). From the documentation:

Weak references are useful for mappings that should have their entries removed automatically once they are not referenced any more (from outside). The difference between a SoftReference and a WeakReference is the point of time at which the decision is made to clear and enqueue the reference:
  • A SoftReference should be cleared and enqueued as late as possible, that is, in case the VM is in danger of running out of memory.
  • A WeakReference may be cleared and enqueued as soon as is known to be weakly-referenced.

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 SoftReference or WeakReference object 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 WeakReference will help our timing problem a little bit better than a SoftReference. 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 LruCache that'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 PhotoToLoad is 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:

  1. If you find yourself trying to tell the GC when to fire, you're going down the wrong path.
  2. You're in for a world of pain if you try to call bitmap.recycle() on bitmaps that are used in the UI, such as ImageViews.
  3. A major reason this is such a headache is because there's way too much misinformation out there on this topic on how to solve the problem. So many of the tutorials or examples out there look good in theory, but in practice are absolute trash (confirmed by all the profiling and tracing I mentioned above). What a maze to navigate!

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:

  • DiskLruCache.java
  • ImageCache.java
  • ImageFetcher.java
  • ImageWorker.java
  • public static ImageFetcher getImageFetcher(final FragmentActivity activity) in UIUtils.java

And 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.

Best way to cache in memory a single Bitmap shared across instances

1,3 & 4 are essentially all the same. You create a static reference to either your Bitmap directly or to something that holds a reference. The same happens when you use the Application class to "anchor" that bitmap. That class is kept by Android alive and is in this context the same as a static reference.

Whether this is a memory leak or not depends on your definition. Leaked objects are those that are kept safe from the garbage collector by unintentional references to them. So it's certainly not a leak while you want that reference to keep your bitmap.

The problem that arises with cached data that is independent of the life of some Activity, Fragment or in more general terms "task" is that the data will keep memory occupied even if the user is never coming back to your app. The app process is kept alive until Android decides it needs the memory. That time between your last legit use of the bitmap and Android finally killing your app and thereby cleaning the memory can be seen as leak.

If we had magic powers, we could simply clean up the cache once we know that is going to happen. There are some realistic options though:

  • Using Android's callbacks: understanding onTrimMemory( int level )
  • time limits on references: e.g. https://github.com/jhalterman/expiringmap

2) is not an option. If you're trying to use WeakReference as cache, you haven't understood what that class is intended for and I honestly don't understand why it is even mentioned in the documentation (weakly referenced objects should be garbage collected as fast as possible once nobody has a strong reference anymore).

SoftReference is intended for "caching" but using it as actual cache is not only broken on Android. It's broken by design because you give the garbage collector the responsibility to maintain a cache for you without telling it how to prioritize objects or how much memory it should keep guaranteed under what conditions. The result is that the GC will clean up the wrong thing or simply everything. SoftReference can be used to in addition to a proper cache that knows how to clean up.


In addition to all of that: be aware that a single Bitmap may not be enough. If you had a look at Tasks and Back Stack you may have noticed that 1 app process can have 2 or more independent tasks in parallel. That means there could be whatever Activity uses the bitmap in different stages. If you don't want to overwrite your cache bitmap between those all the time, you may have to have 1 bitmap per task.

I don't know how to do it per task, but you can easily use a retained fragment to tie the life of your bitmap to that of an activity (ignoring screen rotation etc): http://www.androiddesignpatterns.com/2013/04/retaining-objects-across-config-changes.html / example with bitmap cache https://github.com/google/iosched/blob/master/android/src/main/java/com/google/samples/apps/iosched/util/BitmapCache.java

Android - Reduce the memory usage of Bitmap Drawables

This will always be a problem unfortunately. You can try downsampling. Strange out of memory issue while loading an image to a Bitmap object

You might try saving the images to a temporary file system so that you don't have to hit the web every time. That should help a lot or maybe even 100% with the flicker. Couple that with an access-based heap cache (if you feel up to building one), and you're probably good.



Related Topics



Leave a reply



Submit