📜 ⬆️ ⬇️

Using NDK in Android Studio

Currently, Android Studio is very popular among Android developers, based on IntelliJ IDEA by JetBrains. However, when using this IDE, problems may arise when developing applications using native code, since the Android NDK is designed primarily to use IDE Eclipse and ADT.

The purpose of this article is to provide a detailed description of the process of creating an Android application using the NDK in Android Studio, in particular, offering a fairly simple and efficient configuration of the gradle (package building system used in Android Studio) to ensure that native libraries are included in the APK file. The article also includes a brief instruction on working with NDK in the Eclipse IDE and an introduction to native development sufficient to write the first application.


This article is intended primarily for novice developers. The described solution is not the only one, but it is quite convenient, especially for those who worked with the NDK in Eclipse. If the explanation seems too detailed to someone of the readers, then at the end of the article there is a brief summary describing only the algorithm of the required actions without comments to each step.
')
Since there are many articles describing working with NDK, I will not use complex libraries as an example, but I will limit myself to the simplest example of hello-jni. The source code of this example can be found in the directory <path_to_ndk> / samples / hello-jni

In the Eclipse environment, there were no special problems with using NDK. The project directory looks like this:

Fig.1 Main project directory for Eclipse

We are interested in jni and libs directories. The jni directory contains source codes in native languages ​​(* .c; * .cpp), header files (* .h), makefiles (* .mk). I will not dwell on the purpose of these files for a long time, since quite a lot of materials and articles are devoted to this. I will only mention that jni means java native interface. It is through this interface that the native procedures are called from java code. Therefore, do not forget to include the library <jni.h> in your c / c ++ files and remember the correct syntax of functions that will be called via jni. For example, in my case, the application has a package name:
evi.ntest
therefore, the function description looks like this:
jstring Java_evi_ntest_MainActivity_stringFromJNI( JNIEnv* env, jobject thiz ) 

where jstring is the name of the data type c corresponding to the type of string in java, Java is in this case a service prefix indicating the language from which the function will be called, evi_ntest is the name of the package that will call the function, MainActivity is the name of the activity from which it will be called function, stringFromJNI - the name of the function.
In java-code, the description of this function looks much simpler:
  public native String stringFromJNI(); 


Do not forget to also specify the code files used in the .mk files. For starters, you can use the .mk files from the NDK examples, modifying the file names, but I further recommend that you study their structure.

The libs directory contains ready-made binary libraries for various processor architectures (by default, armeabi). A dynamic library is a file with a .so extension, a static library is a file with a .a extension. To get these libraries, you need to compile the source codes using the Android NDK. In Unix-systems (in my case - Mac OSX) this requires entering the following lines in the terminal:
 cd <__> <__ndk>/ndk-build 

At the same time, the NDK automatically compiles the source codes from the jni folder and places the resulting libraries in libs / armeabi (you can also set the compilation for x86, mips, arm v7-neon processors using command line parameters).
When using Windows, you will need to use additional utilities, perhaps - plug-ins for MS Visual Studio.
In any case, it does not matter which way the finished libraries are obtained, the fact that if the libs subdirectory is in the project folder is important, Eclipse automatically includes its contents in the APK file when it is built.

Since this article is devoted only to the basics of working with NDK, then I’ll finish the introduction to programming in native languages ​​under android. For more detailed acquaintance with the principles of native development, I recommend studying examples from the <path_to_ndk> / samples / directory, as well as reading articles, including those in Russian. An example of a good article about Android NDK in Russian, I advise you to pay attention (it was not written by me):

idev.by/android/21115

I turn to the main section of the article - setting up the Android Studio IDE to work with native code.

The Android Studio environment by default builds an APK using gradle. This collector has extensive customization options, but with the standard settings gradle does not include native libraries in the APK file.

Consider a partial project structure in Android Studio:

Fig.2 Path to the source code of the project Android Studio.

