25. Using the Android NDK

Although Android applications are primarily written in Java, there are times when developers need or prefer to leverage native C or C++ libraries. The Android Native Development Kit (NDK) provides the tools necessary to include and use native libraries in Android applications. In this chapter, you will learn under what circumstances the Android NDK should be considered and how to configure and use it.

Determining When to Use the Android NDK

Most Android applications are written solely in Java using the Android SDK and run within the Dalvik virtual machine (VM). Most applications run smoothly and efficiently in this fashion. However, there are situations when calling in to native code from Java can be preferable. The Android NDK provides tool-chain support for compiling and using native C and C++ libraries in conjunction with Android Java applications. This is usually done for one of two reasons:

Image To perform processor-intensive operations such as complex physics, which can be implemented more efficiently in C and C++, offering substantial performance improvements.

Image To leverage existing code, usually in the form of shared or proprietary C or C++ libraries, when rewriting is not ideal. This is often the case when trying to support multiple platforms with a single code base.


Image Warning

The native libraries created by the Android NDK can be used only on devices running Android 1.5 and higher. You cannot develop applications that use the Android NDK for older platform versions. Currently Android NDK can be used only on Google TV devices based on Android 4.2.2. Devices running Honeycomb and older are not compatible with the NDK.


Calling in to native code from Java involves some trade-offs. Application developers must consider their application design carefully, weighing the benefits of using the NDK versus the drawbacks, which include:

Image Increased code complexity

Image Increased debugging complexity

Image Performance overhead for each native code call

Image More complex build process

Image Developers required to be versed in Java, C/C++, and JNI concepts

Although developers can write entire applications in C or C++, not all of the Android APIs are available directly in native code. If your application requires complex math, physics, graphics algorithms, or other intensive operations, the Android NDK might be right for your project. Your libraries can take advantage of a number of stable native C and C++ APIs, including:

Image C library headers (libc)

Image Math library headers (libm)

Image The zlib compression library headers (libz)

Image 3D graphics library headers (OpenGL ES 1.1, 2.0, and 3.0)

Image Multimedia and sound libraries (OpenMAX AL and OpenSL ES)

Image A CPU features library for detecting device CPU features at runtime

Image Other headers for C++, logging, JNI, and more

The full list of stable APIs that your code can take advantage of is found in the documentation downloaded with the NDK in the STABLE-APIS.HTML file.

Installing the Android NDK

You can install the Android NDK on Windows, Mac OS X, or Linux operating systems that have the Android SDK and tools properly installed. You also need to install or have already installed:

