WHAT'S IN THIS CHAPTER?
This chapter focuses on the details of Android NDK and shows you how to build native C/C++ code in Android Studio. Native code is commonly used in Android projects for games and applications, which require high performance face recognition, audio processing, and so on. Although Android NDK is a powerful tool, many Android developers and projects may not need to use it. This chapter does not aim to teach Android NDK from the ground up but focuses instead on how to use Android NDK with the new Android Studio and Gradle.
At the time of this writing, Android NDK integration with Android Studio is still experimental and subject to change. We strongly suggest keeping your tools up-to-date and that you follow the updates to NDK integration if your application relies on NDK.
If you are an Android NDK newbie looking to learn NDK, we suggest you visit http://developer.android.com/ndk
and follow the tutorials and code samples. NDK might look scary if you are not familiar with C/C++; however, it can unleash the full potential of your device's hardware and native libraries, and is well worth the effort.
Android NDK is an essential part of Android development that lets developers use C/C++ code from Java via JNI. Although the history of Android dates back to 2003, by the time the SDK was released, iPhone was already the main player in the mobile market with a growing application store. To compete, Android needed fast adoption in the developer community. Thus, Google's decision to promote Java as the main language was wise and worked quite well. Android activities, UI widgets, and APIs are all designed in a way that can be used through Java. Java and the Dalvik VM did a great job of lowering the learning curve but that solution lacked the performance that most games and some apps need more than they need features like garbage collection.
Android NDK, which was actually released several months after Android SDK, addressed such performance concerns with native code that can be loaded from Java code. When it was released, Android NDK relied on command-line tools, unlike Android SDK, which can be compiled and run via Eclipse IDE.
With the release of Android Studio, NDK was once again left out of the official tool set. At Google I/O 2015, Google finally announced Android NDK support in Android Studio.
NDK integration for Android Studio was announced with version 1.3. At the time of this writing, NDK integration with Android Studio is still beta and relies on an experimental version of the Gradle plugin. Before going forward with NDK use cases in Android Studio, let's see how to install NDK for Linux, Windows 10, and Mac OS X.
Android NDK packages can be accessed at http://developer.android.com/ndk/downloads/index.html
. There you will see the list of packages for Linux, Windows, and Mac OS X.
In this section, you install Android NDK on Ubuntu 14.04. Download the Linux 64-bit version, android-ndk-r11b-linux-x86_64.zip
, from the URL mentioned in the previous section. Next, extract NDK to the Android SDK installation root. for example /
path
/
to
/
Android
/
Sdk
/android-ndk-r11b
.
The zip file contains all required binaries to build native Android code.
You can also install Android NDK from Android Studio's SDK Manager. The easiest way to do this when you have an open project is to select the Project Structure option from the File menu to open the Project Structure window. Select SDK Location, as shown in Figure 11.2.
You will see that the Android NDK location box is empty. You can give the path to the location where you extracted the NDK package or click the Download Android NDK link to have Android Studio install it for you, as shown in Figure 11.3.
Android Studio will install NDK to the ndk-bundle folder in your SDK path.
There are two ways to install Android NDK for Android Studio on Windows 10. You can install it manually by downloading it from https://developer.android.com/ndk/downloads/index.html
or you can install it from Android Studio's Project Structure window.
To install Android NDK from Android Studio, open the Project Structure window, select SDK Location in Android Studio, and click the Download link under Android NDK Location as shown in Figure 11.4. After you accept the license agreement, the Android NDK binaries and libraries will be extracted into the Android SDK ndk-bundle folder.
No further configuration is needed; you are ready to use Android NDK for your project in Windows 10.
If you want to download Android NDK manually, you can select either the 32-bit or the 64-bit version. Choose version that's appropriate for your Windows 10 machine architecture. In our case we downloaded android-ndk-r11b-windows-x86_64.zip
and extracted it to a folder.
When you finish extracting the zip file, enter the path to the folder in the Android NDK Location text box as shown in Figure 11.4. Now you are ready to use Android NDK for your Android Studio project.
Android NDK installation for Mac OS X can be done either by downloading it from https://developer.android.com/ndk/downloads/index.html
or you can open the Project Structure window to download and extract NDK.
If you choose to install manually, navigate to the URL just mentioned and click the link for the android-ndk-r11b-darwin-x86_64.zip
file.
Extract the zip file to the folder where you want to keep NDK files. Then open the Project Structure window and enter the folder's path in the Android NDK Location text box. Now your project is ready to use Android NDK.
Alternatively, Android Studio can download and extract Android NDK automatically. Open the Project Structure window and select SDK Location, then click Download in the Android NDK Location section. After clicking Download, accept the license agreement, then click Next to open the Component Installer window shown in Figure 11.5.
When the installation finishes, the last line in this windows will read “Installation of NDK complete,” and you can click Finish.
Now you can use NDK tools to build native C/C++ code for your application.
Now that NDK is installed, you can create a new project and start adding your native code to it. (Note that Android Studio does not offer a specific wizard or a template to create NDK projects.)
We first cover some common user preferences, how native applications handle where the native code should be stored, how to create the Gradle file, and so on.
In legacy native application development, Android make files are used to build native modules. These make files were used to define the environment variables and the path of the ndk-build binary to build native C/C++ code. However, when developing in Android Studio, the Gradle build system is used to build native code instead of Android make files.
Native code is usually stored under the jni folder, which is at the same level as the java folder. The output of native library code is a shared library, a .so
file. You also need to identify the .so
file location according to its compatible architecture. The main folder for libraries should be named jniLibs. Subfolders, which also need to be named so as to identify the architectural elements they hold (such as mips, x86, armeabi, and so on), need to be created under the jniLibs folder.
In the following section, you work on the sample HelloJNI sample.
Let's open an existing sample application to understand more about how to use Android NDK and Gradle to build native code.
As in earlier chapters, we will import an Android code sample. Open the Welcome to Android Studio window by closing your currently open project; then click Import an Android code sample, as shown in Figure 11.6.
When the Browse Samples window opens, enter ndk in the Select a sample to import box to filter NDK projects, as shown in Figure 11.7.
Select the Hello JNI project, which you will use to learn the basics of NDK development with Android Studio. Click Next to open the Configure Sample window shown in Figure 11.8 where you name the sample application and download source code from GitHub.
Google's code samples for Android Studio are hosted on GitHub; download and import the sample project, as shown in Figure 11.9.
Once the import is complete, expand the java and jni folders to locate Java and C code for the project, as shown in Figure 11.10.
Now check the basic integration of the NDK module into the project. Start by expanding the project scope in the build.gradle file and locate the experimental Gradle plugin declaration, which may introduce a different version number than the one used in the previous section.
At the time of this writing, experimental plugin version 0.7.0-alpha1 is used for classpath
. The version name might be different by the time you read this chapter. The project's Gradle file is shown in the following snippet.
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle-experimental:0.7.0-alpha1'
}
}
allprojects {
repositories {
jcenter()
}
}
Next, expand the build.gradle file of the app module. You should notice that the com.android.model.application
plugin is used instead of the regular com.android.application
plugin. You should also see DSL syntax changes such as model and android.ndk
moduleName
as well as different build flavors to target different platforms and architectures. The jni folder contains the hello-jni.c
source file, which has been declared as an NDK module in project scope in the build .gradle file, as shown in the module's Gradle code in Listing 11.1.
The Hello JNI sample is the most basic use of NDK in Android applications. Its C code returns only a string and will be used in your activity via JNI. Open the Android activity code, shown in Listing 11.2.
This listing demonstrates a simple example but includes all necessary steps to call your C/C++ code from Java. Because your Java code runs on a VM-managed runtime, you need to manually load the non-vm managed code. Static blocks serve well for this purpose because they are initiated even before the constructor and only once for a class.
Java is a type-safe and strongly-typed language, which makes it impossible to see and call the non-vm code even if it is loaded successfully. To be able to call C/C++ code from Java, you would need a placeholder method, which acts as a gateway proxy. Methods, which are marked with the native
keyword, do not have a method body but share a special naming convention with their C/C++ counterpart; thus, they know which native code to execute when C/C++ codes are accessed.
In the example here, the stringFromJNI
method returns a string and is called when the activity is created. That's it. If you run the sample application, the activity will call the C code and display the returned string. Try changing the string returned from the C code and see how the changes are reflected to the UI.
As the list in Figure 11.7 shows, there are multiple sample applications that you can load to learn more about NDK application development. We will give a quick overview of the samples so you can pick one to learn more about the relationship between Android Studio and NDK.
Using Android NDK in an application requires high performance graphics processing, image processing, and audio processing. The following NDK sample applications are mostly focused on showing basic uses of those features with Android NDK. This list of sample applications follows the order shown in Figure 11.7:
Bitmap
class inside NDK to render a plasma effect and open with a JNI interface on an Android device. The Bitmap Plasma application also uses the Gradle Experimental Plugin to build native code under the jni folder.The experimental Gradle plugin, which provides support for NDK, introduces some changes in Gradle DSL. This section teaches you how to use the plugin to migrate an existing NDK project.
Once the import is complete, you can move on to making changes on Gradle files.
Changing the declaration as shown tells Android Studio to use the experimental Gradle plugin for your project. Android Studio will detect the change and ask you to perform project sync.
The build will fail with a message like the one shown in Figure 11.14. Don't worry: You haven't yet completed the necessary changes.
Next, you can update module scope build.gradle file. There are some structural changes you need to implement, introduced by the experimental Gradle plugin. The most major change is the plugin name, which should be the first line in the gradle file. Change apply plugin: com.android.application
to apply plugin: 'com.android.model.application'
. Another major change is the model
block, which wraps the android
block. As you remember from the sample application, you are trying to change your imported project to be usable with the experimental plugin.
model {
android {
…
}
}
The experimental Gradle plugin also introduces some syntactic changes on configurations inside the Android block. You would need to change the proguard configuration to the following:
proguardFiles.add(file("proguard-rules.pro"
))
The full code for the build.gradle module appears in Listing 11.4.
You have completed all the necessary modifications for the new Gradle plugin syntax. However, you are still missing an important artifact—your NDK module name. The NDK module is declared as shown in Listing 11.5.
The experimental Gradle plugin introduces some other syntax changes to Gradle DSL. Although we covered the most common changes, you may refer to http://tools.android.com/tech-docs/new-build-system/gradle-experimental
for the full set of changes.
In order to see more complex NDK project import samples, refer to Chapter 13, where we work on the vendor-provided NDK samples and import them to Android Studio using the rules covered here. Some examples are more complex with external libraries and some are as easy as changing the Gradle file.
Building and packaging Android projects is pretty straightforward and does not introduce any more complexity, provided that your project has already implemented the DSL differences for the experimental Gradle plugin. This section covers some of the Gradle configurations that you may need to configure your builds.
The first change, which we covered in the previous section, is for configuring ProGuard. Because ProGuard is needed both for obfuscation and shrinking your APK, this configuration should be taken as a mandatory one rather than optional.
The experimental Gradle plugin introduces some changes to DSL syntax, which is shown in bold in the following snippet.
proguardFiles.add(file
("proguard-rules.pro"))
Another change we have already seen but not covered in detail is to create and declare productFlavors
. Product flavors are an essential part of working with NDK because native code is sensitive to device architecture unlike VM managed Java code. Product flavors from our sample project that create different products for different platform architectures are listed in Listing 11.6.
By default, Android Studio assumes C/C++ code is placed in the src/main/java
directory. However, C/C++ source code can be customized from Gradle, as shown in Listing 11.7.
Most tool and compiler-related configuration is performed in the model.android.ndk
block. The following items are some of the options you may choose to configure. Please note that these items are case sensitive and should be used as they appear.
moduleName
—Name of NDK module.toolchain
—Toolchain used by NDK, llvm, or gcc. If you write "clang"
on this parameter, the NDK build system will use the LLVM compiler.toolChainVersion
—Version of the toolchain. There might be versions like 3.7, 2.8, and so on. You can change the version with this parameter.CFlags.add("…")
—Environment variables needed by the C compiler.cppFlags("…")
—Environment variables needed by the C++ compiler.ldFlags("…")
—Library flags for the linker.stl "…"
—Standard Template Library options.Finally, you can set ABI specific configurations using a model.android.abis
block. The following code snippet shows disabling SSSE3 instructions for x86 architecture.
android.abis {
create("x86"){
cppFlags.add("-DENABLE_SSSE3")
}
}
Because the Gradle plugin for NDK integration is still experimental, changes to DSL syntax should be expected with new releases of the plugin.
Android NDK projects used to rely on Android make files, and today, many Android projects with NDK modules still use android.mk
files for the build process. However, Gradle offers an easier and single way to manage dependencies, automated tests, and the build/release cycle.
Although Android runs on different architectures, VM managed code, which is usually written in Java, is abstracted from the hosting platform. This gives the ability to deploy the same code to different architectures and delegates the interpretation problem to the VM. However, C/C++ code built with NDK is not managed by the VM and may require additional steps to preserve compatibility.
One main problem with NDK builds was integrating shared library (.so
) files into your project. The new Android Studio and Gradle offers a more flexible way to handle .so
files.
To include a .so
file, create a folder named jniLibs under the src/main
folder. Each target platform architecture is represented with a folder inside jniLibs, as shown in Listing 11.8.
The current jniLibs folder is the default location to place platform-dependent code. However, this location can be changed via Gradle. To declare a custom folder for .so
files, add the following line to your build.gradle.
android {
sourceSets.main {
jniLibs.srcDir 'src/main/libs'
}
}
This declaration will result in the following change to your folder structure:
- src/main
- libs
- amreabi
- mylib.so
- mips
- mylib.so
- x86
- mylib.so
You finished adding your native libs and code to your project, but depending on the size of your native code, you may have introduced another problem to your project by creating a huge monolithic APK.
Packaging all native code for each platform into one APK is not necessarily a bad thing and actually might help keep your builds and versioning simple. However, if the native code and libraries included in your APK grow in size, the size of the APK grows with a multiplier of each platform, which may introduce unnecessary network traffic and disk usage.
By default, Gradle packages all native code and libraries into one fat APK. Basically, if you don't worry about the APK size, you may choose to continue with the defaults. However, if you want to split native code into platform-dependent APKs, you would need to add a product flavor for each APK, as shown in the following code. Version code has to be dynamically adapted when you have multiple APKs
def versionCodeBase = 11;
def versionCodePrefixes = ['armeabi': 1, 'armeabi-v7a': 2, 'arm64-v8a': 3,
'mips: 5, 'mips-64': 6, 'x86: 8, 'x86-64': 9];
android.productFlavors {
create("arm"
) {
ndk.abiFilters.add("armeabi"
)
versionCode = versionCodePrefixes.get("armeabi", 0) * 1000000 +
versionCodeBase
}
create("arm7"
) {
ndk.abiFilters.add("armeabi-v7a"
)
versionCode = versionCodePrefixes.get("armeabi- v7a", 0) * 1000000 +
versionCodeBase
}
create("arm8"
) {
ndk.abiFilters.add("arm64-v8a"
)
versionCode = versionCodePrefixes.get("arm64-v8a ", 0) * 1000000 +
versionCodeBase
}
create("x86"
) {
ndk.abiFilters.add("x86"
)
versionCode = versionCodePrefixes.get("x86", 0) * 1000000 +
versionCodeBase
}
create("x86-64"
) {
ndk.abiFilters.add("x86_64"
)
versionCode = versionCodePrefixes.get("x86_64", 0) * 1000000 +
versionCodeBase
}
create("mips"
) {
ndk.abiFilters.add("mips"
)
versionCode = versionCodePrefixes.get("mips ", 0) * 1000000 +
versionCodeBase
}
create("mips-64"
) {
ndk.abiFilters.add("mips64"
)
versionCode = versionCodePrefixes.get("mips64", 0) * 1000000 +
versionCodeBase
}
Now you can choose any product flavor to build, run, or package platform-specific APKs. If you still need a fat APK among platform-specific ones, add the following product flavor to include all native code in one APK.
android.productFlavors {
create("arm"
) {
ndk.abiFilters.add("armeabi"
)
}
create("arm7"
) {
ndk.abiFilters.add("armeabi-v7a"
)
}
create("arm8"
) {
ndk.abiFilters.add("arm64-v8a"
)
}
create("x86"
) {
ndk.abiFilters.add("x86"
)
}
create("x86-64"
) {
ndk.abiFilters.add("x86_64"
)
}
create("mips"
) {
ndk.abiFilters.add("mips"
)
}
create("mips-64"
) {
ndk.abiFilters.add("mips64"
)
}
// To include all cpu architectures, leaves abiFilters empty
create("fat")
}
Fat APKs can be useful for development and CI builds where platform APKs would help you to have a smaller footprint in terms of network and storage of your app.
In this chapter we covered Android NDK, which is an essential tool to unleash the performance and graphic capabilities of Android. Native code may be needed for deeper hardware integration, so there is no guarantee that an Android developer would never need to learn the basics of NDK. You have seen how Android Studio offers NDK integration via the experimental Gradle plugin. We focused on configuration and differences in Gradle DSL. We also covered how to integrate a piece of C/C++ code with your project.
Finally, we focused on different packaging options for projects that consist of native code and libraries. You learned how to separate platform-dependent code to minimize network and storage usage as well as how to package the app into a fat APK.