Android JNI Learning (1) - NDK and JNI Basics

This series of articles are as follows:

Android JNI learning (1) - NDK and JNI basic
Android JNI learning (2) - "hello world" of actual JNI
Android JNI learning (3) - Java and Native call each other
Android JNI learning (4) - JNI Chinese API for common methods

1. Introduction

To develop applications on Android OS, Google provides two development kits: SDK and NDK. You can find many excellent books and articles about SDK from Google official website as a reference, but the NDK resources provided by Google are relatively few compared to SDK. This series of articles is mainly used to record the experience of self-study NDK, and hope to help friends who want to learn NDK.

The Android platform has supported C/C++ since its inception. We know that the Android SDK is mainly based on Java, so engineers who use the Android SDK for development must use the Java language. However, Google has stated from the beginning that Android also supports JNI programming, that is, third-party applications can call their own C dynamics through JNI. So NDK came into being.

2. What is NDK

NDK The full spelling of NDK is: Native Develop Kit.
Then let's take a look at how the Android NDK official website explains NDK

The key text is as follows:

The Android NDK is a set of tools that allows you to implement parts of your application in native code languages ​​such as C and C++. This helps you reuse codebases written in these languages ​​when developing certain types of apps.

In simple terms:

Android NDK is a set of tools that allows you to use C/C++ language to implement some functions of the application.

NDK is the meaning of Native Develop Kit, which is easy to understand from the meaning, local development. Everyone knows that the Android development language is Java, but we also know that Android is based on Linux, and many of its core libraries are C/C++, such as Webkit. Then the role of NDK is to provide developers with a tool for calling C/C++ code in Java. NDK itself is actually a cross-work chain, including some library files on Android. Then, NDK provides some scripts for easy use, making it easier to compile C/C++ code. In short, in addition to the Android SDK, there is a tool that is NDK, which is used for C/C++ development. In general, use NDK tools to compile C/C++ into .so files, and then call them in Java.

The NDK is not suitable for most beginning Android engineers and is of little value for many types of Android applications. Because it inevitably increases the complexity of the development process, it is generally rarely used. Then why Google also provides NDK, let's study it together.

3. Why use NDK

As mentioned above, NDK is not suitable for most junior Android engineers. Because it increases the complexity of development, it does not actually have a great effect on many types of Android. However, under the following requirements, NDK still has great value:

  1. Port their applications between platforms
  2. Reuse the existing library, or provide your own library to reuse
  3. Improves performance in some cases, especially for computationally intensive applications like games
  4. Use third-party libraries, many third-party libraries are now written by C/C++ libraries, such as Ffmpeg
  5. Design independent of Dalvik Java virtual machine
  6. code protection. Since the Java layer code of the APK is easy to be decompiled, it is difficult to decompile the C/C++ library.

Four, NDK to so

From the above picture of the Android system framework, our upper layer calls the NDK layer through JNI. Using this tool can easily write and debug JNI code. Because the C language is not cross-platform, use NDK to compile the function library that can be executed under Linux—so file under the Mac system. Its essence is that a bunch of C and C++ header files and implementation files are packaged into a library. At present, the Android system supports the following seven different CPU architectures, each of which corresponds to its own application binary interface ABI: (Application Binary Interface) defines how binary files (especially .so files) run on the corresponding system platform, From the instruction set used, memory alignment to the available system function library. The corresponding relationship is as follows:

  • ARMv5——armeabi
  • ARMv7 ——armeabi-v7a
  • ARMv8——arm64- v8a
  • x86——x86
  • MIPS ——mips
  • MIPS64——mips64
  • x86_64——x86_64

5. JNI

(1) What is JNI?

Guidance on JNI in oracle

Java calls C/C++ in the Java language, not Android's own creation, that is, JNI. JNI is the specification for Java to call C++. Of course, the JNI standard used by general Java programs may be different from that of Android, and Android's JNI is simpler.

JNI, the full name is Java Native Interface, that is, Java local interface. JNI is a feature of Java calling Native language. Java can interact with C/C++ models through JNI. That is, codes in languages ​​such as C/C++ can be called in Java codes or Java codes can be called in C/C++ codes. Since JNI is part of the JVM specification, the JNI program we write can be run in any Java virtual machine that implements the JNI specification. At the same time, this feature allows us to reuse a large amount of code previously written in C/C++. JNI is a standard mechanism for executing code under the Java virtual machine mechanism. The code is written as an assembler or C/C++ program and assembled as a dynamic library. This also allows non-static binding usage. This provides a way to call C/C++ on the Java platform and vice versa.