When working, I had a logical desire to place the jni folder in the src / main directory, since that is where all the other source code files are stored. Of course, the reader can place the jni directory where it suits him. The main thing is to remember to compile binary libraries using NDK (again, on UNIX systems, you need to go to the directory containing jni in the terminal, then call the ndk-build executable file located in the folder with the NDK, writing the full path to it terminal, in MS Windows you need to use additional utilities). The problem is that by default gradle will not pack in the APK of the library.

However, gradle is easy to configure to include in the assembly of java-libraries (* .jar files). It is worth noting that jar-files are zip-archives containing any resources, as well as object code. Thus, to include binary libraries * .so and * .a it is enough to pack them into a jar-file.
This is done like this:

The resulting library can be connected at the project build stage, while the resulting APK file will include binary libraries, and the application will call procedures written in native code.
This issue has been repeatedly discussed in various English-language forums, for example, Stack Overflow:
stackoverflow.com/questions/16667903/android-studio-gradle-and-ndk
However, this information is rather brief, fragmented and requires a certain knowledge of the gradle syntax from the reader. The purpose of my article is to provide readers with a detailed Russian-language explanation, available even to those who have just started working with Android Studio and gradle.

Let's look at 2 ways of packing libraries: manual and automatic.

Manual packing method:

This method is very inconvenient, but it does exist. A valid case of practical application: the availability of a ready-made library and no need to change it. In this case, the operations described below will need to be performed only once.

Open build.gradle, located at the address "<project_path> / <project_name> Project / <project_name> /", in my case:

Fig.3 Location of the configurable build.gradle file.

