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.

Image

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.

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 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.

Image

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.

Supporting 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.

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

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