Tuesday, October 9, 2012

JNI+NDK Arrays, Bitmaps, and Games, Oh My!

The Android Native Development Kit (NDK) is a very powerful tool that can help app developers integrate existing code or leverage native features using Android. The NDK can be intimidating to newcomers to Android development, but it can be very useful in the right situations.

One of the questions I had when I was working with the NDK was what kind of performance I was getting from passing arrays back and forth through the Java Native Interface (JNI). There are several ways of accessing array elements in JNI. The most common way of accessing array elements is by calling Get*ArrayElements(JNIEnv *env, ArrayType arr, jboolean *isCopy). This function returns a pointer to given array to use in native code. This function must be matched by a call to Release*ArrayElements(). What worried me about Get*ArrayElements is the isCopy parameter. isCopy is a pointer to a jboolean that is set to true if the returned pointer is a pointer to a copy of arr. If a copy is made, the call to Release*ArrayElements will copy the array back to the passed Java array. This seemed like an opportunity for poor performance to me. Why should the Java array have to be copied?

It turns out that Android never copies arrays with a call to Get*ArrayElements. The great thing about Android is that you can access all the source code and find out about these things. Here is the relevant function from the Dalvik VM source (from Kernel.org):


/*
 * Get a pointer to a C array of primitive elements from an array object
 * of the matching type.
 *
 * In a compacting GC, we either need to return a copy of the elements or
 * "pin" the memory.  Otherwise we run the risk of native code using the
 * buffer as the destination of e.g. a blocking read() call that wakes up
 * during a GC.
 */

#define GET_PRIMITIVE_ARRAY_ELEMENTS(_ctype, _jname)
    static _ctype* Get##_jname##ArrayElements(JNIEnv* env,
        _ctype##Array jarr, jboolean* isCopy)
    {            
        JNI_ENTER();
        _ctype* data;
        ArrayObject* arrayObj =
            (ArrayObject*) dvmDecodeIndirectRef(env, jarr);
        pinPrimitiveArray(arrayObj);
        data = (_ctype*) arrayObj->contents;
        if (isCopy != NULL)
            *isCopy = JNI_FALSE;
        JNI_EXIT();
        return data;
    }

As you can see, this method always "pins" the memory instead of copying the array. That's great for us because copying a big array can take a while. Note that just because the Dalvik VM never copies, other VM's might decide to copy, and you should check the isCopy parameter returns true.

A problem with working with large bitmaps in Android is that Android applies a memory limit on applications. Android does keep track of memory consumed by Bitmaps, and they count against the application limit. There is a misconception that using direct byte buffers can bypass this limit. This blog reveals that Android does keep track of direct buffers, and it also provides a solution to bypassing the limit. The solution is to allocate the buffer in the native layer and call the JNI method NewDirectByteBuffer() with the allocated pointer as so:


char* ptr = (char*)malloc(numBytes);
return env->NewDirectByteBuffer(ptr, numBytes);

You can return the new byte buffer to the Java layer and use it as you would any ByteBuffer. There is a great benefit to using ByteBuffers. It's that they can represent any bitmap you would like. Specifically, you don't have to use a  full 32 bit integer for your bitmaps. Often it is sufficient or it is more efficient to use a 16 bit short integer to represent pixels in a bitmap. The problem is that all the methods in the Android Bitmap class only accept int[]. To gain full control over the colors in the bitmap, set the pixel format in the Bitmap constructor. For example you can use Bitmap.Config.RGB_565 to use 16 bit pixels. To set the pixels in the bitmap, you avoid the tempting Bitmap.setPixels(); instead use Bitmap.copyPixelsFromBuffer() with the buffer created earlier. From the Android documentation:

"The data in the buffer is not changed in any way (unlike setPixels(), which converts from unpremultipled 32bit to whatever the bitmap's native format is."

This will save us some processing because we can copy directly to the bitmap memory from the buffer memory. 

But why can't we just directly write to the Bitmap object's memory instead of having to copy? This copy can take a while on large bitmaps, and when you're making a game, every millisecond improves frame rate. Android created an extension to the NDK to do just that. This extension is only available in Android 2.2 (API level 8) and higher. You'll find a new file, bitmap.h in <android-ndk>/platforms/<platform>\arch-arm\usr\include\android. This file exposes methods for accessing bitmap memory directly! Calling AndroidBitmap_lockPixels() will return the pointer to the bitmap, and then you just call AndroidBitmap_unlockPixels() when you're finished modifying the bitmap. This will allow you to write directly to the bitmap!

For those of you who want to use the bitmap API in 2.1 (API level 7) and below, there is a (hacky) way to use call these functions in lower Android versions. This Stack Overflow question gives a way to do so.