PS:
The development of JNI programs will be limited by the system environment, because the code or modules written in C/C++ language will depend on some library functions provided by the current operating system environment during the compilation process, and be linked with the local library. Moreover, the binary code generated after compilation can only run in the local operating system environment, because different operating system environments have their own local libraries and CPU instruction sets, and each platform implements standard C/C++ specifications and standard library functions. There are also differences. This has resulted in Java programs using the JNI interface on various platforms, which are no longer as freely cross-platform as before. If you want to achieve cross-platform, you must compile the local code into the corresponding dynamic library under different operating system platforms.

(2) Why do we need JNI

Because in actual requirements, Java code and C/C++ code need to interact, and Java code and C/C++ code can be interacted through JNI

(3) Advantages of JNI

Compared with other similar interfaces such as Microsoft's original native interface, JNI's main competitive advantage is that it ensures binary compatibility from the beginning of its design, application compatibility written by JNI, and Java on some specific platforms. Virtual machine compatibility (when it comes to JNI, it is not specific to the Davik virtual machine here, JNI is applicable to all JVM virtual machines). This is why C/C++ compiled code can be executed on any platform. However, some earlier versions do not support binary compatibility. Binary compatibility is a type of program compatibility that allows a program to work in different compilation environments without changing its executable file.

(4) The three roles of JNI

There are three roles involved in JNI: C/C++ code, local method interface class, and specific business class in the Java layer.

JNI brief process

(5) JNI naming rules

Casually give an example as follows:

JNIExport jstring JNICALL Java_com_example_hellojni_MainActivity_stringFromJNI( JNIEnv* env,jobject thiz ) 

jstring is the return value type
Java_com_example_hellojni is the package name
MainActivity is the class name
stringFromJNI is the method name

Among them, JNIExport and JNICALL are keywords that are not fixed and reserved. Do not modify them.

(6) How to implement JNI

Steps in the JNI development process:

  • Step 1: Declare a native method in Java
  • Step 2: Compile the Java source file javac to get the .class file
  • Step 3: Export the JNI .h header file through the javah -jni command
  • Step 4: Use the native code that Java needs to interact with, and implement the Native method declared in Java (if Java needs to interact with C++, then use C++ to implement the Java Native method.)
  • Step 5: Compile the local code into a dynamic library (.dll file in Windows system, .so file in Linux system, .jnilib in Mac system)
  • Step 6: Execute the Java program through the Java command, and finally implement Java calling the native code.

PS: javah is a command that comes with the JDK. The -jni parameter indicates the function that generates the JNI rule for the function declared in the native class.

As shown below:

 (7) JNI structure

 The composition of this JNI function table is like the virtual function table of C++. A virtual machine can run multiple function tables, for example, one debug function table and another call function table. JNI interface pointers only work in the current thread. This means that pointers cannot pass from one thread to another. However, native methods can be called in a different thread.

 sample code

jdouble Java_pkg_Cls_f__ILjava_lang_String_2 (JNIEnv *env, jobject obj, jint i, jstring s)
{
     const char *str = (*env)->GetStringUTFChars(env, s, 0); 
     (*env)->ReleaseStringUTFChars(env, s, str); 
     return 10;
}

The method inside has three input parameters, let's look at them in turn:

  • *env: an interface pointer
  • obj: the object reference declared in the native method
  • i and s: parameters for passing

For the types of obj, i, and s, you can refer to the following JNI data types. JNI has its own primitive data types and data reference types as follows:

 Regarding env, it will be explained in the following JNI principle*.

(8) JNI principle

In a computer system, each programming language has an execution environment (Runtime), which is used to interpret the statements in the execution language. The execution environments of different programming languages ​​are like the "yin and yang worlds" in the mythical world. Ordinary people cannot live in both worlds of yin and yang at the same time, only some special immortals - "Impermanence in Black and White" can freely shuttle between the two worlds of Yin and Yang. According to the appointment recorded in the book of life and death to "request the soul".

The execution environment of the Java language is the Java Virtual Machine (JVM). The JVM is actually a process in the host environment. Each JVM virtual machine has a JavaVM structure in the local environment, which is returned when the Java virtual machine is created. , the function to create a JVM in the JNI environment is JNI_CreateJavaVM.

JNI_CreateJavaVM(JavaVM **pvm, void **penv, void*args);