Image GNU Make 3.81 or later (http://www.gnu.org/software/make/)

Image GNU Awk (Gawk) or Nawk (http://www.gnu.org/software/gawk/)

Image Cygwin 1.7 or later (Windows only, http://www.cygwin.com)

You can download the Android NDK from the Android developer website at http://d.android.com/tools/sdk/ndk/index.html. As of Android NDK r7, the Windows installer contains everything needed to perform all NDK operations, except ndk-gdb, without the need for Cygwin. ndk-gdb.py is an experimental Python implementation of the debugger, which requires a Python installation, if you do not want to install Cygwin on your machine. That is to say, if you don’t need to debug (for instance, on a build machine) or don’t need the use of gdb, no additional tools need to be installed on Windows machines.

Exploring the Android NDK Sample Application

The Android NDK contains a number of different tools and files, specifically:

Image Native system libraries and headers that are forward compatible with the Android platform (1.5 and beyond)

Image Tools for compiling and linking native libraries for ARMv5TE, ARMv7-A, and x86 devices

Image Build files for embedding native libraries into Android applications

Image Native debugging using ndk-gdb

Image NDK documentation in the /docs subdirectory

Image NDK sample applications in the /samples subdirectory

The best way to familiarize yourself with the Android NDK is to build one of the sample applications provided, such as hello-jni. To do this, take the following steps:

1. Build the hello-jni native library, located in the NDK /samples/hello-jni subdirectory, by typing the following on the command line: ndk-build.

2. In the Android IDE, import the existing project from the /samples/hello-jni subdirectory of the NDK installation directory by choosing New, Android Project from Existing Code, and then choosing Browse to locate the root directory of the project; then click Finish. Do a clean build on the project.

3. Create a Debug Configuration for the project.

4. Create an appropriate AVD if necessary. Run the application as normal.

5. If you get errors, you might need to do a “Clean project” in the Android IDE after running an ndk-build clean and ndk-build again. It’s not uncommon for the Android IDE’s state to get out of sync with the build status of the native library.

Creating Your Own NDK Project

Now let’s look at an example of how to set up and use the NDK for your own Android applications using the Android IDE. To create a new Android application that calls into native code, take the following steps:

1. Create a new Android project in the Android IDE.

2. Navigate to your project directory and create a subdirectory called /jni. Inside this directory, place two C files: native_basics.c and native_opengl2.c.

3. In the /jni subdirectory, create a file called Android.mk. A sample Android.mk file might look like this, the one used in our sample application:

Click here to view code image

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_LDLIBS := -llog -lGLESv2
APP_ABI := all
LOCAL_MODULE    := simplendk
LOCAL_SRC_FILES := native_basics.c native_opengl2.c
include $(BUILD_SHARED_LIBRARY)

4. Edit the Android.mk file and make any build changes necessary. By default, only the ARMv5TE instruction set will be targeted. For our sample, this is not necessary. You might want to target a narrower number of devices, but use ARMv7 code to gain native floating-point operations to possibly improve math performance. The value of all for APP_ABI also currently includes x86 architecture targets.

5. Build the native library and embed it in your Android application by navigating back to the project directory and running the ndk-build script. You might need to set up the path to this batch file; the ndk-build script is located in the ndk install directory.

6. In the Android IDE, update your application manifest file. Be sure to set the <uses-sdk> tag with its android:minSdkVersion attribute set to a value of 3 or higher. In our sample, we ultimately add OpenGL ES 2.0, so we’ve set this value to 8.

7. Create an Android IDE Debug Configuration and any necessary AVDs as normal.


Image Tip

Many of the code examples provided in this chapter are taken from the SimpleNDK application. The source code for this application is provided for download on the book’s website.


Calling Native Code from Java

There are three main steps necessary to add a call from Java to native code, as follows:

1. Add a declaration for the new function in your Java class file as a native type.

2. Add a static initializer for the library that the native function will be compiled into.

3. Add the function of the appropriate name, following a specific naming scheme, to the native source file.

This isn’t as complex as it sounds, but we’ll go through each step now. The Simple NDK project has a class called NativeBasicsActivity.java. Let’s start there by adding the declaration for the native function. The following declaration must be added to the class:

private native void basicNativeCall();

Now, make sure that the native library with this function is loaded. This doesn’t need to happen for every call, just for each library. In the Android.mk file, we identified the library as simplendk (the value of LOCALE_MODULE), so we load that library. Add this static initializer to the NativeBasicsActivity class:

static {
    System.loadLibrary("simplendk");
}

Finally, the function needs to be added to the native_basics.c file in the native library that’s being compiled. Each function must follow a specific naming convention. Instead of dots, each part of the function name is separated using an underscore, as follows:

Java_package_name_ClassName_functionName
(JNIEnv *env, jobject this, your vars...);

For the example, that means our function looks like this:

void Java_com_advancedandroidbook_simplendk_NativeBasicsActivity_basicNativeCall
    (JNIEnv *env, jobject this) {
    // do something interesting here
}

That’s a lengthy function name, but your code will not work if you name it incorrectly. This function is now called whenever the Java method declared as basicNativeCall() is invoked. But how will you know? Add the following line to the function and then make sure to include "android/log.h" in the native_basics.c file:

__android_log_print(ANDROID_LOG_VERBOSE, DEBUG_TAG, "Basic call");

And there you have it—your first call from Android Java to native C! If you’re familiar with JNI, you might realize that it’s mostly the same. The main difference is which libraries are available. If you’re familiar with JNI, you should find using the NDK fairly straightforward.

Handling Parameters and Return Values

Now that you can make a basic native call, let’s pass some parameters in to C and then return something. We make a simple little C function that takes a format string that works with the stdio sprintf() call and two numbers to add. The numbers are added and placed in the format string, and a new string is returned. Although simplistic, this demonstrates the handling of Java objects and reminds us that we need to manage memory properly in native C code.

jstring
Java_com_advancedandroidbook_simplendk_NativeBasicsActivity_formattedAddition
    (JNIEnv *env, jobject this, jint number1,
            jint number2, jstring formatString) {
    // get a C string from a Java string object
    jboolean fCopy;
    const char * szFormat = (*env)->GetStringUTFChars(env,
            formatString, &fCopy);
    char * szResult;
    // add the two values
    jlong nSum = number1 + number2;
    // make sure there's ample room for nSum
    szResult = malloc(sizeof(szFormat)+30);
    // make the call
    sprintf(szResult, szFormat, nSum);
    // get a Java string object
    jstring result = (*env)->NewStringUTF(env, szResult);

    // free the C strings
    free(szResult);
    (*env)->ReleaseStringUTFChars(env, formatString, szFormat);
    // return the Java string object
    return(result);
}

The JNI environment object is used for interacting with Java objects. Regular C functions are used for regular C memory management.

Using Exceptions with Native Code

Native code can throw exceptions that the Java side can catch as well as check for exceptions when making calls to Java code. This makes heavy use of the JNIEnv object and might be familiar to those with JNI experience. The following native function throws an exception if the input number parameter isn’t a certain value:

void Java_com_advancedandroidbook_simplendk_NativeBasicsActivity_throwsException
    (JNIEnv * env, jobject this, jint number) {
    if (number < NUM_RANGE_MIN || number > NUM_RANGE_MAX) {
        // throw an exception
        jclass illegalArgumentException =
            (*env)->FindClass(env, "java/lang/IllegalArgumentException");
        if (illegalArgumentException == NULL) {
            return;
        }
        (*env)->ThrowNew(env, illegalArgumentException,
            "What an exceptional number.");
    } else {
        __android_log_print(ANDROID_LOG_VERBOSE, DEBUG_TAG,
            "Nothing exceptional here");
    }
}

The Java declaration for this, as you might expect, needs a throws clause:

private native void throwsException(int num)
    throws IllegalArgumentException;

Basically, the exception class is found through reflection. Then, the ThrowNew() method of the JNIEnv object is used to do the actual throwing of the exception.

To show how to check for an exception in native C code, we need to also show how to call a Java method from C. The following block of code does just that:

void Java_com_advancedandroidbook_simplendk_NativeBasicsActivity_checksException
    (JNIEnv * env, jobject this, jint number) {
    jthrowable exception;
    jclass class = (*env)->GetObjectClass(env, this);
    jmethodID fnJavaThrowsException =
        (*env)->GetMethodID(env, class, "javaThrowsException", "(I)V");
    if (fnJavaThrowsException != NULL) {
        (*env)->CallVoidMethod(env, this, fnJavaThrowsException, number);
        exception = (*env)->ExceptionOccurred(env);
        if (exception) {
            (*env)->ExceptionDescribe(env);
            (*env)->ExceptionClear(env);
            __android_log_print(ANDROID_LOG_ERROR,
                DEBUG_TAG, "Exception occurred. Check LogCat.");
        }
    } else {
        __android_log_print(ANDROID_LOG_ERROR,
            DEBUG_TAG, "No method found");
    }
}

The call to the GetMethodID() function is best looked up in your favorite JNI reference or online. It’s basically a reflective way of getting a reference to the method, but the fourth parameter must be supplied correctly. In this case, it takes a single integer and returns a void.

Because the method returns a void, use the CallVoidMethod() function to actually call it and then use the ExceptionOccurred() function to check to see whether the method threw an exception. If it did, the ExceptionDescribe() function actually writes the exception out to LogCat, but it looks slightly different from a normal exception output. Then the exception is cleared so it doesn’t go any further.

The Java method being called, javaThrowsException(), is defined as follows:

@SuppressWarnings("unused") // is called from native
private void javaThrowsException(int num)
    throws IllegalArgumentException {
    if (num == 42) {
        throw new IllegalArgumentException("Anything but that number!");
    } else {
        Log.v(DEBUG_TAG, "Good choice in numbers.");
    }
}

The use of the @SuppressWarnings option is due to the fact that the method is never called directly from Java, only from native code. You can also use this process of calling Java methods for Android SDK methods. However, remember that the idea of using NDK is generally to improve performance. If you find yourself doing many Android calls, the performance might be improved by simply staying on the Java side of things and leaving algorithmically heavy functionality on the native side.

Using Native Activities

While most uses of Android NDK involve calling native code through JNI, it is possible to build a fully native Activity and, indeed, a fully native application. This is useful for applications that don’t use the Android APIs much, such as ports of existing games. Many APIs are available, however. Read more about creating a fully native application in the NDK documentation file NATIVE_ACTIVITY.HTML.

Improving Graphics Performance

One of the most common reasons to use the Android NDK is to leverage the OpenGL ES 1.1, 2.0, and 3.0 native libraries to perform complex math calculations that benefit from native code and speed up the porting process. Although you can use the Java APIs in the Android SDK to provide OpenGL ES support, some developers with core graphics libraries built in C or C++ might prefer to use the NDK. Here are some tips for developing and using graphics libraries provided with the Android NDK:

Image OpenGL ES 1.1 native libraries are guaranteed on Android 1.6 (API Level 4) and higher; OpenGL ES 2.0 native libraries are guaranteed on Android 2.0 (API Level 5) and higher; OpenGL ES 3.0 native libraries are guaranteed on Android 4.3 (API Level 18) and higher. Make sure you include “GLES2/gl2.h” or “GLES3/gl3.h” and, optionally, “GLES2/gl2ext.h” or “GLES3/gl3ext.h” to get access to the functions. They are named in the standard OpenGL way (for example, glClearColor).

Image Use the <uses-sdk> manifest tag to enforce the minimum SDK supported by the OpenGL ES version your application leverages.

Image Use the <uses-feature> manifest tag to specify which version of OpenGL ES your application leverages so that the Android Market can filter your application and provide it only to compatible devices.

For example, the following block of code is how the drawFrame() method from Chapter 24, “Developing Android 3D Graphics Applications,” would look in the NDK. You can find this code in the SimpleNDK project:

const GLfloat gVertices[] =  {
    0.0f, 0.5f, 0.0f,
    -0.5f, -0.5f, 0.0f,
    0.5f, -0.5f, 0.0f
};

void Java_com_advancedandroidbook_simplendk_NativeOpenGL2Activity_drawFrame
    (JNIEnv * env, jobject this, jint shaderProgram) {
    glClear(GL_COLOR_BUFFER_BIT);
    glUseProgram(shaderProgram);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 12, gVertices);
    glEnableVertexAttribArray(0);
    glDrawArrays(GL_TRIANGLES, 0, 3);
}

This is called from the onDrawFrame() method of our CustomRenderer class. Because this is the code that runs in a tight loop, it makes sense to implement it with native code. Of course, this particular implementation doesn’t benefit at all, but if we had done a bunch of complex math, transformations, and other algorithmically heavy code, it could possibly be faster. Only testing on actual devices can determine for each case what is or isn’t faster, though.

Comparing RenderScript to the NDK

RenderScript is a set of high-performance computation APIs that was introduced in Android 3.0 (API Level 11). RenderScript uses a C language (C99, specifically) for writing scripts. Through intermediate compilation, RenderScript is designed to run on a variety of processor architectures, providing a means of writing performance-critical code that the system later compiles to native code for the processor it can run on. This can be the device CPU, a multicore CPU, GPU, or even DSPs.

Computing with RenderScript

RenderScript is based on the C programming language. If you’re not familiar with C, we recommend that you get familiar with it before trying to use RenderScript. Using RenderScript for heavy computation might be faster to develop and it might perform better than similar NDK solutions (due to automatic distribution across hardware cores). Unlike developing with the Android NDK, you don’t have to worry about the underlying hardware architecture. In fact, when it comes to NDK limitations, such as no Honeycomb support for Google TV, RenderScript has fewer. It does work on Google TV.

RenderScript is used for computational purposes. As one of the reasons for using Android NDK is also for computational purposes, there is some overlap. However, RenderScript has a couple of advantages that are compelling when you’re not using existing C code.

First, unlike NDK code, RenderScript code is compiled for the target device once on the device. This means you don’t have to worry about new CPU architectures that come out. Second, RenderScript makes it easy to leverage multiple cores. Many calls do this automatically, but there are explicit calls for dividing work. Combined, if all you’re looking for from native code is a performance boost, RenderScript might be an excellent choice.

Native RenderScript

Android KitKat 4.4 (API Level 19) introduced a new native C++ API for using RenderScript with the NDK, and any RenderScript code written using the NDK will work on devices with Android 2.2 or higher. Handling performance-intensive tasks from your native code is now possible by using the native RenderScript APIs.


Image Tip

The Android documentation provides a great introduction to computation with RenderScript found here: http://d.android.com/guide/topics/renderscript/index.html. The Android SDK also comes with a few great sample applications demonstrating RenderScript. You can find these samples in the /sdk/samples/android-19/renderscript folder if you are interested in learning more.


Summary

The Android NDK provides helpful tools that enable developers to call into native C and C++ libraries on devices running Android 1.5 and higher. Installing the Android NDK toolset is a relatively straightforward process. Using the Android NDK involves creating build scripts in order to include shared native libraries in your Android application package files. Although the Android NDK is not necessary for every application, certain types of applications might benefit greatly from its use. Android KitKat 4.4 introduced RenderScript APIs accessible through the Android NDK.

Quiz Questions

1. True or false: You should use the NDK when performing memory-intensive operations.

2. What language options are available for programming with the Android NDK?

3. What is the name of the native debugging tool provided with the NDK?

4. What command-line script is used to compile NDK applications?

5. True or false: RenderScript is based on the C++ programming language.

Exercises

1. Read the ANDROID-MK.HTML documentation that comes with the NDK toolkit to determine the three types of source modules you are able to create.

2. Use the NDK documentation to determine what the ndk-depends tool is used for.

3. Write an application that makes use of a native Activity.

References and More Information

Android Tools: “Android NDK”:

http://d.android.com/tools/sdk/ndk/index.html

Android Developers Blog: NDK label:

http://android-developers.blogspot.com/search/label/NDK

YouTube Android Developers Channel: “DevBytes: Play Games and the NDK—Part 1: Setting Up”:

http://www.youtube.com/watch?v=dxI5bReatJw

YouTube Android Developers Channel: “DevBytes: Play Games and the NDK—Part 2: Achievements and Leaderboards”:

http://www.youtube.com/watch?v=zst3R1OP6Y0

YouTube Android Developers Channel: “DevBytes: Play Games and the NDK—Part 3: Threading and Lifecycle”:

http://www.youtube.com/watch?v=K3BKlczc8bU

Google discussion group: “Android NDK”:

http://groups.google.com/group/android-ndk

JNI reference book:

http://docs.oracle.com/javase/6/docs/technotes/guides/jni/spec/jniTOC.html

Android API Guides: “Computation”:

http://d.android.com/guide/topics/renderscript/index.html

Android Developers Blog: RenderScript label:

http://android-developers.blogspot.com/search/label/Renderscript

Android SDK Reference documentation on the RenderScript API:

http://d.android.com/reference/android/renderscript/RenderScript.html

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.23.101.63