The purpose of this document is to outline the necessary steps to set up a development environment for Android application development with NDK integration.
This document was written with the following setup:
- Android Studio version
1.3.2
- Android NDK version
r10e
Although not required specifically for this guide, it is usually important to have the NDK added to your PATH
environment variable. If it is not, add this line to your (assuming bash
is your preferred shell) ~/.bashrc
:
export ANDROID_NDK_HOME="<ANDROID_NDK_INSTALLATION>"
export PATH="$ANDROID_NDK_HOME:$PATH"
Source ~/.bashrc
(if you have any open shells) for the change to take effect:
. ~/.bashrc
If you have previously-built native libraries (*.so
files) that you would like to use in your Android application, you will need to place them under <PROJECT>/<MODULE>/src/main/java/jniLibs/<TARGET_ARCHITECTURE>/
. By default, Android will automatically include native libraries placed in the above directory, categorized by the pre-built's target architecture.
For example, if you're trying to use a native library called mylib
and you have compiled mylib
for armeabi
, armeabi-v7a
, and x86
architectures, then your module directory should look like the following:
<PROJECT>/
+-- <MODULE>/
+-- src/
+-- main/
+-- java/
+-- jniLibs/
+-- armeabi/
+-- libmylib.so
+-- armeabi-v7a/
+-- libmylib.so
+-- x86/
+-- libmylib.so
You can now load and use mylib
in Android by calling (in Java, usually in a static
block):
static {
System.loadLibrary("mylib") // module name, without the trailing ".so" or leading "lib"
}
If you prefer to have your pre-built JNI libraries in another location, you can specify it in your module's build.gradle
file (not the top-level project's build.gradle
!!), under android.sourceSets.main.jniLibs.srcDir
:
android {
// ... android gradle settings
sourceSets.main {
jniLibs.srcDir 'path/to/myJniLibs' // change to a directory of your choice
}
}
The process of writing C/C++ code, compiling them separately, adding the resulting *.so
files to your Android project, and (finally) compiling the Android project can be pretty tedious. Android Studio provides limited NDK integration, which is often sufficient for simpler projects, but can result in unwanted behaviours for more complex ones. In short, we need to disable Android Studio's limited native integration features, and handle NDK support manually by adding changes to our app module's build.gradle
file.
To be able to write C/C++ code and compile them together with your Android app module, you will first need to do the following:
-
Add and set
ndk.dir
property inside the top-level project'slocal.properties
file to point to your NDK installation. For example:ndk.dir=/home/odhita/Software/android-ndk-r10e
-
Create an
Android.mk
makefile in<MODULE>/src/main/jni
with (at the very minimium) the following contents:LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := <MY_NATIVE_MODULE_NAME> LOCAL_SRC_FILES := <MY_NATIVE_MODULE_SOURCE_FILES> include $(BUILD_SHARED_LIBRARY)
This file is used by the build system (such as Gradle) to describe native source code. It is essentially a partial GNU Makefile that glues your C/C++ code to the build system. Please refer to the official Android NDK guide on
Android.mk
for more information. -
Create an
Application.mk
makefile in<MODULE>/src/main/jni
with (at the very minimum) the following contents:APP_ABI := all
Similarly to
Android.mk
,Application.mk
is a partial GNU Makefile that describes native modules required by your Android application. Please refer to the official Android NDK guide onApplication.mk
for more information. -
In the app module's
build.gradle
file, disable Android Studio's automaticndk-build
call by resettingandroid.sourceSets.main.jni.srcDirs
as follows.You will also want to set
android.sourceSets.main.jniLibs.srcDirs
tosrc/main/libs
, since this is where the manualndk-build
call will place its resulting output*.so
files in, and you will want them included in the app's APK:android { // ... android gradle settings sourceSets.main { jni.srcDirs = [] // disable automatic ndk-build call jniLibs.srcDir 'src/main/libs' // ensures your native library will be included in the APK } }
-
In the app module's
build.gradle
file, define functionsgetNdkDir()
andgetNdkBuildCmd()
.getNdkDir()
will use theANDROID_NDK_HOME
environment variable if it is set, otherwise it will use thendk.dir
definition in the top-level project'slocal.properties
file.getNdkCmd()
will return the appropriatendk-build
binary depending on the host OS. Together, these two functions will build a valid path tondk-build
.For the
getNdkBuildCmd()
function to work, you'll need to place animport
statement for the external libraryorg.apache.tools.ant.taskdefs.condition.Os
at the top of thebuild.gradle
file. For reference:import org.apache.tools.ant.taskdefs.condition.Os apply plugin: 'com.android.application' android { // ... android gradle settings } dependencies { // ... module dependencies } def getNdkDir() { if (System.env.ANDROID_NDK_HOME != null) return System.env.ANDROID_NDK_HOME Properties properties = new Properties() properties.load(project.rootProject.file('local.properties').newDataInputStream()) def ndkdir = properties.getProperty('ndk.dir', null) if (ndkdir == null) throw new GradleException("NDK location not found. Define location with ndk.dir in the" + "local.properties file or with an ANDROID_NDK_HOME environment variable.") return ndkdir } def getNdkBuildCmd() { def ndkbuild = getNdkDir() + "/ndk-build" if (Os.isFamily(Os.FAMILY_WINDOWS)) ndkbuild += ".cmd" return ndkbuild }
-
In the app module's
build.gradle
file, underandroid
, define the tasks:ndkClean
and:ndkBuild
. As their names suggest,:ndkClean
cleans your native build output directory, and:ndkBuild
builds your native code:android { // ... android gradle settings task ndkBuild(type: Exec) { workingDir file('src/main') commandLine getNdkBuildCmd() } task ndkClean(type: Exec) { workingDir file('src/main') commandLine getNdkBuildCmd(), 'clean' } }
-
In the app module's
build.gradle
file, underandroid
, add the following task dependencies:android { // ... android gradle settings // ensures ndkBuild task is run before any JavaCompile tasks tasks.withType(JavaCompile) { compileTask -> compileTask.dependsOn ndkBuild } // ensures ndkClean task is run before clean task clean.dependsOn ndkClean }
For completeness, here is how your build.gradle
should look like:
import org.apache.tools.ant.taskdefs.condition.Os
apply plugin: 'com.android.application'
android {
// ... android gradle settings
sourceSets.main {
jniLibs.srcDir 'src/main/libs'
jni.srcDirs = []
}
task ndkBuild(type: Exec) {
workingDir file('src/main')
commandLine getNdkBuildCmd()
}
task ndkClean(type: Exec) {
workingDir file('src/main')
commandLine getNdkBuildCmd(), 'clean'
}
tasks.withType(JavaCompile) {
compileTask -> compileTask.dependsOn ndkBuild
}
clean.dependsOn ndkClean
}
dependencies {
// ... module dependencies
}
def getNdkDir() {
if (System.env.ANDROID_NDK_ROOT != null)
return System.env.ANDROID_NDK_ROOT
Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
def ndkdir = properties.getProperty('ndk.dir', null)
if (ndkdir == null)
throw new GradleException("NDK location not found. Define location with ndk.dir in the" +
"local.properties file or with an ANDROID_NDK_ROOT environment variable.")
return ndkdir
}
def getNdkBuildCmd() {
def ndkbuild = getNdkDir() + "/ndk-build"
if (Os.isFamily(Os.FAMILY_WINDOWS))
ndkbuild += ".cmd"
return ndkbuild
}
At this point, whenever you build or run your app through Android Studio, your native code will also be built and integrated properly with your app, directly as part of the build process.