1、JavaVM

 Among them, JavaVM is the representative of the Java virtual machine in the JNI layer. JNI has only a JavaVM structure that encapsulates some function pointers (or function table structures) globally. These function pointers encapsulated in the JavaVM are mainly for the JVM operation interface. In addition, the definition of JavaVM in C and C++ is different. In C, JavaVM is a JNIInvokeInterface_ type pointer, while in C++, JNIInvokeInterface_ is encapsulated once, which has one less parameter than C, which is why JNI The reason why the code is more recommended to use C++ to write.

Let's focus on JNIEnv.

2、JNIEnv

JNIEnv is the execution environment of the current Java thread. A JVM corresponds to a JavaVM structure, and multiple Java threads may be created in a JVM. Each thread corresponds to a JNIEnv structure, which is stored in the thread local storage TLS. Therefore, the JNIEnv of different threads is different and cannot be shared with each other. The JNIEnv structure is also a function table, which can be used to manipulate Java data or call Java methods through the JNIEnv function table in the local code. That is to say, as long as the JNIEnv structure is obtained in the local code, the Java code can be called in the local code.

2.1 What is JNIEnv?

JNIEnv is a thread-related structure that represents the execution environment of Java in this thread.

2.2 The difference between JNIEnv and JavaVM:

JavaVM: JavaVM is the representative of the Java virtual machine at the JNI layer. JNI has only one
JNIEnv globally: the code of JavaVM in the thread, each thread has one, and JNI may have many JNIEnv;

2.3. The role of JNIEnv:

Call Java function: JNIEnv represents the Java execution environment, you can use JNIEnv to call the code in Java Manipulate
Java code: Java object passed to JNI layer is the jobject object, you need to use JNIEnv to operate this Java object

2.4, the creation and release of JNIEnv
2.4.1, the creation of JNIEnv

JNIEnv creation and release: Obtained from JavaVM, which is divided into C and C++, let's look at it in turn:

  • In C——JNIInvokeInterface: JNIInvokeInterface is the JavaVM structure in the C language environment, and the JNIEnv structure can be obtained by calling the (AttachCurrentThread)(JavaVM, JNIEnv*, void) method;
  • In C++——_JavaVM: _JavaVM is the JavaVM structure in C++. Call the jint AttachCurrentThread(JNIEnv** p_env, void* thr_args) method to obtain the JNIEnv structure;
2.4.2. Release of JNIEnv
  • Release in C: Call the (DetachCurrentThread)(JavaVM) method in the JavaVM structure JNIInvokeInterface to release the JNIEnv of this thread
  • Release in C++: Call the jint DetachCurrentThread(){ return functions->DetachCurrentThread(this); } method in the JavaVM structure _JavaVM to release the JNIEnv of this thread
2.5, JNIEnv and threads

JNIEnv is thread-related, that is, there is a JNIEnv pointer in each thread, and each JNIEnv is thread-specific, and other threads cannot use the JNIEnv in this thread, that is, thread A cannot call the JNIEnv of thread B. So JNIEnv cannot cross threads.

  • JNIEnv is only valid in the current thread: JNIEnv is only valid in the current thread, and JNIEnv cannot be passed between threads. In the same thread, the JNI layer method is called multiple times, and the incoming JNIEnv is the same
  • Native methods match multiple JNIEnv: native methods defined in the Java layer can be called in different threads, so they can accept different JNIEnv
2.6, JNIEnv structure

JNIEnv is a pointer to a thread-related structure, and the thread-related structure points to an array of JNI function pointers. This array stores a large number of JNI function pointers, which point to detailed JNI functions.

2.7. Commonly used functions related to JNIEnv
2.7.1 Creating Objects in Java
jobject NewObject(JNIEnv *env, jclass clazz,jmethodID methodID, ...):
jobject NewObjectA(JNIEnv *env, jclass clazz,jmethodID methodID, const jvalue *args):
jobject NewObjectV(JNIEnv *env, jclass clazz,jmethodID methodID, va_list args):

The first parameter jclass class represents the object of which class you want to create, and the second parameter jmethodID methodID represents the constructor ID you want to use to create this object. As long as we have jclass and jmethodID, we can create an object of this Java class in a local method.

2.7.2 Create a String object in the Java class
jstring NewString(JNIEnv *env, const jchar *unicodeChars,jsize len):

Creates a new String object from an array of Unicode characters.

env is a JNI interface pointer; unicodeChars is a pointer to a Unicode string; len is the length of a Unicode string. The return value is a Java String object, or null if the string could not be constructed.

