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.
You will focus on these variables when fine-tuning your application for performance:
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:
Each library has its pros and cons. For example:
gnustl_static
supports C++ exceptions and Run-Time Type Information (RTTI). Support for RTTI in STLport library was added in NDK r7.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”).
)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.
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:
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 sub-directories 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.
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.
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:
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:
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:
recursive()
method is called.recursive()
is called.From a design point of view though, it is recommended you use the Strategy pattern:
if (useNative)
” test.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.
18.191.139.169