Our Android Studio / NDK series continues with details on how to create a Gradle-oriented NDK project that references statically compiled native libraries, a feature currently unsupported by the current Gradle plugin.
Using an Android.mk NDK compilation process with pre-built native libraries
In part 1 of this series, I laid out the basic steps to implement a basic NDK module in Android Studio using the built-in Gradle Plugin NDK support. At the outset, however, I mentioned that my client needed to utilize PRE-BUILT (*.a) libraries containing proprietary code. Normally, this would be as simple as just adding a directive to the Android.mk file, but since Gradle automatically generates the Android.mk file deep in the .idea folder, we have less control over how that file is written and it’s relative location. While we are awaiting on Google for a more elegant solution to this issue, I had to move Android Studio back to an older NDK compilation model using the Android.mk makefile.
The first thing I did was put some of my local directories in a properties file so that I could add them individually to source control and let other developers on my team install the ndk and keystores where they wanted:
properties.gradle (main)
ndk_build_path="/path/to/ndk/ndk-build"
keystore_path="/path/to/application.keystore"
Next, I updated the build.gradle to ‘hide’ the jni files from the automatic NDK build so that I could run my custom ndk-build command (and utilize the Android.mk makefile to get finer control over how the build will run).
NOTE: You won’t be able to see your jni folder in the ‘Android’ project view in Android Studio, but the tradeoff (successful compilation and linking) is worth it… I tried experimenting with moving around the jni.srcDirs variable during compilation time but Android Studio didn’t fall for my cheap tricks...
build.gradle (complex module)
apply from: '../properties.gradle'
apply plugin: 'com.android.library'
android {
compileSdkVersion 19
buildToolsVersion "20.0.0"
defaultConfig {
applicationId "com.sdgsystems.examples.android.ndk.ndkmodule"
minSdkVersion 16
targetSdkVersion 19
versionCode 1
versionName "1.0"
// We need to set the libs dir for the output and the srcdirs to null to
// prevent the gradle ndk hooks from firing. We need to depend on Android.mk
// NOTE: this appears to break the android studio 'android' project view
// (at least, it hides the jni directory…)
sourceSets.main {
//Tell Gradle where to put the compiled library
jniLibs.srcDir 'src/main/libs'
//hide the ‘jni’ folder so that the automatic gradle build doesn’t try to run
//it’s own ndk-build process
jni.srcDirs = [];
}
}
//Signing your application consistently with a keystore that you control
//is a great habit to form
signingConfigs {
mySigningConfig {
keyAlias '<keyalias>'
keyPassword '<keypassword>'
//pull the keystore path from properties.gradle
storeFile file(keystore_path)
storePassword '<storepassword>'
}
}
buildTypes {
release {
runProguard false
signingConfig signingConfigs.mySigningConfig
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
signingConfig signingConfigs.mySigningConfig
}
}
// Tell Gradle the run the ndkBuild task when compiling
tasks.withType(JavaCompile) {
compileTask -> compileTask.dependsOn ndkBuild
}
// This task utilizes the Android.mk file defined in src/main/jni so that you
// have more control over the build parameters (like library inclusion)
task ndkBuild(type: Exec) {
commandLine ndk_build_path, '-C', file('src/main/jni').absolutePath
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile project(':usbSerialForAndroid') // <- this is a very handy library… Contact us
// if you need to talk to a USB device
// in Android
}
Now that I’m depending on an Android.mk file to build my NDK project, I have to make one:
(If you’re pulling in a legacy project, you probably already have this and is why you’re reading this article…)
Android.mk (main/src/jni)
LOCAL_PATH := $(call my-dir)
### Pull in a pre-built library
include $(CLEAR_VARS)
LOCAL_MODULE := libPrecompiledLib
# this libs path is relative to my jni files, so, src/main/jni/libs/libPrecompiledLib.a
LOCAL_SRC_FILES := libs/$(TARGET_ARCH_ABI)/libPrecompiledLib.a
include $(PREBUILT_STATIC_LIBRARY)
include $(CLEAR_VARS)
### Pull in our local code
# Recognize these from our ndk block in the simple build.gradle?
LOCAL_CFLAGS += -std=c99
LOCAL_MODULE := NdkModule
## You can (unadvisedly) mix in cpp with your c code… I’m doing it here because
## my client’s pre-built headers and structs were in cpp but I want to keep the JNI
## pieces in c...
SRC_FILES := $(wildcard $(LOCAL_PATH)/*.cpp)
LOCAL_SRC_FILES := $(SRC_FILES:$(LOCAL_PATH)/%=%) main.c
# link our new library to the static library we referred to a few lines back
LOCAL_STATIC_LIBRARIES := PrecompiledLib
## Pull in some android helper libraries
LOCAL_LDLIBS := -llog -landroid
include $(BUILD_SHARED_LIBRARY)
There you go, sync up your gradle files and build your library to get a fully linked native library available to your application.
In part 3 of this series, I will discuss Using Binder and Messenger to create an asynchronous native process and consuming it with a standard Android activity.
Ben Friedberg, Lead Software Engineer
Comments