39

For my 10,000 points, i've decided to contibute something with this cool website: a mechanism to cache bitmaps on native memory.

Background

Android devices have a very limited amount of memory for each app- the heap ranges from 16MB to 128MB , depending on various parameters .

If you pass this limit, you get OOM , and this can occur many times when you use bitmaps.

Many times, an app might need to overcome those limitations, perform heavy operations on huge bitmaps or just store them for later use, and you need to

What i've come up with, is a simple java class that would make things a bit easier for those purposes.

It's using JNI in order to store the bitmap data , and be able to restore it when needed.

In order to support multiple instances of the class, i had to use a trick i've found (here) .

Important notes

  • The data is still stored on the RAM, so if the device doesn't have enough RAM, the app might be killed.

  • Remember to free the memory as soon as you can. it's not only to avoid memory leaks, but it's also in order to avoid being prioritized by the system to be killed first, once your app comes to the background.

  • If you don't want to forget freeing the memory, you can either free it each time you restore the bitmap, or make the class implement Closable .

  • As a safety measure, i've made it automatically free its native memory in the finalize() method, but don't let it be responsible of the job. it's too risky. i've also made it write to the log when such a thing occurs.

  • The way it works is by copying the entire data into JNI objects, and in order to restore, it creates the bitmap from scratch and puts the data inside.

  • Bitmaps being used and restored are in ARGB_8888 format. of course, you can change it to whatever you wish, just don't forget to change the code...

  • Large bitmaps could take time to store and restore, so it might be wise to do it on a background thread.

  • This is not a full OOM solution, but it could help. for example, you could use it in conjunction with your own LruCache, while avoiding using the heap memory for the cache itself.

  • Code is only for storing and restoring. if you need to perform some operations, you will need to perform some research. openCV might be the answer, but if you wish to perform some basic stuff, you could implement them by yourself (here's an example of rotatign large images using JNI). if you know other alternatives, please let me know, here .

Hope this will be useful for some people. please write down your comments.

Also, if you find any problem with the code or a suggestion for impovement, please let me know.


Better solution

If you wish to perform even more operations on the JNI side, you can use this post that I've made. it's based on the code I've written here, but allows you to do more operations and you can easily add more of your own.

Community
  • 1
  • 1
android developer
  • 114,585
  • 152
  • 739
  • 1,270
  • @Akanksha Have you tried the sample? did it work? if so, convert the project i've made into an android library and then use it from your original project. if that still doesn't work, copy the entire code to your project. i don't mind :) BTW, how do I add license info (of apache) ? – android developer Jan 01 '14 at 10:57
  • Thanks! can u share except libs, jni and src i else i have to copy in my project to use it. – Akanksha Rathore Jan 01 '14 at 11:19
  • @Akanksha you can do whatever you wish, it's your app... – android developer Jan 01 '14 at 11:48
  • i am asking the procedure to integrate is with my app. – Akanksha Rathore Jan 01 '14 at 11:51
  • @Akanksha Would you like me to make a sample project that uses my project? Maybe it's better this way, so that I will separate the sample from the library, thus making the library the bare minimum of files. – android developer Jan 04 '14 at 23:59
  • U r little bit late.I already done my work with another approach,if you can do this, it will beneficial for others who don't have knowledge of ndk, so that people can add it like library and can do their work. When u will done plz,update the link also.Thanks – Akanksha Rathore Jan 06 '14 at 05:28
  • @Akanksha ok, i've separated the library from the sample. now it should be much cleaner. thanks for your advice. you can check it out and see that it works just fine: https://github.com/AndroidDeveloperLB/AndroidJniBitmapOperations – android developer Jan 06 '14 at 19:43

2 Answers2

15

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 developer
  • 114,585
  • 152
  • 739
  • 1,270
  • Yes, github project working fine. so i want to include it with my project and i tried both ways converted sample project in library and in android tag i included it. it is not showing any error on compile time but on run time ExceptionInInitializerError. I think problem is here System.loadLibrary("JniBitmapOperations"); or may b its packeg name problem. I never used ndk.how to implement it? – Akanksha Rathore Jan 01 '14 at 10:59
  • You will need to do some research on this. please continue the comments where you started it instead of multiple places. however, you are correct that the package is important, as the code in the C/C++ side assumes about their paths. – android developer Jan 01 '14 at 11:51
  • Thanks for reply. What i have to change if i make ur project as library project and include it in my app. like pkg name or path? – Akanksha Rathore Jan 01 '14 at 11:54
  • is it your first time of importing an android library, or do you have a specific problem with this one in particular? if there is a problem with this one, you can just copy its files to your project, change the packages paths inside all files (especially the C/C++ files) , and it should work fine. – android developer Jan 01 '14 at 12:23
  • depends on what you do with it (in which package you put it). – android developer Jan 01 '14 at 14:30
  • @androiddeveloper After saving the image in Native Memory,when i try to get the image and then saving it to SD Card, it returns me an Black Image with no display at all. any clue about this? – AndroidLearner Apr 08 '14 at 13:43
  • you need to pass the width first in create bitmap method like this jobject newBitmap = env->CallStaticObjectMethod(bitmapCls, createBitmapFunction, jniBitmap->_bitmapInfo.width, jniBitmap->_bitmapInfo.height, bitmapConfig); – Sabish.M Oct 03 '16 at 07:59
  • @AndroidLearner Have you tried the sample? It should work fine. – android developer Oct 03 '16 at 12:04
2

If you just want to cache bitmaps off the heap, a simpler solution is to use parcel memory.

This is the Gist of it (full code bellow). You can use it for Parcelable instances other than Bitmap. Use it like this:

private final CachedParcelable<Bitmap> cache = new CachedParcelable<>(Bitmap.CREATOR);

cache.put(bitmap);
bitmap = cache.get();
cache.close();

public final class CachedParcelable<T extends Parcelable> implements AutoCloseable {
    private final Parcelable.Creator<T> creator;
    private Parcel cache;

    public CachedParcelable(Parcelable.Creator<T> creator) {
        this.creator = creator;
    }

    public synchronized T get() {
        if (cache == null) return null;
        try {
            cache.setDataPosition(0);
            return creator.createFromParcel(cache);
        } catch (BadParcelableException e) {
            //
        } catch (RuntimeException e) {
            if (creator != Bitmap.CREATOR) throw e;
        }
        return null;
    }

    public synchronized void put(T value) {
        if (cache != null) cache.recycle();
        if (value == null) {
            cache = null;
            return;
        }
        try {
            cache = Parcel.obtain();
            value.writeToParcel(cache, 0);
        } catch (RuntimeException e) {
            if (creator != Bitmap.CREATOR) throw e;
        }
    }

    @Override
    public synchronized void close() {
        if (cache != null) {
            cache.recycle();
            cache = null;
        }
    }
}
Nuno Cruces
  • 1,584
  • 15
  • 18
  • Parcelizing a Bitmap will move it away from the heap? – android developer Jun 07 '18 at 20:50
  • 1
    Yes, `Parcel` uses the native `malloc` heap. – Nuno Cruces Jun 08 '18 at 01:14
  • Interesting. But can you do anything with the bitmap when it's there, like in my JNI solution ? Or just cache it? – android developer Jun 08 '18 at 09:08
  • 1
    No, it's opaque. But if keeping a (few) large bitmap(s) cached away from the heap is your only goal, it's the simplest solution I know of. – Nuno Cruces Jun 08 '18 at 09:17
  • What do you mean by "it's opaque" ? And is there a way to know how many Bitmaps I can store ? Maybe get total amount of RAM used by my app (including all kinds), vs what's free for it (it's obviously not all the free RAM of the device...) ? – android developer Jun 08 '18 at 09:53
  • 1
    I mean there is no way to access the data while it is parceled. I'm not sure how many. I use it to cache a couple of really large bitmaps. – Nuno Cruces Jun 08 '18 at 10:22
  • I see. Nice to know this too. I give you +1 for the effort. BTW, did you know that from Android O, Bitmaps actually don't take space in heap memory? – android developer Jun 08 '18 at 13:16
  • I think all primitive arrays larger than X now get their own heap space. This is something the GC is aware of, but doesn't need to scan (no object references). Bitmaps use an int[] internally, so they go there as well. – Nuno Cruces Jun 08 '18 at 14:09
  • Interesting! Where did you read about it? And why did they do it? Isn't it harder now to manage the memory? As I've noticed, you don't get OOM anymore when creating Bitmaps. You will get something else: https://stackoverflow.com/q/48091403/878126 – android developer Jun 08 '18 at 15:50
  • Honestly don't remember. Another interesting change is that it seems backing memory for arrays might not be allocated at all until you read/write from/to them. Some of this I figured out by memory profiling an app that loads multi-megapixel bitmaps, all with a 10 MB heap. – Nuno Cruces Jun 13 '18 at 11:42
  • Nice. These decisions of Google seem good and bad to me. Good because there is much less hassle about memory management and less fear of OOM. Bad because such things can still occur (and always will) yet I don't think we can use a good way to know about such matters and I don't think we have good API to deal with this approach (example: know how much memory we can use for Bitmaps). – android developer Jun 13 '18 at 11:46