“This is the seventh day of my participation in the August More Text Challenge. For details, see: August More Text Challenge.”

JNI

concept

JNI is short for Java Native Interface, which enables Java to interact with other languages such as C and C++.

It is a feature of the Java calling Native language and belongs to the Java language, not Android.

Why do you need JNI

  • Java source files are easy to decompile, while.so library files generated in the Native language are not.
  • Sometimes when we use Java, we need to use libraries to implement functions, but these libraries only provide Native language interfaces.
  • The code written in Native language has high efficiency, especially in the processing of audio, video, pictures and other operations that require a large number of complex operations. Take full advantage of the performance of the hardware.

For these reasons, this is where we need Java to interact with the Native language. Because of the characteristics of Java, the interaction with Native language is very weak. At this point, we need to use JNI features to enhance Java’s ability to interact with Native methods.

Implementation steps

  1. Declare Native methods in Java (Native methods to be called)
  2. Compiling Java source files with Javac (generating.class files)
  3. Generate JNI header file (generate.h file) with javah command
  4. Through the Native language implementation in the Java source code declaration of Native methods
  5. Compile to.so library file
  6. throughJavaCommand executionJavaProgram, the final implementationJavaCall native code (with so library files)

NDK

concept

Native is short for Native Development Kit. It is an Android Development Kit. It has nothing to do with Java.

It can quickly develop dynamic libraries of C/C++ and automatically package.SO and applications together as APK. Therefore, we can use the NDK to interact with Native methods in Android development through JNI.

use

  1. Configure the Android NDK environment (download NDK, CMake, LLDB from SDK Manager)
  2. Create an Android project and associate it with the NDK (select C++ support when creating the project)
  3. Declare the Native methods you need to call in your Android project
  4. Implement Native methods declared in Android in Native language
  5. Compile the. So library file using the ndk-bulid command

Associate the Android project with the NDK

Configure the NDK path

Add the following line to local.properties

The NDK. Dir = > < the NDK path

Add the configuration

Add the following line to Gradle’s Gradle. properties for support of older versions of the NDK

android.useDeprecatedNdk=true

The NDK node is added

Add the following externalNativeBuild node to defaultConfig in build.gradle and android

apply plugin: 'com.android.application' android { compileSdkVersion 27 defaultConfig { applicationId "com.n0texpecterr0r.ndkdemo" 19 targetSdkVersion 27 versionCode minSdkVersion 1 versionName testInstrumentationRunner "1.0" "android.support.test.runner.AndroidJUnitRunner" externalNativeBuild { cmake { cppFlags "" } } } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } externalNativeBuild { cmake { path "CMakeLists.txt" } } } dependencies { implementation fileTree(dir: 'libs', include: [' *. Jar ']) implementation 'com. Android. Support: appcompat - v7:27.1.1' implementation 'com. Android. Support. The constraint, the constraint - layout: 1.1.3' testImplementation junit: junit: '4.12' androidTestImplementation  'com.android.support.test<span class="emoji emoji-sizer" Style = "background - image: url (/ emoji - data/img - apple - 64/1 f3c3. PNG)" data - codepoints = "1 f3c3" > < / span > 1.0.2 ' AndroidTestImplementation 'com. Android. Support. Test. Espresso: espresso - core: 3.0.2'}Copy the code

Developing Native code

Declare native methods in a Java file

We first need to load our Native library via static blocks in a class of Java code. This can be done by taking the name of the Native library defined in cmakelist.txt as the loadLibrary parameter

static {

    System.loadLibrary("native-lib");

}
Copy the code

We can then declare Native methods in this class

public native String getStringFromJNI();
Copy the code

Create CMakeList. TXT

We also need to create a cmakelist.txt file in SRC, which will constrain the compilation of Native language source files. Such as the following

Cmake_minimum_required (VERSION 3.4.1) add_library(nil-lib SHARED SRC /main/ CPP/nil-lib. CPP) find_library(log-lib log) target_link_libraries(native-lib ${log-lib})Copy the code

The add_library method defines a so library with the name native-lib, which is the string we use in Java files, followed by the path to the library’s native file

Find_library defines a path variable. The value of log-lib is the path to the Android log library