Is there a way to directly new a utf-8 string?
The answer is yes, that is jstring NewStringUTF(JNIEnv *env, const char *bytes) This method is to directly create a new string encoded as utf-8.

2.7.3 Create an array of primitive type PrimitiveType
ArrayType New<PrimitiveType>Array(JNIEnv *env, jsize length);

Specify a length and return an array of the corresponding Java primitive type.

2.7.4 Create an array of type elementClass
jobjectArray NewObjectArray(JNIEnv *env, jsize length,
jclass elementClass, jobject initialElement);

Create a new data group, the type is elementClass, all types are initialized to initialElement.

2.7.5 Get an element at a certain position in an array
jobject GetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index);

Returns an element of the Object array

2.7.6 Get the length of the array
jsize GetArrayLength(JNIEnv *env, jarray array);

Get the length of the array array.

Regarding the common methods of JNI, we will introduce them in detail in the next issue. Documentation can refer to https://docs.oracle.com.

(9) References to JNI

Java memory management is completely transparent. When creating a new instance, you only know that after creating an instance of this class, it will return a reference to this instance, and then use this reference to access its members (properties, methods), completely unnecessary Regardless of how the JVM is implemented internally, how to apply for memory for newly created objects, and how to release memory after use, you only need to know that there is a garbage collector handling these things. However, objects created from the Java virtual machine are passed to C /C++ code will generate references. According to Java's garbage collection mechanism, as long as there is a reference, it will not trigger the garbage collection of the Java object pointed to by the reference.

Java-like application types are also defined in JNI. In JNI, three reference types are defined:

  • Local Reference
  • Global Reference
  • Weak Global Reference

Let's take a look at it in turn:

1. Local Reference

Local references, also called local references, are usually created and used within functions. Will prevent GC from reclaiming all referenced objects.

The most common type of reference, basically the references returned by JNI are local references. For example, if NewObject is used, the local reference of the created instance will be returned. The local reference value is valid in this native function, and all generated in this function Local references are automatically released (freed) when the function returns, or you can use the DeleteLocalRef function to manually release the application. The reason why the DeleteLocalRef function is used : In fact, the existence of local references will prevent the object it points to from being recycled by the garbage collection period, especially when a local variable reference points to a very large object, or a local reference is generated in a cycle. A good practice is to release this reference after using the object, or at the end of the cycle, to ensure that it is reclaimed when the garbage collector is triggered . During the validity period of the local reference, it can be passed to other local functions. It should be emphasized that its validity period is still only in the first Java local function call, so you must not use all C++ variables to save it or put it Defined as a C++ static local variable.

2. Global Reference

Global references can be used across methods and threads until they are explicitly released by the developer. Similar to local references, a global reference guarantees that the referenced object will not be reclaimed by GC until it is released. Unlike local application, there are not so many functions that can create global references. The only function that can create all references is NewGlobalRef , and releasing it needs to use the ReleaseGlobalRef function

3. Weak Global Reference

It is a newly added function of JDK 1.2. Similar to global references, both creation and deletion need to be performed by programmers. This kind of references can be valid in multiple local codes like global references. The difference is that weak references will not prevent The garbage collection period reclaims the object pointed to by this reference, so you need to be more careful when using it. The object it refers to may not exist or has been recycled.

Generate and dereference by using NewWeakGlobalRef , ReleaseWeakGlobalRef .

4. Reference comparison

Given two references, no matter what they are, we just need to call the IsSameObject function to determine whether they point to the same object. code show as below:

(*env)->IsSameObject(env, obj1, obj2)

Returns JNI_TRUE (or 1) if obj1 and obj2 point to the same object, otherwise returns JNI_FALSE (or 0).

PS: There is a special reference to pay attention to: NULL, the NULL reference in JNI points to the null object in the JVM, if obj is a global or local reference, use (*env)->IsSameObject(env, obj, NULL) or obj == NULL is used to determine whether obj points to a null object. But it should be noted that when IsSameObject is used to compare weak global references with NULL, the meaning of the return value is different from that of local references and global references. code show as below:

jobject local_obj_ref = (*env)->NewObject(env, xxx_cls,xxx_mid);
jobject g_obj_ref = (*env)->NewWeakGlobalRef(env, local_ref);
// ... 业务逻辑处理
jboolean isEqual = (*env)->IsSameObject(env, g_obj_ref, NULL);

Since then, the basics of NDK and JNI have been explained. In the next article, let us learn about Android JNI learning (2) - "hello world" of actual combat JNI

Guess you like

Origin blog.csdn.net/Jason_Lee155/article/details/132234448