Getting Started With the NDK
2.1 What Is In the NDK?
2.2Mixing Java and C/C++ Code
2.3 Creating the Makefiles
2.4 implementing the Native Function
2.5Compiling the Native Library
2.6 Native Activity
2.7 Summary
The Android Native Development Kit (NDK) is a companion to the SDK and is what you use when you want part or all of your Android application to use native code. While bytecode needs to be interpreted by a virtual machine, native code can be directly executed by the device’s processor without any intermediate step, making execution faster, and sometimes much faster. The Dalvik Just-In-Time (JIT) compiler is compiling the bytecode into native code, making your applications faster by having to interpret the code less often (and ideally, only once) since it will use the native code it generated whenever it is available. When you use the NDK, the compilation into native code occurs on your development environment and not on the Android device. You may be wondering why you would need to worry about the NDK since the Dalvik JIT compiler can generate native code dynamically动态 and therefore you could write your application in java using the SDK. This chapter covers the reasons why you may need to use the NDK and the various ways to use it.
There are essentially two ways to use native code and the NDK:
You can write one part of your application in Java and the other part in C/C++.
You can write the whole application in C/C++.
NOTE: NDK support was added in Android 1.5. Today very few devices run Android versions older than 1.5, and it is therefore safe to use the NDK to write part of your application in C/C++.However, writing your entire application in C/C++ requires Android 2.3 or later.This chapter starts by showing you what the NDK is made of. Then, we will take a look at how to mix C/C++ code with Java code in an Android application, and how to make sure the code is optimized for all platforms you want to target. Finally, we’ll delve into a
new class, NativeActivity, introduced in Android 2.3 that allows you to write your whole pplication in C/C++, and we’ll show you a simple example of using sensors in your C/C++ code.
2.1 What Is In the NDK?
The NDK is a set of tools you use to develop native code for your application(应用开发本地代码而生的一套工具). Everything is in a single directory, which you download as an archive file from http://d.android.com/sdk/ndk. For example, the Windows version of the NDK revision
6b contains these directories:
build
docs
platforms
samples
sources
tests
toolchains
A few files are also located at the root of the NDK directory:
documentation.html
GNUmakefile
ndk-build
ndk-gdb
ndk-stack
README.txt
RELEASE.txt
The NDK documentation is nowhere near as thorough as the SDK on
http://d.android.com(和SDK相比,NDK文档少得可怜), so start by opening documentation.html with your favorite web browser. The README text file is also begging you so go ahead and oblige.
The NDK is a collection of six components:
Documentationdao
Header files
C/C++ files
Precompiled libraries 预编译库
Tools to compile, link, analyze, and debug your调试 code Sample applications
Native code is, by definition, specific to a certain architecture. For example, an Intel CPU would not understand ARM instructions指令, and vice versa(反之亦然). Therefore, the NDK includes precompiled libraries for multiple platforms as well as different versions of tools. NDK revision 7 supports three Application Binary Interfaces (ABIs):
armeabi
armeabi-v7a
x86
NOTE: The NDK does not support the ARMv6 ABI.
Most of you are already familiar with the x86 name as it refers to the Intel architecture, a name that is practically ubiquitous普遍存在. The armeabi and armeabi-v7a names may not sound familiar, but you can find ARM-based chips in many products, from washing machines to DVD players, so chances are you used an ARM-based device long before you even heard of Android. Close to 2 billion ARM-based chips(ARM 芯片) were shipped in the second quarter of 2011 alone: 1.1 billion in mobile phones and tablets, and 0.8 billion in other consumer and embedded devices. The term “armeabi” stands for ARM Embedded Application Binary Interface, while v5 and v7a refer to two different architectures. ARM architectures started with v1, and the latest one is v7. Each architecture is used by a family of processor cores, with v5 being used by some ARM7, ARM9, and ARM10 cores, and v7 by the Cortex family. The Cortex series includes A5, A8, A9, and soon A15(即将面世的A15), with the majority of today’s smartphones and tablets using A8 and A9 cores.
The Android NDK does not support the ARMv6 architecture, which is used by the
ARM11 family of processor cores, even though some Android devices use ARM11-
based chipsets. Table 2–1 shows a list of Android devices.
Table 2–1. Some Android Devices and Their Architectures Device Manufacturer CPU Processor family
Blade ZTE Qualcomm MSM7227 ARM11
LePhone Lenovo Qualcomm Snapdragon Based on Cortex A8
Nexus S Samsung Samsung Hummingbird Cortex A8
Xoom Motorola Nvidia Tegra 2 Cortex A9 (dual core)
Galaxy Tab (7’’) Samsung Samsung Hummingbird Cortex A8
Galaxy Tab 10.1 Samsung Nvidia Tegra 2 Cortex A9 (dual core)
Revue (set-top box) Logitech CE4150 (Sodaville) Intel Atom
NSZ-GT1 (Blu-ray player) Sony CE4170 (Sodaville) Intel Atom
While MIPS Technologies announced that a MIPS-based smartphone running Android 2.2 passed the Android CTS back in June 2011, the Android NDK still does not support the MIPS ABI. As of today, ARM is still the dominant architecture主导架构 in Android devices.
NOTE: All Google TV devices released in 2010 (Logitech set-top box机顶盒, Sony TVs, and Blu-ray player蓝光播放机) are based on the Intel CE4100. However, the Google TV platform currently does not support the NDK.
As the NDK is frequently updated, you should always try to use the latest revision. New revisions may improve performance, for example by providing better compilers or more optimized precompiled libraries. New revisions can also fix bugs from previous revisions. When publishing an update to your application, consider rebuilding your C/C++ code with the latest NDK even if you modified only the Java part of your application. However, make sure you always run tests on the C/C++ code! Table 2–2 shows the NDK revisions.
Table 2–2. Android NDK Revisions(修订版)
Revision Date Features
1 June 2009 Android 1.5 NDK, Release 1
Supports ARMv5TE instructions
GCC 4.2.1
2 September
2009
Android 1.6 NDK, Release 1
Adds OpenGL ES 1.1 native library support
3 March 2010 Adds OpenGL ES 2.0 native library support
GCC 4.4.0
4b June 2010 Simplifies build system with ndk-build tool
Simplifies debugging with ndk-gdb tool
Adds supports for armeabi-v7a (Thumb-2, VFP, NEON Advanced SIMD)
Adds API for accessing pixel buffers of Bitmap objects from native code
5c June 2011 Many more native APIs (really, many!)
Adds support for prebuilt libraries
GCC 4.4.3
Fixes issues from revisions 5 (December 2010) and 5b (January 2011).
Revision Date Features
6b August 2011 Adds support for x86 ABI
New ndk-stack tool for debugging
Fixes issues from revision 6 (July 2011).
7 November 20 Native multimedia APIs based on OpenMAX AL 1.0.1
Native audio APIs based on OpenSL 1.0.1
New C++ runtimes (gabi++ and gnustl_shared)
Support for RTTI in STLport
2.2Mixing Java and C/C++ Code
Calling a C/C++ function from Java is actually quite easy but requires several steps:
1. The native method must be declared in your Java code.
2. The Java Native Interface (JNI) glue layer粘合层 needs to be implemented.
3. Android makefiles have to be created.
4. The native method must be implemented in C/C++.
5. The native library must be compiled.
6. The native library must be loaded.
It really is easy in its own twisted扭动 way. We will go through each one of these steps, and by the end of this section, you will know the basics of mixing Java and C/C++. We will discuss the more intricate更复杂 details of the Android makefiles, which allow you to optimize your code even more, in later sections. Since the Android NDK exists for Linux, MacOS X, and Windows (with Cygwin, or without when using NDK revision 7), the specific steps may vary slightly although the overall operations will remain the same. The following steps assume an Android project is already created and you now want to add nativecode to it(已经创建一个Android项目,接下去你要创建本地代码).
2.2.1 Declaring the Native Method
The first step is shown in Listing 2–1 and is rather trivial.
Listing 2–1. Declaration of the Native Method in Fibonacci.java
public class Fibonacci {
public static native long recursiveNative (int n); // note the ‘native’ keyword
}
The native method is simply declared with the native keyword, and no implementation is provided in Java. The method shown above is public, but native methods can be public, protected, private, or package-private, just like any other Java method. Similarly, native methods don’t have to be static methods, and don’t have to use primitive基本 types。only. From the caller’s point of view, a native method is just like any other method. Once it is declared, you can start adding calls to this method in your Java code, and everything will compile just fine. However, if your application runs and calls Fibonacci.recursiveNative, it will crash with an UnsatisfiedLinkError exception. This is expected because you really haven’t done much so far other than declare a function, and the actual implementation of the function does not exist yet.Once your native method is declared, you can start writing the JNI glue layer.
2.2.2 Implementing the JNI Glue Layer
Java uses the JNI framework to call methods from libraries written in C/C++. The Java evelopment Kit (JDK) on your development platform can help you with building the JNI glue layer. First, you need a header file that defines the function you are going to implement. You don’t have to write this header file yourself as you can (and should) use the JDK’s javah tool for that.
In a terminal命令行终端, simply change directories to your application directory, and call javah tocreate the header file you need. You create this header file in your application’s jni directory. Since the jni directory does not exist initially, you have to create it explicitly明确 before you create the header file. Assuming your project is saved in ~/workspace/MyFibonacciApp, the commands to execute are: cd ~/workspace/MyFibonacciApp mkdir jni
javah –classpath bin –jni –d jni com.apress.proandroid.Fibonacci
NOTE: You have to provide the fully qualified name of the class. If javah returns a “Classom.apress.proandroid.Fibonacci not found” error, make sure you specified the right directory with –classpath, and the fully qualified name is correct. The –d option is to specify where the header file should be created. Since javah will need to use Fibonacci.class, make sure your Java application has been compiled before you execute the command. You should now have a header file only a mother could love calledcom_apress_proandroid_Fibonacci.h in ~/workspace/MyFibonacciApp/jni, as shown in
Listing 2–2. You shouldn’t have to modify this file directly. If you need a new version of the file (for example, if you decide to rename the native method in your Java file or add a new one), you can use javah to create it.
Listing 2–2. JNI Header File
/* DO NOT EDIT THIS FILE – it is machine generated */
#include <jni.h>
/* Header for class com_apress_proandroid_Fibonacci */
#ifndef _Included_com_apress_proandroid_Fibonacci
#define _Included_com_apress_proandroid_Fibonacci
#ifdef __cplusplus
extern “C” {
#endif
/*
* Class: com_apress_proandroid_Fibonacci
* Method: recursiveNative
* Signature: (I)J
*/
JNIEXPORT jlong JNICALL
Java_com_apress_proandroid_Fibonacci_recursiveNative
(JNIEnv *, jclass, jint);
#ifdef __cplusplus
}
#endif
#enddif
A C header file alone won’t do you any good though. You now need the implementation of the Java_com_apress_proandroid_Fibonacci_recursiveNative function in a file you will create, com_apress_proandroid_Fibonacci.c, as shown in Listing 2–3.
Listing 2–3. JNI C Source File源文件
#include “com_apress_proandroid_Fibonacci.h”
/*
* Class: com_apress_proandroid_Fibonacci
* Method: recursiveNative
* Signature: (I)J
*/
jlong JNICALL
Java_com_apress_proandroid_Fibonacci_recursiveNative
(JNIEnv *env, jclass clazz, jint n)
{
return 0; // just a stub for now, let’s return 0
}
All functions in the JNI layer (JNICENG)have something in common: their first argument is always of type JNIEnv* (pointer to a JNIEnv object). The JNIEnv object is the JNI environment itself that you use to interact with the virtual machine (should you need to). The secondargument is of type jclass when the method is declared as static, or jobject when it is not.
TIP: Try javah with the –stubs option to generate the C file (javah –classpath bin –
stubs com_apress_proandroid_Fibonacci –d jni). It may work if you are using an old
JDK, although it is likely you’ll get this error message: “Error: JNI does not require stubs, please refer to the JNI documentation”.
2.2.3 Creating the Makefiles
At that point, you most certainly could compile this C++ file into a library using the
NDK’s GCC compiler, but the NDK provides a tool, ndk-build, that can do that for you.To know what to do, the ndk-build tool uses two files that you create:
Application.mk (optional)
Android.mk
You should create both files in the application’s jni directory (where the JNI header and source files are already located已经在里面了). As a source of inspiration when creating these two files, simply refer to existing projects that already define these files. The NDK contains examples of applications using native code in the samples directory, hello-jni being the simplest one. Since Application.mk is an optional file, you won’t find it in every single sample. You should start by using very simple Application.mk and Android.mk files to build your application as fast as possible without worrying about performance for now. Even though Application.mk is optional and you can do without it, a very basic version of the file is shown in Listing 2–4.
Listing 2–4. Basic Application.mk File Specifying One ABI
APP_ABI := armeabi-v7a
This Application.mk specifies only one version of the library should be built, and this
version should target the Cortex family of processors(Cortex系列处理器). If no Application.mk is provided, a single library targeting the armeabi ABI (ARMv5) will be built, which would be equivalent相当 to defining an Application.mk file, as shown in Listing 2–5.
Listing 2–5. Application.mk File Specifying armeabi As Only ABI
APP_ABI := armeabi
Android.mk in its simplest form is a tad more verbose冗长 as its syntax语句 is dictated口述, in part, by the tools that will be used to eventually compile the library. Listing 2–6 shows a basic version of Android.mk.
Listing 2–6. Basic Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := fibonacci
LOCAL_SRC_FILES := com_apress_proandroid_Fibonacci.c
include $(BUILD_SHARED_LIBRARY)
The file must start with the definition of the local path, where Android.mk is located. The Android NDK provides several macros宏 you can use in your makefiles, and here we use the my-dir macro, which returns the path of the last included makefile. In our case, the last included makefile is simply Android.mk in ~/workspace/MyFibonacciApp/jni, and
therefore LOCAL_PATH will be set to ~/workspace/MyFibonacciApp.
The second line is simply to clear all the LOCAL_XXX variables, except LOCAL_PATH. If you forget that line, the variables could be defined incorrectly. You want your build to start in a predictable state可预见模式, and therefore you should never forget to include that line in Android.mk before you define a module.
LOCAL_MODULE simply defines the name of a module, which will be used to generate the name of the library. For example, if LOCAL_MODULE is set to fibonacci, then the shared library will be libfibonacci.so. LOCAL_SRC_FILES then lists all the files to be compiled, in this case only com_apress_proandroid_Fibonacci.c (the JNI glue layer) as we haven’t implemented the actual Fibonacci function yet. Whenever you add a new file, always remember to add it to LOCAL_SRC_FILES or it won’t be compiled into the library.
Finally, when all the variables are defined, you need to include the file that contains the rule to actually build the library. In this case, we want to build a shared library, and therefore we include $(BUILD_SHARED_LIBRARY).
While虽然 this may seem convoluted复杂, at first you will only need to worry about defining LOCAL_MODULE and LOCAL_SRC_FILES as the rest of the file is pretty much boilerplate样板.
For more information about these makefiles, refer to the Application.mk and Android.mk sections of this chapter.
2.2.4 implementing the Native Function实现本地函数
Now that the makefiles are defined, we need to complete the C implementation by
creating fibonacci.c, as shown in Listing 2–7, and calling the newly implemented
function from the glue layer, as shown in Listing 2–8. Because the function implemented in fibonacci.c needs to be declared before it can be called, a new header file is also created, as shown in Listing 2–9. You will also need to add fibonacci.c to the list of files to compile in Android.mk.
Listing 2–7. Implementation of the New Function in fibonacci.c
#include “fibonacci.h”
uint64_t recursive (unsigned int n)
{
if (n > 1) return recursive(n-2) + recursive(n-1);
return n;
}
Listing 2–8. Calling the Function From the Glue Layer
#include “com_apress_proandroid_Fibonacci.h”
#include “fibonacci.h”
/*
* Class: com_apress_proandroid_Fibonacci
* Method: recursiveNative
* Signature: (I)J
*/
jlong JNICALL
Java_com_apress_proandroid_Fibonacci_recursiveNative
(JNIEnv *env, jclass clazz, jint n)
{
return recursive(n);
}
Listing 2–9. Header File fibonacci.h
#ifndef _FIBONACCI_H_
#define _FIBONACCI_H_
#include <stdint.h>
extern uint64_t recursive (unsigned int n);
#endif
NOTE: Make sure you use the right types in your C/C++ code as jlong is 64-bit. Use welldefined良好定义 types such as uint64_t or int32_t when size matters.
Some may argue that using multiple files creates unnecessary complexity复杂性, and everything could be implemented in the glue layer, that is, in a single file instead of three or four (fibonacci.h, fibonacci.c, com_apress_proandroid_Fibonacci.c, and possibly even com_apress_proandroid_Fibonacci.h). While this is technically feasible可实现的, as s in Listing 2–10, it is not recommended. Doing this would tightly couple the glue layer with the implementation of the native function, making it harder to reuse the code in a non-Java application. For example, you may want to reuse the same header and C/C++ files in an iOS application. Keep the glue layer JNI-specific, and JNI-specific only.
While you may also be tempted to remove the inclusion包含 of the JNI header file, keeping it as it guarantees your functions are consistent函数实现 with what is defined in the Java layer (assuming you remember to recreate the header file with the javah tool whenever there is a relevant change in Java).
Listing 2–10. All Three Files Combined Into One
#include “com_apress_proandroid_Fibonacci.h”
#include <stdint.h>
static uint64_t recursive (unsigned int n)
{
if (n > 1) return recursive(n-2) + recursive(n-1);
return n;
}
/*
* Class: com_apress_proandroid_Fibonacci
* Method: recursiveNative
* Signature: (I)J
*/
jlong JNICALL
Java_com_apress_proandroid_Fibonacci_recursiveNative
(JNIEnv *env, jclass clazz, jint n)
{
return recursive(n);
}
2.2.5Compiling the Native Library 编译本地库
Now that the C implementation is complete, we can finally build the shared library by calling ndk-build from the application’s jni directory.
TIP: Modify your PATH environment variable to include the NDK directory so you can call ndkbuild and other scripts easily without having to specify the command’s full path指定命令的全路劲.
The result is a shared library called libfibonacci.so in the lib/armeabi directory. You
may have to refresh the project in Eclipse to show the newly created libraries. If you
compile and run the application, and the application calls Fibonacci.recursiveNative, it will again crash with an UnsatisfiedLinkError exception. This is a typical error as many developers simply forget to explicitly load显示地加载 the shared library from the Java code: the virtual machine is not clairvoyant先知 yet and needs to be told what library to load. This is achieved by a call to System.loadLibrary(), as shown in Listing 2–11.
Listing 2–11. Loading the Library in Static Initialization Block
public class Fibonacci {
static {
System.loadLibrary(“fibonacci”); // to load libfibonacci.so
}
public static native long recursiveNative (int n);
}
Loading the Native Library
Calling System.loadLibrary from within a static initialization block is the easiest way to load a library. The code in such a block is executed when the virtual machine loads the class and before any method is called. A potential, albeit即使 quite uncommon, performance issue is if you have several methods in your class, and not all of them require everything to be initialized (for example, shared libraries loaded). In other words, the static initialization block can add significant overhead显著增加 that you would want to avoid for certain functions, as shown in Listing 2–12.
Listing 2–12. Loading the Library in the Static Initialization Block
public class Fibonacci {
static {
System.loadLibrary(“fibonacci”); // to load libfibonacci.so
// do more time-consuming things here, which would delay the execution of
superFast
}
44 CHAPTER 2: Getting Started With the NDK
public static native long recursiveNative (int n);
public long superFast (int n) {
return 42;
}
}
NOTE: The time it takes to load a library also depends on the library itself (its size and number of
methods, for example).
So far, we have seen the basics of mixing Java and C/C++. While native code can
improve performance, the difference is in part due to how the C/C++ code is compiled. In fact, many compilation options编译选项 exist, and the result may vary greatly depending on which options are used.
The following two sections tell you more about the options you can define in the
Application.mk and Android.mk makefiles, which until now were very basic.
Application.mk The Application.mk file shown in Listing 2–4 is one of the simplest of its kind. However, this file can specify quite a few more things, and you may need to define many of them in your application. Table 2–3 shows the different variables you can define in Application.mk.
Table 2–3. Variables in Application.mk
Variable Meaning
APP_PROJECT_PATH Project path
APP_MODULES List of modules to compile
APP_OPTIM Defined as “release” or “debug”
APP_CFLAGS Compiler flags for C and C++ files
APP_CXXFLAGS Obsolete, use APP_CPPFLAGS instead
APP_CPPFLAGS Compiler flags for C++ files only
APP_BUILD_SCRIPT To use a build script other than jni/Android.mk
APP_ABI List of ABIs to compile code for
APP_STL The C++ Standard Library to use, defined as “system,”
“stlport_static,” “stlport_shared,” or “gnustl_static”
STLPORT_FORCE_REBUILD To build the STLport library from scratch instead of using the
precompiled one, set to true
You will focus on these variables when fine-tuning your application for performance:
APP_OPTIM
APP_CFLAGS
APP_CPPFLAGS
APP_STL
APP_ABI
APP_OPTIM is optional and can be set to either “release” or “debug.” If it is not defined, it will be automatically set based on whether your application is debuggable调试模式 (android:debuggable set to true in the application’s manifest): if the application is debuggable, then APP_OPTIM would be set to “debug”; otherwise, it would be set to “release.” Since it makes sense to build libraries in debug mode when you want to debug your application, the default behavior should be deemed acceptable for most cases, and therefore most of the time you would not need or want to explicitly define
APP_OPTIM in your Application.mk.
APP_CFLAGS (C/C++) and APP_CPPFLAGS (C++ only) define the flags passed to the compiler. They don’t necessarily specify flags to optimize the code as they could simply be used to include a path to follow to find include files (for example, APP_CFLAGS += - I$(LOCAL_PATH)/myincludefiles). Refer to the gcc documentation for an exhaustive详细 list of flags. The most typical performance-related flags would be the –Ox series, where x specifies the optimization level, from 0 for no optimization to 3, or –Os. However, in most cases, simply defining APP_OPTIM to release, or not defining APP_OPTIM at all should be sufficient足够 as it will choose an optimization level for you, which should produce
acceptable results.
APP_STL is used to specify which standard library the application should use. For
example, four possible values are defined in NDK revision 6:
system
stlport_static
stlport_shared
gnustl_static
Each library has its pros优点 and cons缺点. For example:
Only gnustl_static supports C++ exceptions and Run-Time Type Information (RTTI).
Support for RTTI in STLport library was added in NDK r7. Use stlport_shared if multiple shared native libraries use the C++ library. (Remember to load the library explicitly with a call to System.loadLibrary(“stlport_shared”).)
Use stlport_static if you have only one shared native library in your application (to avoid loading the library dynamically动态.
You can enable C++ exceptions and RTTI by adding –fexceptions and –frtti to
APP_CPPFLAGS respectively.
2.3.1 Optimizing For (Almost) All Devices 为所有设备优化
If the performance of your application depends heavily on the performance of the C++ library, test your application with different libraries and choose the best one. The choice may not be solely based on performance though, as you have to consider other parameters too, such as the final size of your application or the features you need from the C++ library (for example, RTTI).
The library we compiled above (libfibonacci.so) was built for the armeabi ABI. Two
issues now surface出现两个问题:
While the native code is compatible with the armeabi-v7a ABI, it is not optimized for the Cortex family of processors.
The native code is not compatible with the x86 ABI.
The Cortex family of processors is more powerful than the processors based on the
older ARMv5 architecture. One of the reasons it is more powerful is because new
instructions were defined in the ARMv7 architecture, which a library built for ARMv5 will not even use. As the compiler was targeting the armeabi ABI, it made sure it would not use any instruction that an ARMv5-based processor would not understand. Even though your library would be compatible with a Cortex-based device, it would not fully take advantage of the CPU and therefore would not realize its full performance potential(也不能充分发挥它的性能潜力).
NOTE: There are many reasons why the ARMv7 architecture is more powerful than ARMv5, and the instruction set is only one of them. Visit the ARM website (http://www.arm.com) for more information about their various architectures.
The second issue is even more serious as a library built for an ARM ABI simply could not be used on an x86-based device. If the native code is mandatory必须 for the application to function, then your application won’t work on any Intel-based Android device. In our case, System.loadLibrary(“fibonacci”) would fail with an UnsatisfiedLinkError exception, meaning the library could not be loaded.
These two issues can easily be fixed though, as APP_ABI can be defined as a list of
ABIs to compile native code for, as shown in Listing 2–12. By specifying multiple ABIs, you can guarantee the native code will not only be generated for all these architectures, but also optimized for each one of them.
Listing 2–12. Application.mk Specifying Three ABIs
APP_ABI := armeabi armeabi-v7a x86
After recompiling your application with this new Application.mk, the lib directory will contain three sub-directories. In addition to armeabi, it will contain two new subdirectories named armeabi-v7a and x86. As you will have easily guessed, the two new directories refer to the two new ABIs the application now supports. Each of these three directories contains a file called libfibonacci.so.
TIP: Use ndk-build –B V=1 after you edit Application.mk or Android.mk to force a rebuild of your libraries and display the build commands. This way you can always verify your changes have the desired effect on the build process.
The application file is much bigger now because it contains three instances三个 of the “same” library, each targeting a different ABI. The Android package manager will determine which one of these libraries to install when the application is installed on the device. The Android system defines a primary ABI and, optionally, a secondary ABI. The primary ABI is the preferred ABI, that is, the package manager will first install libraries that target the primary ABI. If a secondary ABI is defined, it will then install the libraries that target the secondary ABI and for which there is no equivalent library targeting the primary ABI. For example, a Cortex-based Android device should define the primary ABI as armeabi-v7a and the secondary ABI as armeabi.
Table 2–4 shows the primary and
secondary ABIs for all devices.
Table 2–4. Primary and Secondary ABIs
Android system Primary ABI Secondary ABI
ARMv5-based armeabi not defined
ARMv7-based armeabi-v7a armeabi
x86-based x86 not defined
The secondary ABI provides a means for newer Android devices to maintain
compatibility with older applications as the ARMv7 ABI is fully backward compatible
with ARMv5.
NOTE: An Android system may define more than primary and secondary ABIs in the future, for example if ARM designs a new ARMv8 architecture that is backward compatible向前兼容 with ARMv7 and ARMv5.
2.3.2Supporting All Devices支持所有设备
An issue remains though. Despite the fact that the application now supports all the ABIs supported by the NDK, Android can (and most certainly will) be ported被移植 onto new architectures. For example, we mentioned a MIPS cellphone earlier. While Java’s premise is “write once, run everywhere” (the bytecode is target-agnostic平台独立, and one does not need to recompile the code to support new platforms), native code is target-specific针对目标平台, and none of the three libraries we generated would be compatible with a MIPS-based Android system. There are two ways to solve this problem:
You can compile the new library and publish an update to your application as soon as an NDK supports a new ABI.
You can provide a default Java implementation to be used when the package manager fails to install the native code.
The first solution is rather trivial 容易as it only involves installing the new NDK, modifying your application’s Application.mk, recompiling your application, and publishing the update (for example, on Android Market). However, the official Android NDK may not always support all ABIs Android has already been ported on or will be ported on. As a consequence, it is recommended you also implement the second solution; in other words, a Java implementation should also be provided.
NOTE: MIPS Technologies provides a separate NDK, which allows you to build libraries for the MIPS ABI. Visit http://developer.mips.com/android for more information.
Listing 2–13 shows how a default Java implementation can be provided when loading the library fails.
Listing 2–13. Providing a Default Java Implementation
public class Fibonacci {
private static final boolean useNative;
static {
boolean success;
try {
System.loadLibrary(“fibonacci”); // to load libfibonacci.so
success = true;
} catch (Throwable e) {
success = false;
}
useNative = success;
}
public static long recursive (int n) {
if (useNative) return recursiveNative(n);
return recursiveJava(n);
}
private static long recursiveJava (int n) {
if (n > 1) return recursiveJava(n-2) + recursiveJava(n-1);
return n;
}
private static native long recursiveNative (int n);
}
An alternative design is to use the Strategy pattern策略模式:
Define a strategy interface.
Define two classes that both implement the interface (one using native code, the other one using only Java).
Instantiate举例 the right class based on the result of System.loadLibrary().
Listing 2–14 shows an implementation of this alternative design.
Listing 2–14. Providing a Default Java Implementation Using the Strategy Pattern
// FibonacciInterface.java
public interface FibonacciInterface {
public long recursive (int n);
}
// Fibonacci.java
public final class FibonacciJava implements FibonacciInterface {
public long recursive(int n) {
if (n > 1) return recursive(n-2)+recursive(n-1);
return n;
}
}
// FibonacciNative.java
public final class FibonacciNative implements FibonacciInterface {
static {
System.loadLibrary("fibonacci");
}
public native long recursive (int n);
}
// Fibonacci.java
public class Fibonacci {
private static final FibonacciInterface fibStrategy;
static {
FibonacciInterface fib;
try {
fib = new FibonacciNative();
} catch (Throwable e) {
fib = new FibonacciJava();
}
fibStrategy = fib;
}
public static long recursive (int n) {
return fibStrategy.recursive(n);
}
}
NOTE: Since the native function is now declared in FibonacciNative.java instead of
Fibonacci.java, you will have to create the native library again, this time using
com_apress_proandroid_FibonacciNative.c and
com_apress_proandroid_FibonacciNative.h.
(Java_com_apress_proandroid_FibonacciNative_recursiveNative would be the
name of the function called from Java.) Using the previous library would trigger an
UnsatisfiedLinkError exception.
While there are minor differences between the two implementations as far as
performance is concerned, they are irrelevant enough to be safely ignored:
The first implementation requires a test every single time the recursive() method is called.
The second implementation requires an object allocation分配 in the static initialization block and a call to a virtual function when recursive() is called.
From a design point of view though, it is recommended you use the Strategy pattern:
You only have to select the right strategy once, and you don’t take the risk of forgetting an “if (useNative)” test. You can easily change strategy by modifying only a couple of lines of code.
You keep strategies in different files, making maintenance easier.
Adding a method to the strategy interface forces you to implement the methods in all implementations.
As you can see, configuring Application.mk is not necessarily a trivial不值一提 task. However, you will quickly realize that you are using the same parameters most of the time for all your applications, and simply copying one of your existing Application.mk to your new application will often do the trick.
2.4 Android.mk
While Application.mk allows you to specify variables common to your whole application, Android.mk is used to specify what modules模块 you want to build and how to build them, all of this in excruciating detail. Table 2–5 lists the available variables in NDK revision 6.
Table 2–5. Variables You Can Define in Android.mk
Variable Meaning
LOCAL_PATH Path of Android.mk, can be set to $(call my-dir)
LOCAL_MODULE Name of the module
LOCAL_MODULE_FILENAME Redefinition of library name (optional)
LOCAL_SRC_FILES Files to compile in module
LOCAL_CPP_EXTENSION Redefinition of file extension of C++ source files
(default is .cpp)
LOCAL_C_INCLUDES List of paths to append to the include search path
LOCAL_CFLAGS Compiler flags for C and C++ files
LOCAL_CXXFLAGS Obsolete, use LOCAL_CPPFLAGS instead
LOCAL_CPPFLAGS Compiler flags for C++ files only
LOCAL_STATIC_LIBRARIES List of static libraries to link to module
LOCAL_SHARED_LIBRARIES List of shared libraries the module depends on at
runtime
LOCAL_WHOLE_STATIC_LIBRARIES Similar to LOCAL_STATIC_LIBRARIES, but uses the --
whole-archive flag
LOCAL_LDLIBS List of additional linker flags (for example, –lGLESv2 to
link with the OpenGL ES 2.0 library)
LOCAL_ALLOW_UNDEFINED_SYMBOLS Allow undefined symbols by setting this variable to
true (default is false)
LOCAL_ARM_MODE The mode (ARM or Thumb) the binaries will be
compiled in
LOCAL_ARM_NEON Allow the use of NEON Advanced SIMD
instructions/intrinsics
LOCAL_DISABLE_NO_EXECUTE Disable NX bit (default is false, that is NX enabled)
LOCAL_EXPORT_CFLAGS
LOCAL_EXPORT_CPPFLAGS
LOCAL_EXPORT_C_INCLUDES
LOCAL_EXPORT_LDLIBS
Used to export the variables to modules depending on
this module (that is, list the module in
LOCAL_STATIC_LIBRARY or
LOCAL_SHARED_LIBRARY)
LOCAL_FILTER_ASM Allow a shell command to be executed on assembly files
52 CHAPTER 2: Getting Started With the NDK
Once again, we are going to focus on the few variables that have an impact on
performance:
LOCAL_CFLAGS
LOCAL_CPPFLAGS
LOCAL_ARM_MODE
LOCAL_ARM_NEON
LOCAL_DISABLE_NO_EXECUTE
LOCAL_CFLAGS and LOCAL_CPPFLAGS are similar to APP_CFLAGS and APP_CPPFLAGS, but apply only to the current module, whereas the flags defined in Application.mk are for all the modules. It is recommended you actually don’t set optimization levels in Android.mk but instead rely on APP_OPTIM in Application.mk.
LOCAL_ARM_MODE can be used to force the binaries to be generated编译 in ARM mode, that is, using 32–bit instructions. While code density may suffer compared to Thumb mode (16- bit instructions), performance may improve as ARM code tends to be faster than Thumb code. For example, Android’s own Skia library is explicitly明确 compiled in ARM mode. Obviously, this only applies to ARM ABIs, that is, armeabi and armeabi-v7a. If you want to compile only specific files using ARM mode, you can list them in LOCAL_SRC_FILES with the .arm suffix, for example, file.c.arm instead of file.c. LOCAL_ARM_NEON specifies whether you can use Advanced SIMD instruction or intrinsics内联 in your code, and whether the compiler can generate NEON instructions in the native code. While the performance can be dramatically明显 improved with NEON instructions, NEON was only introduced in the ARMv7 architecture and is an optional component可选组件. Consequently所以, NEON is not available on all devices. For example, the Samsung Galaxy Tab 10.1 does not support NEON but the Samsung Nexus S does. Like LOCAL_ARM_MODE,
support for NEON can be for individual files单个文件, and the .neon suffix is used. Chapter 3 covers涵盖 the NEON extension and provides sample code.
TIP: You can combine the .arm and .neon suffixes in LOCAL_SRC_FILES, for example,
file.c.arm.neon. If both suffixes后缀 are used, make sure .arm is used first or else it won’t compile.
The LOCAL_DISABLE_NO_EXECUTE does not have any impact on performance in itself. However, expert developers may be interested in disabling the NX bit when code is enerated dynamically (most likely to achieve much better performance). This is not a common thing to do, and you probably won’t ever have to specify that flag in your Android.mk as the NX bit is enabled by default. Disabling the NX bit is also considered a security risk.
Android.mk can specify multiple modules, and each module can use different flags and different source files. Listing 2–15 shows such an Android.mk file, compiling two
modules with different flags for each.
Listing 2–15. Two Modules Specified In Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := fibonacci
LOCAL_ARM_MODE := thumb
LOCAL_SRC_FILES := com_apress_proandroid_Fibonacci.c fibonacci.c
include $(BUILD_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := fibonarmcci
LOCAL_ARM_MODE := arm
LOCAL_SRC_FILES := com_apress_proandroid_Fibonacci.c fibonacci.c
include $(BUILD_SHARED_LIBRARY)
Like Application.mk, Android.mk can be configured in many ways. Choosing the right values for variables in these two files can be the key to achieving good performance without having recourse to more advanced and complicated optimizations. With the NDK always evolving发展, you should refer to the latest online documentation as new variables can be added in new releases while others may become deprecated. When a new NDK is released, it is recommended you recompile your application and publish an update, especially when the new release comes with a new compiler.
NOTE: Test your application again after you have recompiled it with a different tool chain (that is, the new SDK or NDK).
2.5 Performance Improvements With C/C++
Now that you know how to combine Java and C/C++ code, you may think that C/C++ is always preferred over Java to achieve best performance. This is not true, and native code is not the answer to all your performance problems(本地代码不是所有性能问题的解决方案). Actually, you may sometimes experience a performance degradation性能下降 when calling native code. While this may sound
surprising, it really shouldn’t as switching from Java space to native space is not without any cost. The Dalvik JIT compiler will also produce native code, which may be equivalent to or possibly even better than your own native code.
Let’s consider the Fibonacci.computeIterativelyFaster() method from Chapter 1 and its C implementation, as shown in Listing 2–16.
Listing 2–16. Iterative C Implementation of Fibonacci Series
uint64_t computeIterativelyFaster (unsigned int n)
{
if (n > 1) {
uint64_t a, b = 1;
n--;
a = n & 1;
n /= 2;
while (n-- > 0) {
a += b;
54 CHAPTER 2: Getting Started With the NDK
b += a;
}
return b;
}
return n;
}
}
As you can see, the C implementation is very similar to the Java implementation, except for the use of unsigned types无符号类型. You can also observe the Dalvik bytecode shown in Listing 2–17 looks similar to the ARM native code shown in Listing 2–18, generated with the NDK’s objdump tool. Among other things, the objdump NDK tool allows you to disassemble反编译 a binary二进制 file (object file, library, or executable) and display the assembler mnemonics汇编助记符. This tool is very much like dexdump, which basically performs the same operations but on .dex files (for example, an application’s classes.dex file).
NOTE: Use objdump’s –d option to disassemble a file, for example, objdump –d
libfibonacci.so. Execute objdump without any option or parameter to see the list of all supported options. The NDK comes with different versions of objdump: one for the ARM ABIs and one for the x86 ABI.
Listing 2–17. Dalvik Bytecode of Fibonacci.iterativeFaster
0008e8: |[0008e8] com.apress.proandroid.Fibonacci.iterativeFaster:(I)J
0008f8: 1215 |0000: const/4 v5, #int 1 // #1
0008fa: 3758 1600 |0001: if-le v8, v5, 0017 // +0016
0008fe: 1602 0100 |0003: const-wide/16 v2, #int 1 // #1
000902: d808 08ff |0005: add-int/lit8 v8, v8, #int -1 // #ff
000906: dd05 0801 |0007: and-int/lit8 v5, v8, #int 1 // #01
00090a: 8150 |0009: int-to-long v0, v5
00090c: db08 0802 |000a: div-int/lit8 v8, v8, #int 2 // #02
000910: 0184 |000c: move v4, v8
000912: d808 04ff |000d: add-int/lit8 v8, v4, #int -1 // #ff
000916: 3c04 0400 |000f: if-gtz v4, 0013 // +0004
00091a: 0425 |0011: move-wide v5, v2
00091c: 1005 |0012: return-wide v5
00091e: bb20 |0013: add-long/2addr v0, v2
000920: bb02 |0014: add-long/2addr v2, v0
000922: 0184 |0015: move v4, v8
000924: 28f7 |0016: goto 000d // -0009
000926: 8185 |0017: int-to-long v5, v8
000928: 28fa |0018: goto 0012 // -0006
Listing 2–18. ARM Assembly Code of C Implementation of iterativeFaster
00000410 <iterativeFaster>:
410: e3500001 cmp r0, #1 ; 0x1
414: e92d0030 push {r4, r5}
418: 91a02000 movls r2, r0
41c: 93a03000 movls r3, #0 ; 0x0
420: 9a00000e bls 460 <iterativeFaster+0x50>
424: e2400001 sub r0, r0, #1 ; 0x1
428: e1b010a0 lsrs r1, r0, #1
42c: 03a02001 moveq r2, #1 ; 0x1
CHAPTER 2: Getting Started With the NDK 55
430: 03a03000 moveq r3, #0 ; 0x0
434: 0a000009 beq 460 <iterativeFaster+0x50>
438: e3a02001 mov r2, #1 ; 0x1
43c: e3a03000 mov r3, #0 ; 0x0
440: e0024000 and r4, r2, r0
444: e3a05000 mov r5, #0 ; 0x0
448: e0944002 adds r4, r4, r2
44c: e0a55003 adc r5, r5, r3
450: e0922004 adds r2, r2, r4
454: e0a33005 adc r3, r3, r5
458: e2511001 subs r1, r1, #1 ; 0x1
45c: 1afffff9 bne 448 <iterativeFaster+0x38>
460: e1a01003 mov r1, r3
464: e1a00002 mov r0, r2
468: e8bd0030 pop {r4, r5}
46c: e12fff1e bx lr
NOTE: Refer参照 to http://infocenter.arm.com for a complete documentation of the ARM instruction set.
The assembly code汇编码 is what is going to be executed by the CPU. Since the Dalvik bytecode looks a lot like the assembly code (even though the assembly code is more compact紧凑), one could infer that the native code the Dalvik Just-In-Time compiler will generate should be pretty close to the native code shown in Listing 2–18. Also, if the bytecode were significantly different from the assembly code, the Dalvik JIT compiler may still generate native code very similar to the assembly code the NDK generated.
Now, to be able to compare these methods, we need to run some tests. Actual
performance evaluation needs empirical实验 evidence, and we are going to test and
compare four items:
Java implementation without JIT compiler
Java implementation with JIT compiler
Native implementation (debug)
Native implementation (release)
The test skeleton (found in Fibonacci.java) is shown in Listing 2–19, and results are
shown in Figure 2–1 and Figure 2–2.
Listing 2–19. Test Skeleton
static {
System.loadLibrary(“fibonacci_release”); // we use two libraries
System.loadLibrary(“fibonacci_debug”);
}
private static final int ITERATIONS = 1000000;
private static long testFibonacci (int n)
{
long time = System.currentTimeMillis();
56 CHAPTER 2: Getting Started With the NDK
for (int i = 0; i < ITERATIONS; i++) {
// call iterativeFaster(n), iterativeFasterNativeRelease(n) or
interativeFasterNativeDebug(n)
callFibonacciFunctionHere(n);
}
time = System.currentTimeMillis() - time;
Log.i(“testFibonacci”, String.valueOf(n) + “ >> Total time: ” + time + “
milliseconds”);
}
private static void testFibonacci ()
{
for (int i = 0; i < 92; i++) {
testFibonacci(i);
}
}
private static native long iterativeFasterNativeRelease (int n);
private static native long iterativeFasterNativeDebug (int n);
Figure 2–1 shows the duration of the test in milliseconds for each of the four
implementations listed above. Figure 2–2 shows the relative performance of the four
implementations with the baseline being the Java implementation with JIT compiler
enabled.
Figure 2–1. The performance of different implementations of iterativeFaster()
Figure 2–2. The performance of different implementations of iterativeFaster() relative to a JIT-enabled Java implementation
We can draw a few conclusions:
The Dalvik JIT compiler can increase performance significantly. (TheJIT-enabled version is 3 to 6 times faster than JIT-disabled version.)
The native implementation is not always faster than the JIT-enabled
Java version.
The more time spent in the native space, the more diluted稀释 the Java/native transition cost is.
Google’s own tests showed the Dalvik JIT compiler could improve performance by a factor of 5 with CPU-intensive code, and our own results here confirm that. The
performance gain will depend on the code依赖代码完成的工作 though, so you should not always assume such a ratio不能总是依赖这样假定的比率. This is important to measure if you still target older devices running a JITless version of Android (Android 2.1 or earlier). In some cases, using native code is the only option to provide an acceptable user experience on older devices.
More About JNI
The JNI glue layer we used was extremely simple as all it did was to call another C
function. Unfortunately, it won’t be as easy all the time as things get more complicated when non-primitive types非原始类型 are used and when the native code needs to access fields or methods域或者方法 from the Java object or class. On the plus side, everything you do in the JNI glue layer will be quite mechanical.
1 Strings
Working with strings in both Java and C/C++ can often lead to performance problems. Java’s String uses 16-bit Unicode characters (UTF-16) while many C/C++ functions simply use char* to refer to strings (that is, strings in C/C++ are most of the time ASCII or UTF-8). Nostalgic怀旧的 developers may even use the EBCDIC encoding for obfuscation混淆 purposes. That being said, Java strings have to be converted to C/C++ strings before they can be used. A simple example is shown in Listing 2–20.
Listing 2–20. Java Native Method Using String and JNI Glue Layer
// Java (in Myclass.java)
public class MyClass {
public static native void doSomethingWithString (String s);
}
// JNI glue layer (in C file)
void JNICALL
Java_com_apress_proandroid_MyClass_doSomethingWithString
(JNIEnv *env, jclass clazz, jstring s)
{
const char* str = (*env)->GetStringUTFChars(env, s, NULL);
if (str != NULL) {
// do something with str string here
// remember to release the string to avoid memory leaks galore
(*env)->ReleaseStringUTFChars(env, s, str);
}
}
The JNI offers multiple methods to work with strings, and they all pretty much work the same way:
The Java String must be converted to a C/C++ string.
C/C++ string must be released.
Table 2–6 shows the various string get/release methods the JNI provides, together with a short description
Table 2–6. JNI Get/Release String Methods
Get Release Description
GetStringChars ReleaseStringChars Gets a pointer to a UTF-16 string (may
require memory allocation)
GetStringUTFChars ReleaseStringUTFChars Gets a pointer to a UTF-8 string (may
require memory allocation)
GetStringCritical ReleaseStringCritical Gets a pointer to a UTF-16 string (may
require memory allocation, restrictions on
what you can do between calls to
GetStringCritical and ReleaseStringCritical)
GetStringRegion n/a Copies part of a string to a pre-allocated
buffer (UTF-16 format, no memory
allocation)
GetStringUTFRegion n/a Copies part of a string to a pre-allocated buffer (UTF-8 format, no memory allocation)
Since memory allocations内存分配 are never free, you should favor the GetStringRegion and GetStringUTFRegion in your code whenever possible. By doing so, you:
Avoid possible memory allocations.
Copy only the part of the String you need in a pre-allocated buffer
(possibly in the stack).
Avoid having to release the string, and avoid forgetting about
releasing the string.
NOTE: Refer to the online JNI documentation and the NDK’s jni.h header file for more information about other String functions.
2. Accessing Fields or Methods 访问域或者方法
You can access fields and methods from Java objects or classes from within the JNI
glue layer, however it is not as simple as accessing a field or calling a function of a C++ object or class. Fields and methods of Java objects or classes are accessed by id. To access a field or call a method, you need to:
Get the id of this field or method.
Use a JNI function to set/get the field or call the method.
An example is shown in Listing 2–21.
Listing 2–21. Modifying a Field and Calling a Method From the JNI Glue Layer
// Java (in MyClass.java)
public class MyClass {
static {
System.loadLibrary(“mylib”);
}
public static int someInteger = 0;
public static native void sayHelloToJNI();
public static void helloFromJNI() {
Log.i(“MyClass”, “Greetings! someInteger=” + someInteger);
}
}
// JNI glue layer (in C file)
void JNICALL
Java_com_apress_proandroid_MyClass_sayHelloToJNI
(JNIEnv *env, jclass clazz)
{
// we get the ids for the someInteger field and helloFromJNI method
jfieldID someIntegerId = (*env)->GetStaticFieldID(env, clazz, “someInteger”, “I”);
jfieldID helloFromJNIId = (*env)->GetStaticMethodID(env, clazz, “helloFromJNI”,
“()V”);
// we increment someInteger
jint value = (*env)->GetStaticIntField(env, clazz, someIntegerId);
(*env)->SetStaticIntField(env, clazz, value + 1);
// we call helloFromJNI
(*env)->CallStaticVoidMethod(env, clazz, helloFromJNIId);
}
For performance reasons, you don’t want to retrieve获取 the field or method ids every single time you need to access a field or call a method. The field and method ids are set when the class is loaded by the virtual machine and are valid有效的 only as long as the class is still loaded. If the class is unloaded by the virtual machine and reloaded again, the new ids may be different from the old ones. That being said, an efficient approach is to retrieve the ids when the class is loaded, that is, in the static initialization block大厦, as shown in
Listing 2–22.
Listing 2–22. Retrieving Field/Method Ids Only Once
// Java (in MyClass.java)
public class MyClass {
static {
System.loadLibrary(“mylib”);
getIds(); // we get the ids only once when the class is loaded
}
public static int someInteger = 0;
public static native void sayHelloToJNI();
public static void helloFromJNI() {
Log.i(“MyClass”, “Greetings! someInteger=” + someInteger);
}
private static native void getIds();
}
// JNI glue layer (in C file)
static jfieldID someIntegerId;
static jfieldID helloFromJNIId;
void JNICALL
Java_com_apress_proandroid_MyClass_sayHelloToJNI
(JNIEnv *env, jclass clazz)
{
// we do not need to get the ids here anymore
// we increment someInteger
jint value = (*env)->GetStaticIntField(env, clazz, someIntegerId);
(*env)->SetStaticIntField(env, clazz, value + 1);
// we call helloFromJNI
(*env)->CallStaticVoidMethod(env, clazz, helloFromJNIId);
}
void JNICALL
Java_com_apress_proandroid_MyClass_getIds
(JNIEnv *env, jclass clazz)
{
// we get the ids for the someInteger field and helloFromJNI method
someIntegerId = (*env)->GetStaticFieldID(env, clazz, “someInteger”, “I”);
helloFromJNIId = (*env)->GetStaticMethodID(env, clazz, “helloFromJNI”, “()V”);
}
The JNI defines tons of functions you can use to access fields and call methods. For
example, accessing an integer field and accessing a Boolean field are two operations
that are done with two different functions. Similarly, different functions are defined to call a static method and a non-static method.
NOTE: Refer to the online JNI documentation and the NDK’s jni.h header file for a complete list of functions you can use.
Android defines its own set of functions and data structures to access the most
common classes used in native code. For example, the APIs defined in android/bitmap.h (introduced in NDK release 4b) allow access to the pixel buffers of bitmap objects:
AndroidBitmap_getInfo
AndroidBitmap_lockPixels
AndroidBitmap_unlockPixels
NDK revision 5 introduced many new APIs application developers can use from native code to access parts of the Android Java framework, without relying on JNI
idiosyncrasies特性 (JNIEnv, jclass, jobject, for example).
2.6 Native Activity
So far, we have seen how to mix Java and C/C++ in a single application. Android 2.3 goes one step further and defines the NativeActivity class, which allows you to write the whole application in C/C++ but does not force you to, as you still have access to the whole Android Java framework through JNI.
NOTE: You do not have to use NativeActivity for all activities in your application. For example, you could write an application with two activities: one NativeActivity and one ListActivity.
If you prefer to read source files or header files in lieu代替 of more formal documentation, you are in for a treat. In fact, most of the documentation is contained in the header files, which you can find in the NDK’s platforms/android-9/arch-arm/usr/include/android
directory. The list of header files is shown in Table 2–7.
Table 2–7. Header Files That Native Activities Will Use
Header file Content
api-level.h Definition of __ANDROID_API__
asset_manager.h Asset Manager APIs
asset_manager_jni.h API to convert Java object (AssetManager) to native object
bitmap.h Bitmap APIs
configuration.h Configuration APIs
input.h Input APIs (devices, keys, gestures, etc)
keycodes.h Definition of all key codes (e.g. AKEYCODE_SEARCH)
log.h Log APIs
looper.h Convenience APIs to handle events
native_activity.h Where many things start
native_window.h Window APIs
Header file Content
native_window_jni.h API to convert Java object (Surface) to native object
obb Opaque Binary Blob APIs (see Java’s StorageManager)
rect Definition of ARect type
sensor Sensor APIs (accelerometer, gyroscope, etc)
storage_manager.h Storage Manager APIs
window.h Window flags (see Java’s WindowManager.LayoutParams)
Creating a native activity is quite simple. The first step is to define your application’s manifest file to let Android know you are going to use a native activity. For that you need to specify two things in your application’s AndroidManifest.xml for each native activity:
The class to instantiate
The library to load and its entry point
The first item is actually no different from other non-native activities. When your activity is created, Android needs to instantiate the right class, and this is what android:name is for inside the <activity> tag. In most cases there is no need for your application to extend the NativeActivity class, so you will almost always use
android.app.NativeActivity as the class to instantiate. Nothing prevents you from
instantiating a class of your creation that extends NativeActivity though.
The second item is for Android to know what library contains your activity’s native code so that it can be loaded automatically when the activity is created. That piece of information is given to Android as metadata as a name/value pair: the name has to be set to android.app.lib_name, and the value specifies the name of the library without the lib prefix or .so suffix. Optionally另外, you can also specify the library’s entry point as name/value metadata, the name being set to android.app.func_name, and the value to the function name. By default, the function name is set to ANativeActivity_onCreate.
An example of a manifest file is shown in Listing 2–23. The minimum SDK version is set to 9 as NativeActivity was introduced in Android 2.3, and the activity’s native code is in libmyapp.so.
Listing 2–23. The Native Application’s AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.apress.proandroid"
android:versionCode="1"
android:versionName="1.0">
<uses-sdk android:minSdkVersion="9" />
<application android:icon="@drawable/icon"
android:label="@string/app_name"
64 CHAPTER 2: Getting Started With the NDK
android:hasCode="false">
<activity android:name="android.app.NativeActivity"
android:label="@string/app_name">
<meta-data android:name="android.app.lib_name"
android:value="myapp" />
<meta-data android:name="android.app.func_name"
android:value="ANativeActivity_onCreate" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
NOTE: Optionally, if your application does not contain any Java code, you can set
android:hasCode to false in the <application> tag.
Launching this application now would result in a runtime error as the libmyapp.so does not exist yet. Consequently, the next step is to build this missing library. This is done as usual using the NDK’s ndk-build tool.
2.6 Building the Missing Library 构建缺失的库
You have to define your Application.mk file as well as your Android.mk file. When using native activities, the difference lies in Android.mk, as shown in Listing 2–24. You also need a file that contains the actual implementation of the application, myapp.c (shown in
Listing 2–25).
Listing 2–24. The Native Application’s Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := myapp
LOCAL_SRC_FILES := myapp.c
LOCAL_LDLIBS := -llog -landroid
LOCAL_STATIC_LIBRARIES := android_native_app_glue
include $(BUILD_SHARED_LIBRARY)
$(call import-module,android/native_app_glue)
The differences between this Android.mk and the one we previously used are:
The shared libraries to link with
The static library to link with
Since your native activity is going to use the Android native application APIs, you need to add –landroid to LOCAL_LDLIBS. You may need to link with more libraries depending on what you are going to use. For example, –llog is for linking with the logging library to allow you to use the logcat debugging facility工具.
The Android NDK provides a simpler way to create a native application, which is
implemented in the NDK’s native_app_glue module. To use this module you need to not only add it to LOCAL_STATIC_LIBRARIES, but also import it into your project by using the import-module function macro, as indicated by the last line in Listing 2–24.
NOTE: The native_app_glue module is implemented in the NDK, and the source code is located in the android-ndk-r7/sources/android/native_app_glue directory. You are
free to modify the implementation and compile your application using your own modified version as the library is linked statically静态库链接到应用.
Listing 2–25 shows an example of an application, implemented in a single file myapp.c, which listens to the following events:
Application events (similar to methods like onStart in Java)
Input events (key, motion)
Accelerometer
Gyroscope 陀螺仪(callback-based基于回调)
This application does not do anything meaningful other than enabling sensors and
showing you how to process sensor events. In this particular case, the sensor values are displayed with a call to __android_log_print. Use this application as the skeleton for your own needs.
Listing 2–25. Implementation of myapp.c
#include <android_native_app_glue.h>
#include <android/sensor.h>
#include <android/log.h>
#define TAG "myapp"
typedef struct {
// accelerometer
const ASensor* accelerometer_sensor;
ASensorEventQueue* accelerometer_event_queue;
// gyroscope
const ASensor* gyroscope_sensor;
ASensorEventQueue* gyroscope_event_queue;
} my_user_data_t;
static int32_t on_key_event (struct android_app* app, AInputEvent* event)
{
// use AKeyEvent_xxx APIs
return 0; // or 1 if you have handled the event
}
static int32_t on_motion_event (struct android_app* app, AInputEvent* event)
66 CHAPTER 2: Getting Started With the NDK
{
// use AMotionEvent_xxx APIs
return 0; // or 1 if you have handled the event
}
// this simply checks the event type and calls the appropriate function
static int32_t on_input_event (struct android_app* app, AInputEvent* event)
{
int32_t type = AInputEvent_getType(event);
int32_t handled = 0;
switch (type) {
case AINPUT_EVENT_TYPE_KEY:
handled = on_key_event(app, event);
break;
case AINPUT_EVENT_TYPE_MOTION:
handled = on_motion_event(app, event);
break;
}
return handled;
}
// some functions not yet implemented
static void on_input_changed (struct android_app* app) {}
static void on_init_window (struct android_app* app) {}
static void on_term_window (struct android_app* app) {}
static void on_window_resized (struct android_app* app) {}
static void on_window_redraw_needed (struct android_app* app) {}
static void on_content_rect_changed (struct android_app* app) {}
// we enable the sensors here
static void on_gained_focus (struct android_app* app)
{
my_user_data_t* user_data = app->userData;
if (user_data->accelerometer_sensor != NULL) {
ASensorEventQueue_enableSensor(
user_data->accelerometer_event_queue,
user_data->accelerometer_sensor);
ASensorEventQueue_setEventRate(
user_data->accelerometer_event_queue,
user_data->accelerometer_sensor, 1000000L/60);
}
if (user_data->gyroscope_sensor != NULL) {
ASensorEventQueue_enableSensor(
user_data->gyroscope_event_queue,
user_data->gyroscope_sensor);
ASensorEventQueue_setEventRate(
user_data->gyroscope_event_queue,
user_data->gyroscope_sensor, 1000000L/60);
}
}
// we disable the sensors when focus is lost
static void on_lost_focus (struct android_app* app)
{
my_user_data_t* user_data = app->userData;
if (user_data->accelerometer_sensor != NULL) {
ASensorEventQueue_disableSensor(
user_data->accelerometer_event_queue,
user_data->accelerometer_sensor);
}
if (user_data->gyroscope_sensor != NULL) {
ASensorEventQueue_disableSensor(
user_data->gyroscope_event_queue,
user_data->gyroscope_sensor);
}
}
// more functions to implement here…
static void on_config_changed (struct android_app* app) {}
static void on_low_memory (struct android_app* app) {}
static void on_start (struct android_app* app) {}
static void on_resume (struct android_app* app) {}
static void on_save_state (struct android_app* app) {}
static void on_pause (struct android_app* app) {}
static void on_stop (struct android_app* app) {}
static void on_destroy (struct android_app* app) {}
// this simply checks the command and calls the right function
static void on_app_command (struct android_app* app, int32_t cmd) {
switch (cmd) {
case APP_CMD_INPUT_CHANGED:
on_input_changed(app);
break;
case APP_CMD_INIT_WINDOW:
on_init_window(app);
break;
case APP_CMD_TERM_WINDOW:
on_term_window(app);
break;
case APP_CMD_WINDOW_RESIZED:
on_window_resized(app);
break;
case APP_CMD_WINDOW_REDRAW_NEEDED:
on_window_redraw_needed(app);
break;
case APP_CMD_CONTENT_RECT_CHANGED:
on_content_rect_changed(app);
break;
case APP_CMD_GAINED_FOCUS:
on_gained_focus(app);
break;
case APP_CMD_LOST_FOCUS:
on_lost_focus(app);
break;
case APP_CMD_CONFIG_CHANGED:
on_config_changed(app);
break;
case APP_CMD_LOW_MEMORY:
on_low_memory(app);
break;
case APP_CMD_START:
on_start(app);
break;
case APP_CMD_RESUME:
on_resume(app);
break;
case APP_CMD_SAVE_STATE:
on_save_state(app);
break;
case APP_CMD_PAUSE:
on_pause(app);
break;
case APP_CMD_STOP:
on_stop(app);
break;
case APP_CMD_DESTROY:
on_destroy(app);
break;
}
}
// user-defined looper ids
#define LOOPER_ID_USER_ACCELEROMETER (LOOPER_ID_USER + 0)
#define LOOPER_ID_USER_GYROSCOPE (LOOPER_ID_USER + 1)
// we’ll be able to retrieve up to 8 events at a time
#define NB_SENSOR_EVENTS 8
static int gyroscope_callback (int fd, int events, void* data)
{
// not really a good idea to log anything here as you may get more than you wished
for…
__android_log_write(ANDROID_LOG_INFO, TAG, "gyroscope_callback");
return 1;
}
static void list_all_sensors (ASensorManager* sm)
{
ASensorList list;
int i, n;
n = ASensorManager_getSensorList(sm, & list);
for (i = 0; i < n; i++) {
const ASensor* sensor = list[i];
const char* name = ASensor_getName(sensor);
const char* vendor = ASensor_getVendor(sensor);
int type = ASensor_getType(sensor);
int min_delay = ASensor_getMinDelay(sensor);
float resolution = ASensor_getResolution(sensor);
__android_log_print(
ANDROID_LOG_INFO, TAG, "%s (%s) %d %d %f",name, vendor, type, min_delay,
resolution);
}
}
// where things start…
void android_main (struct android_app* state)
{
my_user_data_t user_data;
ASensorManager* sm = ASensorManager_getInstance();
app_dummy(); // don't forget that call
// we simply list all the sensors on the device
list_all_sensors(sm);
state->userData = & user_data;
state->onAppCmd = on_app_command;
state->onInputEvent = on_input_event;
// accelerometer
user_data.accelerometer_sensor =
ASensorManager_getDefaultSensor(sm, ASENSOR_TYPE_ACCELEROMETER);
user_data.accelerometer_event_queue = ASensorManager_createEventQueue(
sm, state->looper, LOOPER_ID_USER_ACCELEROMETER, NULL, NULL);
// gyroscope (callback-based)
user_data.gyroscope_sensor =
ASensorManager_getDefaultSensor(sm, ASENSOR_TYPE_GYROSCOPE);
user_data.gyroscope_event_queue = ASensorManager_createEventQueue(
sm, state->looper, LOOPER_ID_USER_GYROSCOPE, gyroscope_callback, NULL);
while (1) {
int ident;
int events;
struct android_poll_source* source;
while ((ident = ALooper_pollAll(-1, NULL, &events, (void**)&source)) >= 0) {
// “standard” events first
if ((ident == LOOPER_ID_MAIN) || (ident == LOOPER_ID_INPUT)) {
// source should not be NULL but we check anyway
if (source != NULL) {
// this will call on_app_command or on_input_event
source->process(source->app, source);
}
}
// accelerometer events
if (ident == LOOPER_ID_USER_ACCELEROMETER) {
ASensorEvent sensor_events[NB_SENSOR_EVENTS];
int i, n;
while ((n = ASensorEventQueue_getEvents(
user_data.accelerometer_event_queue, sensor_events,
NB_SENSOR_EVENTS)) > 0) {
for (i = 0; i < n; i++) {
ASensorVector* vector = & sensor_events[i].vector;
__android_log_print(
ANDROID_LOG_INFO, TAG,
"%d accelerometer x=%f y=%f z=%f", i, vector->x, vector->y,
vector->z);
}
}
}
// process other events here
// don’t forget to check whether it’s time to return
if (state->destroyRequested != 0) {
ASensorManager_destroyEventQueue(sm,
user_data.accelerometer_event_queue);
ASensorManager_destroyEventQueue(sm, user_data.gyroscope_event_queue);
return;
}
}
// do your rendering here when all the events have been processed
}
}
2.6.2 Alternative
Another way to create a native application is to implement your native version of
onCreate in which you not only initialize your application but also define all your other callbacks (that is, the equivalent of onStart, onResume, to name just a few). This is exactly what the native_app_glue module implements for you to simplify your own development. Also, the native_app_glue module guarantees certain events are handled in a separate thread, allowing your application to remain responsive. Should you decide to define your own onCreate implementation, you would not need to link with the native_app_glue library, and instead of implementing android_main, you would implement ANativeActivity_onCreate, as shown in Listing 2.26.
Listing 2–26. Implementation of ANativeActivity_onCreate
#include <android/native_activity.h>
void ANativeActivity_onCreate (ANativeActivity* activity, void* savedState, size_t
savedStateSize)
{
// set all callbacks here
activity->callbacks->onStart = my_on_start;
activity->callbacks->onResume = my_on_resume;
…
// set activity->instance to some instance-specific data
activity->instance = my_own_instance; // similar to userData
// no event loop here, it simply returns and NativeActivity will then call the
callbacks you defined
}
While this may appear simpler, it becomes more complicated when you start listening to some other events (such as sensors) and draw things on the screen.
TIP: Do not change the name of the library’s entry point in your manifest file if you are using the native_app_glue module as it implements ANativeActivity_onCreate. The new NativeActivity class in itself does not improve performance. It is simply a mechanism to make native application development easier. In fact, you could implement that same mechanism in your own application to write native applications on older Android versions. Despite the fact that your application is, or can be, fully written in C/C++, it still runs in the Dalvik virtual machine, and it still relies on the NativeActivity Java class.
2.7 Summary
We have seen how the use of native code can improve performance. Even though
carefully crafted雕琢 native code rarely results in a degradation of降低 performance, performance is not the only reason why you should use the NDK. The following are all good reasons
why you should use it:
You want to reuse existing code instead of having to rewrite everything in Java
重用现有代码,而不是Java中重写一遍。
You want to write new code to use on other platforms that don’t support Java.
编写新的代码,让他在不支持java的平台使用
You want to target older Android devices that do not have a Just-In- Time compiler (Android 2.1 and earlier), and native code is the only way to offer a good user experience.
Using native code in your application makes the user experience better, even on Android devices with a JIT compiler.
The first two reasons are so important you may actually be willing to sacrifice
performance in some cases (although make sure the user experience does not suffer, or at least does not suffer beyond an acceptable threshold). Like many developers, your resources are limited, yet you want to reach as many people as possible with your applications. Limiting yourself to a single platform won’t maximize your investments局限在单一平台不会让你的收益最大化.
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。