The target_link_libraries link the native lib library to the log library so that we can use the log library’s methods in the native Lib.

Create Native method files

As you can see from the previous CMake file, we put the files in SRC /main/ CPP /, so we create the CPP directory and create the C++ source file native-lib. CPP in it.

From there, we can start writing the following code:

#include <jni.h> #include <string> extern "C"{ JNIEXPORT jstring JNICALL Java_com_n0texpecterr0r_ndkdemo_MainActivity_getStringFromJNI(JNIEnv* env, jobject) {STD :: String Hello = "IG ExpecTERR0R_NDKDemo_MAINActiVITy_GETStringFromJNi (JNIEnv* env, jobject); return env->NewStringUTF(hello.c_str()); }}Copy the code

We’re using C++ here, so let’s look at the code.

First, we introduce jni.h, a header file that declares the functions that jNI needs. We also introduced string.h from C++.

And then we see extern “C”. To understand why I used extern “C” here, we first need to know the following:

In C, the compile-time signature of a function simply contains the name of the function, so functions with different arguments have the same signature. This is why C does not support overloading.

In C++, to support overloading, the signature of a function at compile time contains the name of the function, its arguments, its return type, and so on.

Imagine that we have a C library of functions to call to C++, and we can’t find the corresponding function because of the different signature. Therefore, we need to use extern “C” to tell the compiler to concatenate in a way that compiles C.

Next, let’s look at the JNIEXPORT and JNICALL keywords, which are macro definitions whose main purpose is to indicate that the function is a JNI function.

Jstring corresponds to the String class in Java. JNI has many classes similar to JString to correspond to Java classes. The following is a comparison table of Java classes and JNI types

We continue to see the function name Java_com_n0texpecterr0r_ndkdemo_MainActivity_getStringFromJNI. Actually _ in the function name is equivalent to in Java. The function name is represents the getStringFromJNI java.com.n0texpecterr0r.ndkdemo.MainActivity.java method, also is the native method we defined before.

The format is roughly as follows:

Java_ package name _ class name _ method name to call

Where Java must be capitalized, in the package name. I’m going to change it to _, and _ is going to change it to _1

Next we see the two arguments to this function:

  • JNIEnv* env: represents the JVM environment and Native methods can call Java code through this pointer
  • Jobject obj: This is equivalent to the this reference to the class (MainActivity) that defines the JNI method

Env ->NewStringUTF(hello.c_str()) creates a variable of type jString and returns it.

Call native methods in Java code

We can then call this Native method from MainActivty just like we would call a Java method

TextView tv = findViewById(R.id.sample_text);

tv.setText(getStringFromJNI());
Copy the code

We try to run it, and you can see that we successfully build a string in C++ and return it to the Java call:

CMake

In NDK development, we use CMake syntax to write simple code describing the compilation process. Since this article is about the NDK, we will not go into details about CMake syntax… If you want to learn about CMake grammar, you can learn this book CMake Practice.

JNI interacts with Java code

The method signature

concept

When we call a method at the JNI layer, we need to pass one parameter — the method signature.

Why use method signatures? Because methods in Java can be overloaded, two methods may have the same name but different parameters. To distinguish the methods being called, the concept of method signatures is introduced.

Signature rule

For parameters of the basic types, each type corresponds to a different letter:

  • boolean Z
  • byte B
  • char C
  • short S
  • int I
  • long J
  • float F
  • double D
  • void V

For classes, the method L+ class name is used, where (.) Replace it with (/) and add a semicolon at the end

For example, java.lang.String is Ljava/lang/String;

In the case of an array, add [, and then add the signature of the type.

For example, int[] corresponds to [I, Boolean [][] corresponds to [[Z, java.lang.String[] is [Ljava/lang/String;

Print method Signature

We can print the signature of the method with the javap -s command.

example

For example, the following method

public native String getMessage();



public native String getMessage(String id,long i);
Copy the code

The corresponding method signatures are:

()Ljava/lang/String; (Ljava/long/String; J)Ljava/lang/String;Copy the code

As you can see, the first parentheses represent the parameter list of the method, and the second parentheses represent the return value.

So much for today, tomorrow

Public account: Programmer Meow (focus on Android study notes, interview questions and IT information sharing)