JNI and Gradle in Android Studio
Gradle Build Tools 2.2.0+ - The closest the NDK has ever come to being called 'magic'
In trying to avoid experimental and frankly fed up with the NDK and all its hackery I am happy that 2.2.x of the Gradle Build Tools came out and now it just works. The key is the externalNativeBuild
and pointing ndkBuild
path argument at an Android.mk
or change ndkBuild
to cmake
and point the path argument at a CMakeLists.txt
build script.
android {
compileSdkVersion 19
buildToolsVersion "25.0.2"
defaultConfig {
minSdkVersion 19
targetSdkVersion 19
ndk {
abiFilters 'armeabi', 'armeabi-v7a', 'x86'
}
externalNativeBuild {
cmake {
cppFlags '-std=c++11'
arguments '-DANDROID_TOOLCHAIN=clang',
'-DANDROID_PLATFORM=android-19',
'-DANDROID_STL=gnustl_static',
'-DANDROID_ARM_NEON=TRUE',
'-DANDROID_CPP_FEATURES=exceptions rtti'
}
}
}
externalNativeBuild {
cmake {
path 'src/main/jni/CMakeLists.txt'
}
//ndkBuild {
// path 'src/main/jni/Android.mk'
//}
}
}
For much more detail check Google's page on adding native code.
After this is setup correctly you can ./gradlew installDebug
and off you go. You will also need to be aware that the NDK is moving to clang since gcc is now deprecated in the Android NDK.
Android Studio Clean and Build Integration - DEPRECATED
The other answers do point out the correct way to prevent the automatic creation of Android.mk
files, but they fail to go the extra step of integrating better with Android Studio. I have added the ability to actually clean and build from source without needing to go to the command-line. Your local.properties
file will need to have ndk.dir=/path/to/ndk
apply plugin: 'com.android.application'
android {
compileSdkVersion 14
buildToolsVersion "20.0.0"
defaultConfig {
applicationId "com.example.application"
minSdkVersion 14
targetSdkVersion 14
ndk {
moduleName "YourModuleName"
}
}
sourceSets.main {
jni.srcDirs = [] // This prevents the auto generation of Android.mk
jniLibs.srcDir 'src/main/libs' // This is not necessary unless you have precompiled libraries in your project.
}
task buildNative(type: Exec, description: 'Compile JNI source via NDK') {
def ndkDir = android.ndkDirectory
commandLine "$ndkDir/ndk-build",
'-C', file('src/main/jni').absolutePath, // Change src/main/jni the relative path to your jni source
'-j', Runtime.runtime.availableProcessors(),
'all',
'NDK_DEBUG=1'
}
task cleanNative(type: Exec, description: 'Clean JNI object files') {
def ndkDir = android.ndkDirectory
commandLine "$ndkDir/ndk-build",
'-C', file('src/main/jni').absolutePath, // Change src/main/jni the relative path to your jni source
'clean'
}
clean.dependsOn 'cleanNative'
tasks.withType(JavaCompile) {
compileTask -> compileTask.dependsOn buildNative
}
}
dependencies {
compile 'com.android.support:support-v4:20.0.0'
}
The src/main/jni
directory assumes a standard layout of the project. It should be the relative from this build.gradle
file location to the jni
directory.
Gradle - for those having issues
Also check this Stack Overflow answer.
It is really important that your gradle version and general setup are correct. If you have an older project I highly recommend creating a new one with the latest Android Studio and see what Google considers the standard project. Also, use gradlew
. This protects the developer from a gradle version mismatch. Finally, the gradle plugin must be configured correctly.
And you ask what is the latest version of the gradle plugin? Check the tools page and edit the version accordingly.
Final product - /build.gradle
// Top-level build file where you can add configuration options common to all sub-projects/modules.
// Running 'gradle wrapper' will generate gradlew - Getting gradle wrapper working and using it will save you a lot of pain.
task wrapper(type: Wrapper) {
gradleVersion = '2.2'
}
// Look Google doesn't use Maven Central, they use jcenter now.
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.2.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
jcenter()
}
}
Make sure gradle wrapper
generates the gradlew
file and gradle/wrapper
subdirectory. This is a big gotcha.
ndkDirectory
This has come up a number of times, but android.ndkDirectory
is the correct way to get the folder after 1.1. Migrating Gradle Projects to version 1.0.0. If you're using an experimental or ancient version of the plugin your mileage may vary.
Where to place JNI/native libraries in Android Studio Project
The typical structure of an Android project with jni support is as below:
.
├── CMakeLists.txt // Your cmake configuration files.
├── app.iml
├── build
├── build.gradle
├── libs
├── proguard-rules.pro
└── src
├── androidTest
│ └── java
├── main
│ ├── AndroidManifest.xml
│ ├── cpp // Directory to put your jni native source code.
│ │ └── native-lib.cpp
│ ├── java
│ ├── jniLibs // Directory to put your jni libs, i.e. the .so files.
│ └── res
└── test
└── java
But, theoretically you can configure your jniLibs
path anywhere that you like inside the app level build.gradle
file.
android {
...
defaultConfig {
...
externalNativeBuild {
cmake {
cppFlags "-frtti -fexceptions"
}
}
}
...
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
...
sourceSets {
main {
// put your jni libs.
jniLibs.srcDirs += "${projectDir}/jniLibs"]
}
debug {
// put your debug version jni libs.
jniLibs.srcDirs += "${projectDir}/jniLibs/debug"]
}
release {
// put your release version jni libs.
jniLibs.srcDirs += "${projectDir}/jniLibs/release"]
}
}
...
}
For Android Studio 3.0+, you don't need to explicitly configure the jniLibs
path for your c/c++
source code as it will be automatically managed by Android Studio. All those c/c++
source code under src/main/cpp
will be compiled and packaged into your apk automatically.
Android gradle ndk jni build with external library & native debugging (ARToolkit)
Okay, I researched a lot and found some examples that are working. First of all you have to use the latest gradle experimantal plugin like this in your root build.gradle
'com.android.tools.build:gradle-experimental:0.4.0'
Then your gradle file will look like this
apply plugin: 'com.android.model.application'
/**
* The ar.dir in relative format
*/
def arRoot = new File("../artoolkit5")
def arPath = arRoot.absolutePath + '/android/obj/local/'
def curlPath = arRoot.absolutePath + '/android/jni/curl/libs/'
/**
* The main experimental model
*/
model {
/**
* Android APK values
*/
android {
compileSdkVersion = 23
buildToolsVersion = "23.0.3"
defaultConfig.with {
applicationId = "com.nomad5.ndkdebug"
minSdkVersion.apiLevel = 16
targetSdkVersion.apiLevel = 23
versionCode = 1
versionName = "0.1"
}
}
/**
* The build types
*/
android.buildTypes {
release {
minifyEnabled = false
proguardFiles.add(file('proguard-rules.txt'))
}
debug {
debuggable = true
ndk.with {
debuggable = true
}
}
}
/**
* All statically linked libs
*/
repositories {
libs(PrebuiltLibraries) {
lib_ar2 {
binaries.withType(StaticLibraryBinary) {
staticLibraryFile = file("${arPath}${targetPlatform.getName()}/libar2.a")
}
}
lib_kpm {
binaries.withType(StaticLibraryBinary) {
staticLibraryFile = file("${arPath}${targetPlatform.getName()}/libkpm.a")
}
}
lib_util {
binaries.withType(StaticLibraryBinary) {
staticLibraryFile = file("${arPath}${targetPlatform.getName()}/libutil.a")
}
}
lib_eden {
binaries.withType(StaticLibraryBinary) {
staticLibraryFile = file("${arPath}${targetPlatform.getName()}/libeden.a")
}
}
lib_argsub_es {
binaries.withType(StaticLibraryBinary) {
staticLibraryFile = file("${arPath}${targetPlatform.getName()}/libargsub_es.a")
}
}
lib_armulti {
binaries.withType(StaticLibraryBinary) {
staticLibraryFile = file("${arPath}${targetPlatform.getName()}/libarmulti.a")
}
}
lib_ar {
binaries.withType(StaticLibraryBinary) {
staticLibraryFile = file("${arPath}${targetPlatform.getName()}/libar.a")
}
}
lib_aricp {
binaries.withType(StaticLibraryBinary) {
staticLibraryFile = file("${arPath}${targetPlatform.getName()}/libaricp.a")
}
}
lib_jpeg {
binaries.withType(StaticLibraryBinary) {
staticLibraryFile = file("${arPath}${targetPlatform.getName()}/libjpeg.a")
}
}
lib_arvideo {
binaries.withType(StaticLibraryBinary) {
staticLibraryFile = file("${arPath}${targetPlatform.getName()}/libarvideo.a")
}
}
lib_cpufeatures {
binaries.withType(StaticLibraryBinary) {
staticLibraryFile = file("${arPath}${targetPlatform.getName()}/libcpufeatures.a")
}
}
lib_curl {
binaries.withType(StaticLibraryBinary) {
staticLibraryFile = file("${curlPath}${targetPlatform.getName()}/libcurl.a")
}
}
}
}
/*
* native build settings
*/
android.ndk {
moduleName = "ndkDebugModule"
toolchain = "clang"
stl = "c++_static"
platformVersion = 15
cppFlags.addAll(["-frtti",
"-fexceptions",
"-I${file(arRoot.absolutePath + "/include")}".toString(),
"-I${file(arRoot.absolutePath + "/android/jni/curl/include")}".toString()
])
ldLibs.addAll(['android',
'log',
'z',
'GLESv1_CM'])
abiFilters.addAll(["armeabi-v7a",
/*"arm64-v8a",*/
"x86",
/*"x86_64"*/])
}
/**
* The android native sources
*/
android.sources.main {
jni {
exportedHeaders {
srcDirs = [arRoot.absolutePath + "/include",
arRoot.absolutePath + "/android/jni/curl/include"]
}
source {
/* we set this to NOT automatically compile everything */
srcDirs = ["src/main"]
include "jni/nativeCodeA.cpp"
include "jni/nativeCodeB.cpp"
}
dependencies {
library "lib_ar2" linkage "static"
library "lib_kpm" linkage "static"
library "lib_util" linkage "static"
library "lib_eden" linkage "static"
library "lib_argsub_es" linkage "static"
library "lib_armulti" linkage "static"
library "lib_ar" linkage "static"
library "lib_aricp" linkage "static"
library "lib_jpeg" linkage "static"
library "lib_arvideo" linkage "static"
library "lib_cpufeatures" linkage "static"
library "lib_curl" linkage "static"
}
}
jniLibs {
source {
srcDirs = [arRoot.absolutePath + "/include",
arRoot.absolutePath + "/android/jni/curl/include"]
}
}
}
}
/**
* The Java dependencies
*/
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile project(':aRBaseLib')
compile 'com.android.support:appcompat-v7:23.4.0'
compile 'com.android.support:design:23.4.0'
}
/**
* Dynamically add libs to the linker
*/
class SampleMigrationRuleSource extends RuleSource {
@Mutate
void injectArmeabiV7aDebugLinkerFlags(
@Path('tasks.linkNdkDebugModuleArmeabi-v7aDebugSharedLibrary')
Task linkTask) {
injectLinkerFlags(linkTask, 'armeabi-v7a', 'debug')
}
@Mutate
void injectArmeabiV7aReleaseLinkerFlags(
@Path('tasks.linkNdkDebugModuleArmeabi-v7aReleaseSharedLibrary')
Task linkTask) {
injectLinkerFlags(linkTask, 'armeabi-v7a', 'release')
}
@Mutate
void injectX86DebugLinkerFlags(
@Path('tasks.linkNdkDebugModuleX86DebugSharedLibrary')
Task linkTask) {
injectLinkerFlags(linkTask, 'x86', 'debug')
}
@Mutate
void injectX86ReleaseLinkerFlags(
@Path('tasks.linkNdkDebugModuleX86ReleaseSharedLibrary')
Task linkTask) {
injectLinkerFlags(linkTask, 'x86', 'release')
}
private void injectLinkerFlags(linkTask, arch, buildType) {
def arRoot = new File("../artoolkit5")
def arPath = arRoot.absolutePath + '/android/obj/local/'
def curlPath = arRoot.absolutePath + '/android/jni/curl/libs/'
linkTask.doFirst {
// We are pretty clueless on this one but it is needed
if (arch.equals('arm64-v8a')) {
properties["linkerArgs"].add("-fuse-ld=gold")
}
properties["linkerArgs"].addAll([
"-l${arPath}/${arch}/libar.a".toString(),
"-l${arPath}/${arch}/libar2.a".toString(),
"-l${arPath}/${arch}/libutil.a".toString(),
"-l${arPath}/${arch}/libkpm.a".toString(),
"-l${arPath}/${arch}/libeden.a".toString(),
"-l${arPath}/${arch}/libargsub_es.a".toString(),
"-l${arPath}/${arch}/libarmulti.a".toString(),
"-l${arPath}/${arch}/libaricp.a".toString(),
"-l${arPath}/${arch}/libjpeg.a".toString(),
"-l${arPath}/${arch}/libarvideo.a".toString(),
"-l${arPath}/${arch}/libcpufeatures.a".toString(),
"-l${curlPath}/${arch}/libcurl.a".toString(),
])
}
}
}
apply plugin: SampleMigrationRuleSource
Link static library to JNI shared library in Android
As it turns out, I needed to upgrade my Gradle plugin. I was able to delete CMakeLists.txt
. I also needed to upgrade my Gradle wrapper version to 3.2 to support the new experimental plugin.
Here's my root build.gradle
:
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
jcenter()
}
dependencies {
//classpath 'com.android.tools.build:gradle:2.2.3'
classpath 'com.android.tools.build:gradle-experimental:0.9.0-beta1'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
Here's my app
build.gradle
:
apply plugin: 'com.android.model.application'
model {
android {
compileSdkVersion 25
buildToolsVersion "25.0.2"
defaultConfig {
applicationId "com.neonorb.mish_android"
minSdkVersion.apiLevel 15
targetSdkVersion.apiLevel 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
ndk {
moduleName "mish-android"
ldLibs.add("log")
cppFlags.add("-std=c++14")
cppFlags.add("-Wno-implicit-exception-spec-mismatch")
// ${targetPlatform.getName()}
// ${buildType.getName()}
stl "c++_static"
abiFilters.addAll(["x86_64"])
// only build for x86_64 because that's all Feta and Mish support atm
}
buildTypes {
release {
minifyEnabled false
proguardFiles.add(file("proguard-rules.pro"))
}
}
sources {
main {
jni {
source {
srcDir "src"
}
dependencies {
library "feta" linkage "static"
library "mish" linkage "static"
}
}
}
}
}
repositories {
libs(PrebuiltLibraries) {
feta {
headers.srcDir "../../feta/include/"
binaries.withType(StaticLibraryBinary) {
staticLibraryFile = file("../../feta/build/libfeta.a")
}
}
}
libs(PrebuiltLibraries) {
mish {
headers.srcDir "../../mish/include/"
binaries.withType(StaticLibraryBinary) {
staticLibraryFile = file("../../mish/build/libmish.a")
}
}
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:25.1.0'
testCompile 'junit:junit:4.12'
}
Android Studio NDK being incompatible though having been set properly
The Issue I had was not with the NDK tool , it was with the directory where i had placed it.
There was space between file name which resulted in the error eg"User space Name".eg:C:/User/Elon Musk/SDK/ndk
I rectified this mistake of mine after a warning display by Android Studio lint .
I changed the Android SDK directory placement and rerun the code and the issue got resolved.
As per the answer above by Lakindu , there is no need for downgrading the Ndk library just make sure that the NDK is in the right place .
And also dont forget to add the NDK tool in the Project Structure which you can find by clicking Ctrl + Shift +Alt + S simultaneously.
jni library not getting included in debug build
Explicitlty adding the architecure variants to the app grade file also helps:
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
ndk {
abiFilters "armeabi"
}
}
debug {
ndk {
abiFilters "armeabi"
}
}
}
Related Topics
Versioncode VS Versionname in Android Manifest
Navigation Drawer: Set as Always Opened on Tablets
Android Background Image Memory Usage
How to Use View.Ontouchlistener Instead of Onclick
Proguard: Can't Find Referenced Class Com.Google.Android.Gms.R
System.Exit(0) Doesnt Close All My Activities
Passing Strings Between Activities in Android
How to Read "Adb Shell Dumpsys Alarm" Output
Android - How to Achieve Setonclicklistener in Kotlin
How to Start an Intent by Passing Some Parameters to It
Java.Lang.Noclassdeffounderror:Failed Resolution of :Lorg/Apache/Http/Protocolversion
Custom Cut/Copy Action Bar for Edittext That Shows Text Selection Handles
Lazy Load Images on Listview in Android(Beginner Level)
How to Use Radiogroup in Listview Custom Adapter
Android:Configchanges="Orientation" Does Not Work with Fragments
Google Maps API V2 Draw Part of Circle on Mapfragment