Media ISVs do a lot of blits and conversions in order to offer different video playback and video/image editing features. Decode, scaling, and color space transformations are often done in software at the application layer and are therefore subject to bottlenecks caused by the algorithms employed. These compute intensive algorithms can be sped up simply by converting the algorithm into native C code via JNI. This paper will illustrate step-by-step how to create a JNI library with one implementation of a NV12 to ARGB8888 conversion algorithm and how to then call that function from the Android* application layer. It will also contrast the performance of the same algorithm when written purely in Java*. Using native code via JNI for compute intensive algorithms can help ISVs to easily improve the performance of their media apps.
Since we will be working with native code, we need to install the Android NDK in addition to the Android SDK. You can grab the SDK here: http://developer.android.com/sdk/index.html and the NDK here: http://developer.android.com/tools/sdk/ndk/index.html.
From the Eclipse* ADT, create a new Android Application Project and enter the appropriate Target SDK for your platform. Since I was developing this sample app for a Medfield DV2.0 tablet running Ice Cream Sandwich, I chose that from the Target SDK drop-down list:
Select all the defaults for the rest of the project creation.
Create JNI Wrapper Function
Create a new Class under the src directory of the created project for the JNI wrapper function. There should already be a MainActivity.java file under the correct package, so create your new .java file in the same place. Also remember to load the library we will be creating that will hold the conversion algorithm. Note that System.loadLibrary() takes the undecorated library name only, so you’ll need to strip off the beginning “lib” and ending “.so” parts of the file. For example, libJNIFourCCConversion.so is passed into System.LoadLibary() using JNIFourCCConversion. Here is the source code for FourCCConversionWrapper.java:
Create C Header
We can now use the Java source to create a C header file with the function prototype for the native method we declared. First, compile the Java source file into a class file using the javac command. Since we are using Bitmap, make sure to call javac with classpath set to the correct SDK android.jar file:
Change the directory back to the src folder and then call javah on the created class file to generate the header file. Since the class passed into javah is inside a package, you’ll need to provide the correct path:
This creates the .h file with the full qualified class name, but we can rename it to something simpler to match the name of our library, which we load in our JNI wrapper function. In this case, we rename it to JNIFourCCConversion.h.
Create jni Folder
Back in the Eclipse ADT, create a jni subfolder for the project on the same level as the src directory. This is where we will put all of our native C code. Now move the renamed C header from the previous step to that directory:
Create C Source
Create a new .c file under the jni directory. In this case, we will call it JNIFourCCConversion.c to match the name of our library, which we load in our JNI wrapper function. Using the prototype generated by javah (in this example it is in JNIFourCCConversion.h), we now can implement our C source as follows:
Create Android.mk file
Create a new Android.mk file in your jni folder. Note that this is where we tell Android the name of our library, which we load in the JNI wrapper function: JNIFourCCConversion. Again, this file uses the undecorated library name only. The actual file that is created and loaded is libJNIFourCCConversion.so.
Call ndk-build to build shared library
Now that all the native code is written, build the JNI library that exposes JNINV12ToARGB8888().To do this, use the ndk-build command with APP_ABI=x86:
Call JNINV12ToARGB8888() from a simple Android App
Open MainActivity.java and update the code so that the same algorithm used in the JNI library to convert NV12 to ARG8888 is implemented in Java. Then create a 176x144 frame filled with all 0s for the NV12 data and time the calls to both the Java version and the native version via the JNI call. Draw the resultant ARB888-converted bitmaps and the conversion times to the screen. The simple code to do all this is below:
Run Android App
Using the Eclipse ADT, you can now create a new Run Configuration for your project and when you execute it, you should see a two green boxes (the result of converting an NV12 value of 0 to ARGB8888) and the return values of the times in nanoseconds from calling the native JNINV12ToARGB8888() versus the Java ConvertNV12toARG8888(). Below is a screenshot of the result:
This sample app shows step-by-step how to create a JNI library in order to allow execution of compute intensive algorithms to happen in native code instead of at the Dalvik Java layer. Executing the app over a course of five runs on a Medfield tablet with an Intel® Atom™ processor running the Android Ice Cream Sandwich OS, we see a very nice and consistent average of 89% improvement when using the native method. Obviously, the very simple data used for conversion in this sample app is not indicative of real data, but it serves its purpose of illustrating the benefit of native code when using compute intensive algorithms. The extra steps required for JNI are not very complicated and as we’ve seen in this sample, using native methods for compute intensive algorithms is an easy way for any ISV to get performance improvement when running on Android.
Christine M. Lin is a Senior Software Engineer working in the Personal Form Factors team of Intel’s Platform Applications Engineering group in SSG. She has a B.S. in Electrical Engineering and Computer Science from the University of California at Berkeley and has worked at Intel for 15 years. In her current position, she works with enabling Atom phones and tablets for both Android and Windows 8.