This file initially looks something like this:

 buildscript { repositories { mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:0.5.+' } } apply plugin: 'android' repositories { mavenCentral() } android { compileSdkVersion 17 buildToolsVersion "17.0.0" defaultConfig { minSdkVersion 9 targetSdkVersion 9 } } dependencies { compile 'com.android.support:support-v4:18.0.0' } 


At the very bottom there is a dependencies section. There should add the following line:

compile fileTree (dir: 'src / main /', include: '* .jar')

Consider this command: gradle during compilation will be forced to include the file tree (file and folder structure matching the specified mask) located at the address 'src / main /' (i.e. in the directory where the source codes are located, as well as the created us jar file), while the parameter '* .jar' is used as a mask, i.e. all files with this extension will be included. Note that in this case the path is considered relative to the location of the build.gradle file.
As a result of executing this command, gradle will unpack the jar file and include binary libraries in the APK file.

Become familiar with the modified build.gradle file, so as not to confuse dependencies and buildscript.dependencies.

 buildscript { repositories { mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:0.5.+' } } apply plugin: 'android' repositories { mavenCentral() } android { compileSdkVersion 17 buildToolsVersion "17.0.0" defaultConfig { minSdkVersion 9 targetSdkVersion 9 } } dependencies { compile 'com.android.support:support-v4:18.0.0' compile fileTree(dir: 'src/main/', include: '*.jar') } 


The reader will most likely notice that this method is very inconvenient if there is a need for frequent changes in the native code, since after each recompilation it is necessary to delete the old jar file, rename the libs folder to lib, archive it, change the archive extension. Therefore, we use the power of gradle and automate the process.

Automatic mode

The gradle packet collector allows you to create tasks (functions), and its capabilities include the creation of various types of archives, including zip. We will use this and add the following lines to build.gradle (the location of this file is discussed above):

 task nativeLibsToJar(type: Zip, description: 'create a jar archive of the native libs') { destinationDir file("$buildDir/native-libs") baseName 'native-libs' extension 'jar' from fileTree(dir: 'src/main/libs', include: '**/*.so') into 'lib/' } tasks.withType(Compile) { compileTask -> compileTask.dependsOn(nativeLibsToJar) } 


This code snippet can be added to any part of the file, except for existing sections, for example at the end of the file. I placed them in front of the dependencies section.
This code includes the task, which creates in the build folder, located at the address "<project_path> / <project_name> Project / <project_name> /" subdirectory native-libs, in this subfolder creates a file native-libs.jar, the file structure corresponds the required java-library structure containing binary libraries .so. If you plan to use also static .a libraries, instead of the line:

from fileTree (dir: 'src / main / libs', include: '** / *. so')

You should use:

from fileTree (dir: 'src / main / libs', include: '** / *. *')

Then it remains to add the line to the dependencies section:

compile fileTree (dir: "$ buildDir / native-libs", include: 'native-libs.jar')

During the build, this command will include the contents of the native-libs.jar library created programmatically in the APK file.

An example of build.gradle with this code:

 buildscript { repositories { mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:0.5.+' } } apply plugin: 'android' repositories { mavenCentral() } android { compileSdkVersion 17 buildToolsVersion "17.0.0" defaultConfig { minSdkVersion 9 targetSdkVersion 9 } } task nativeLibsToJar(type: Zip, description: 'create a jar archive of the native libs') { destinationDir file("$buildDir/native-libs") baseName 'native-libs' extension 'jar' from fileTree(dir: 'src/main/libs', include: '**/*.so') into 'lib/' } tasks.withType(Compile) { compileTask -> compileTask.dependsOn(nativeLibsToJar) } dependencies { compile 'com.android.support:support-v4:18.0.0' compile fileTree(dir: "$buildDir/native-libs", include: 'native-libs.jar') } 


Please note that my directories jni, libs are located at the address "<project_path> / <project_name> Project / <project_name> / src / main". If in your project these folders lie elsewhere, then you should take this into account in shaping the paths for all commands.

If everything is done correctly, Android Studio will automatically create the correct library in the build directory during the project build and include it in the finished program. Thus, after each recompilation of the native code, there is no need to perform any additional actions and settings, the gradle will do everything by itself.

Now, as promised at the beginning of the article, a brief summary describing only the full algorithm without any unnecessary comments:

Brief summary


  1. Open the folder "<project_path> / <project_name> Project / <project_name> / src / main" and create a jni subfolder there.
  2. Open the file "<project_path> / <project_name> Project / <project_name> /build.gradle", modify the dependencies section, and then add the code:

     dependencies { compile 'com.android.support:support-v4:18.0.0' compile fileTree(dir: "$buildDir/native-libs", include: 'native-libs.jar') } task nativeLibsToJar(type: Zip, description: 'create a jar archive of the native libs') { destinationDir file("$buildDir/native-libs") baseName 'native-libs' extension 'jar' from fileTree(dir: 'src/main/libs', include: '**/*.so') into 'lib/' } tasks.withType(Compile) { compileTask -> compileTask.dependsOn(nativeLibsToJar) } 

    To include also static libraries * .a (if available), change the string

    from fileTree (dir: 'src / main / libs', include: '** / *. so')

    on

    from fileTree (dir: 'src / main / libs', include: '** / *. *')

  3. In the jni subfolder we place the files * .mk, * .h, * .c, we write the native code.
  4. Open the folder "<project_path> / <project_name> Project / <project_name> / src / main" in the terminal.
  5. Enter the command <path_to_k tok> / ndk-build in the terminal
  6. Run the project.


Important!
This manual is intended for Unix operating systems (in my case, MacOSX). For the MS Windows operating system, items 4 and 5 are not relevant, as additional utilities are required to compile the native libraries. It is also likely that it would be advisable to change the library storage paths to more convenient ones and take this into account in the build script.

At this point, I conclude the article and bow out. I hope someone this article will save time.

Good luck to your native programming, the main thing is that every time, do not forget to ask yourself whether you should use the native code. There may well be java-analogs, the use of which is simpler, and in most cases better, since development time is reduced, code understanding is improved by others, the complexity of the application architecture is reduced, and the power of modern devices is enough to perform most tasks even in Dalvik VM.

Source: https://habr.com/ru/post/193122/


All Articles