How Does Bitmap Allocation Work on Oreo, and How to Investigate Their Memory

Determining available memory for bitmaps

It seems that getting available memory is a bit quirky in Android, but it turned out that my main problem was that on newer versions of Android the memory allocation for bitmaps has changed, and they now easily blow the limit of the heap.

Setting largeHeap in the manifest got me around that problem, but I'm still not sure it's ideal.

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);
}

Choosing a max bitmap size for memory concerns

I found myself with similar issues. After some research and testing I came up with a number of methods that helped me on that subject. Those are implemented with C# using Mono for Android but I guess they should be almost identical with Java:

/// <summary>
///Calculates the memory bytes used by the given Bitmap.
/// </summary>
public static long GetBitmapSize(Android.Graphics.Bitmap bmp)
{
return GetBitmapSize(bmp.Width, bmp.Height, bmp.GetConfig());
}

/// <summary>
///Calculates the memory bytes used by a Bitmap with the given specification.
/// </summary>
public static long GetBitmapSize(long bmpwidth, long bmpheight, Android.Graphics.Bitmap.Config config)
{
int BytesxPixel = (config == Android.Graphics.Bitmap.Config.Rgb565) ? 2 : 4;

return bmpwidth * bmpheight * BytesxPixel;
}

/// <summary>
///Calculates the memory available in Android's VM.
/// </summary>
public static long FreeMemory()
{
return Java.Lang.Runtime.GetRuntime().MaxMemory() - Android.OS.Debug.NativeHeapAllocatedSize;
}

/// <summary>
///Checks if Android's VM has enough memory for a Bitmap with the given specification.
/// </summary>
public static bool CheckBitmapFitsInMemory(long bmpwidth, long bmpheight, Android.Graphics.Bitmap.Config config)
{
return (GetBitmapSize(bmpwidth, bmpheight, config) < FreeMemory());
}

That code proved pretty reliable preventing out of memory exceptions. An example using those methods in the namespace called Utils is the code snippet below. This code calculates the memory necessary for 3 bitmaps, two of them being 3 times as big as the first one.

/// <summary>
/// Checks if there's enough memory in the VM for managing required bitmaps.
/// </summary>
private bool NotEnoughMemory()
{
long bytes1 = Utils.GetBitmapSize(this.Width, this.Height, BitmapConfig);
long bytes2 = Utils.GetBitmapSize(this.Width * 3, this.Height * 3, BitmapConfig);

return ((bytes1 + bytes2 + bytes2) >= Utils.FreeMemory());
}

How to clear bitmap memory of previous activity when open new one

You can use bitmap.recycle() in your onDestroy().

Also you can use this code in your onDestroy() to avoid OOMExceptions. This basically unbinds all images in your activity:

public void unbindDrawables(View view) {//pass your parent view here
try {
if (view.getBackground() != null)
view.getBackground().setCallback(null);

if (view instanceof ImageView) {
ImageView imageView = (ImageView) view;
imageView.setImageBitmap(null);
} else if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
for (int i = 0; i < viewGroup.getChildCount(); i++)
unbindDrawables(viewGroup.getChildAt(i));

if (!(view instanceof AdapterView))
viewGroup.removeAllViews();
}
} catch (Exception e) {
e.printStackTrace();
}
}


Related Topics



Leave a reply



Submit