1.1 the JNI (Java Native Interface)
JNI describes a technique that provides a solution for Java bytecode invocation of C/C++
1.2 the NDK (Native Development Kit)
The Android NDK is a set of tools that allow you to embed C or C++ (” native code “) into your Android applications. The NDK describes the toolset. Being able to use native code in Android apps is particularly useful for developers who want to do one or more of the following:
- Porting their applications between platforms.
- Reuse existing libraries or provide their own libraries for reuse.
- Improve performance in some cases, especially in computationally intensive applications like games.
1.3 the JNI registered
1.3.1 Environment Configuration
There is already an article here that quotes others, which is very detailed about JNI environment configuration
1.3.2 Static Registration
When the Java layer calls the Navtie function, it looks up the corresponding JNI function based on the function name in the JNI library. If not found, an error is reported. If found, the association between native function and JNI function will be established, which is actually to save the function pointer of JNI function. The next time you call native, you can use the function pointer directly.
JNI function name format (package name inside the “. Java_ + package name (com_example_AUTO_jnitest) + class name (_MainActivity) + function name (_stringFromJNI)
Disadvantages of static registration:
- JNI function names are required to follow the naming format of the JNI specification;
- Long, error-prone names;
- The first call will search the corresponding function in JNI according to the function name, which will affect the execution efficiency.
- All Java classes that declare native functions need to be compiled. For each generated class file, javah tools need to generate a header file.
Static registration example
Class JNITest package name: com.hqk.jnitestone
package com.hqk.jnitestone;
public class JNITest {
static {
System.loadLibrary("native-lib");
}
public static native String sayHello(a);
}
Copy the code
Corresponding to c++ code, CPP class name: native-lib.cpp
#include <jni.h>
#include <string>
extern "C"
JNIEXPORT jstring JNICALL
Java_com_hqk_jnitestone_JNITest_sayHello(JNIEnv *env, jclass clazz) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
Copy the code
Note:
- If the CPP class name is native-lib. CPP, the corresponding Java interaction class must be added
static {
System.loadLibrary("native-lib");
}
Copy the code
- If the package name of the Java interaction class is com.hqk.jnitestone, the CPP corresponding method needs to convert the corresponding [.] to [_]. (Refer to above format)
1.3.2 Dynamic Registration
By providing a function mapping table registered with the JVM virtual machine, the JVM can use the function mapping table to call the corresponding function without having to look up the function to be called by the function name.
Java and JNI use the JNINativeMethod structure to establish the function mapping table, which is defined in the jni.h header file, the structure content is as follows:
typedef struct {
const char* name; // Corresponds to the method name of the interactive Java class
const char* signature; // Function signatures corresponding to interactive methods (see article 1.4.3)
void* fnPtr; // The pointer function to the interactive CPP method (pointing to the corresponding function)
} JNINativeMethod;
Copy the code
1. After creating the mapping table, call the RegisterNatives function to register the mapping table to the JVM. 2. When the Java layer loads the JNI library through system. loadLibrary, it looks up the JNI_OnLoad function in the library. You can think of JNI_OnLoad as an entry function to the JNI library, where you need to do all the function mapping and dynamic registration, as well as some other initialization.
So with the concept out of the way, let’s do it
JAVA class JNITest package name: com.hqk.jnitestone
package com.hqk.jnitestone;
public class JNITest {
static {
System.loadLibrary("native-lib");
}
public static native String sayHello(a);
public static native String sayHello2(a);
}
Copy the code
Note that there is a sayHello2 method that returns String
CPP class name native-lib. CPP corresponding code
#include <jni.h>
#include <string>
#include <android/log.h>
extern "C"
JNIEXPORT jstring JNICALL
Java_com_hqk_jnitestone_JNITest_sayHello(JNIEnv *env, jclass clazz) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
// CPP interaction methods
jstring sayHello2(JNIEnv *env, jobject thiz) {
std::string hello = "Hello, I am dynamic registration success!";
return env->NewStringUTF(hello.c_str());
}
// Dynamically register the function structure array
static const JNINativeMethod gMethods[] = {
{"sayHello2".// The method name corresponding to the Java interaction class
"()Ljava/lang/String;".// Function signature corresponding to method name (see article 1.4.3)
(jstring *) sayHello2 // Pointer function corresponding to CPP interaction class}};JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
__android_log_print(ANDROID_LOG_INFO, "native"."Jni_OnLoad");
JNIEnv *env = NULL;
if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) ! = JNI_OK)// Get the JNIEnv from JavaVM, typically using version 1.4
return - 1;
// Note that FindClass must correspond to the package name of the interaction class with the [/] symbol
jclass clazz = env->FindClass("com/hqk/jnitestone/JNITest");
if(! clazz) { __android_log_print(ANDROID_LOG_INFO,"native"."cannot get class: com/hqk/jnitestone/JNITest");
return - 1;
}
if (env->RegisterNatives(clazz, gMethods, sizeof(gMethods) / sizeof(gMethods[0]))) {
__android_log_print(ANDROID_LOG_INFO, "native"."register native method failed! \n");
return - 1;
}
return JNI_VERSION_1_4;
}
Copy the code
As you can see from the code above
JNI_OnLoad () : JNI_OnLoad (); CPP: JNI_OnLoad (); In the end, the RegisterNatives dynamic registration is realized. 2. The CPP class has a new JNINativeMethod structure array; SayHello2 (CPP) ¶ sayHello2 (CPP) ¶
1.4 Data type conversion
1.4.1 Basic data conversion
1.4.2 Reference Data Type Conversion
Except for classes, Strings, Throwable, and arrays of primitive data types, the data types of all Java objects are represented by Jobject in JNI. String is also a reference type in Java, but because it is used more frequently, a separate jString type is created in JNI.
- Reference types cannot be directly used in the Native layer, and can only be used after type conversion according to JNI functions.
-
- Multi-dimensional arrays (including two-dimensional arrays) are reference types and need to use jobjectArray to access their values.
For example, a two-dimensional array of integers is an array pointing to a one-bit array. The declaration is used as follows:
// Get a class reference to a one-dimensional array, that is, type jintArray
jclass intArrayClass = env->FindClass("[I");
// Construct an array of objects that points to a one-dimensional array of the jintArray class. The initial size of the array is Length and the type is jsize
jobjectArray obejctIntArray = env->NewObjectArray(length ,intArrayClass , NULL);
Copy the code
1.4.3 JNI Function Signature Information
Because Java supports function overloading, it is impossible to find the corresponding JNI function based on the function name alone. To solve this problem, JNI uses parameter types and return value types as signature information for functions.
- Function signature information format defined by the JNI specification:
(Parameter 1 type character…) Return value type character
- Function signature examples:
3.JNI commonly used data types and corresponding characters:
1.4.4 JNIEnv is introduced
- JNIEnv concept: A JNIEnv is a thread-specific structure that represents the environment in which Java is run in this thread. A series of JNI system functions can be called from JNIEnv.
- JNIEnv thread dependency: There is a JNIEnv pointer in each thread. JNIEnv is valid only on the thread it is in, and it cannot be passed between threads.
Note: the JNIEnv is obtained in a child thread created by C++ by calling AttachCurrentThread on JavaVM. When the child thread exits, JavaVM’s DetachCurrentThread function is called to release the corresponding resource, otherwise an error occurs.
JNIEnv:
- Access Java member variables and member methods;
- Call Java constructors to create Java objects, etc.
1.5 compile JNI
1.5.1 Cmake compilation
CMake is a cross-platform compilation tool that does not directly compile objects. Instead, it generates corresponding makefiles or project files according to custom language rules (cmakelists.txt) and then invokes the underlying compilation. Cmake compilation is supported after Android Studio 2.2.
cmake_minimum_required(VERSION 3.41.)
add_library(
native-lib
SHARED
native-lib.cpp)
find_library(
log-lib
log)
target_link_libraries(
native-lib
${log-lib})
Copy the code
1.5.1.1 add_library instruction
Grammar: add_library (libname SHARED | STATIC | MODULE [source])
Compile a library file from a set of source files and save it as libname.so (the lib prefix was automatically added by CMake when the file was generated). There are three library file types, which default to STATIC if not written;
- SHARED: indicates a dynamic library, which can be called dynamically in (Java) code using system.loadLibrary (name);
- STATIC: represents a STATIC library that is integrated into the code and called at compile time;
- MODULE: valid only for systems that use dyId. If dyId is not supported, it is treated as SHARED.
- EXCLUDE_FROM_ALL: indicates that the library is not built by default unless other components depend on it or are manually built;
Libcompresse.c (compress SHARED compresse.c)Copy the code
1.5.1.2 target_link_libraries command
Grammar: target_link_libraries (target library < the debug | optimized > library2…).
This directive can be used to add a shared library link for target as well as to add a shared library link for your own shared library. Such as:
Target_link_libraries (compress libjpeg ${log-lib})Copy the code
1.5.1.3 find_library instruction
Grammar: find_library ( name1 path1 path2 …)
The VAR variable represents the full path to the found library, including the library file name. Such as:
find_library(libX X11 /usr/lib)
find_library(log-lib log) # path is empty, should be to find the system environment variable pathCopy the code
1.5.1.4 More detailed usage
References: More on Cmake in detail
1.5.2 Abi architecture
Application Binary Interface (ABI) Application binary interface. Each combination of different cpus and instruction sets has a defined ABI (Application binary Interface). A program can only run on that CPU if it complies with this interface specification, so the same program code needs to build different libraries for different ABIs to be compatible with multiple cpus. Of course, different architectures do not necessarily mean incompatible cpus.
- Armeabi devices are compatible only with Armeabi;
- Armeabi-v7a devices are compatible with ArmeabI-V7A and ArmeABI;
- Arm64-v8a devices compatible with ARM64-V8A, ArMEABI-V7A, ArmeABI;
- X86 devices are compatible with X86 and Armeabi;
- X86_64 devices are compatible with X86_64, X86, and Armeabi;
- Mips64 devices are compatible with MIPS64 and MIPS.
- MIPS is only compatible with MIPS;
According to the compatibility summary above, we can also get some rules:
- Armeabi’s SO file is basically a panacea that runs on devices other than MIPS and MIPS64, but still suffers from performance loss on non-Armeabi devices;
- 64-bit CPU architectures are always backward compatible with their corresponding 32-bit instruction sets. For example, X86_64 is X86 compatible, ARM64-V8A is Armeabi-V7A compatible, and MIPS64 is MIPS compatible.
1.6 Project Address
GitHub address: Click to download