This is the second day of my participation in Gwen Challenge

Like attention, no more lost, your support means a lot to me!

🔥 Hi, I’m Chouchou. GitHub · Android-Notebook has been included in this article. Welcome to grow up with Chouchou Peng. (Contact information at GitHub)

preface

  • For Java/Android engineers, native development is a necessary step up the engineering ladder and a great way to differentiate yourself from your competitors in an interview! In order to unlock the Native skill tree, the first step is to unlock the JNI (Java Native Interface) cornerstone runes.
  • In this article, I’ll take you through the basics of JNI programming. Please be sure to like and follow if you can help, it really means a lot to me.
  • The code for this article can be downloaded from DemoHall·HelloJni.

directory


Front knowledge

The content of this article will involve the following pre/related knowledge, dear I have prepared for you, please enjoy ~

  • C language review notes
  • C++ review notes
  • Why is Java platform independent?

1. An overview of the

1.1 What problems does JNI solve?

Java designed the JNI mechanism to enhance Java’s ability to interact with native code. Let’s start with the difference between Java and native code: we know that the runtime environment/platform of the program is mainly operating system + CPU, and each platform has its own native library and CPU instruction set. Native languages such as C/C++ convert to platform-dependent native code and are not cross-platform in nature. On the other hand, Java is cross-platform with the support of virtual machines and bytecodes, but on the other hand, Java’s ability to interact with native code is weak, and native platform-specific features cannot be fully utilized. Therefore, it is necessary to design JNI mechanisms to enhance Java’s ability to interact with native code.

Tip: Native code is usually C/C++, but not limited to C/C++.

1.2 Advantages of JNI

  • 1. Solve the efficiency problem of intensive computing, such as image processing, OpenGL, games and other scenes are all implemented in Native;
  • 2. Reuse existing C/C++ libraries, such as OpenCV.

1.3 What did JNI sacrifice?

  • 1. The native language does not have cross-platform features, so it must be compiled for different runtime environments.
  • 2. Java and Native call each other less efficiently than Java call Java (note: call efficiency is low, not to be confused with execution efficiency);
  • 3, increased the complexity of the project.

2. First JNI program

In this section, we demonstrate the basic flow of JNI programming through a simple HelloWorld program.

2.1 Basic process of JNI programming

  • Create helloworld.java and declare native sayHi();
  • 2. Compile the source file using javac command to generate the HelloWorld. Class bytecode file;
  • 3. Use javah command to export helloworld. h header file, which contains the function prototype of the local method;
  • 4. Use C/C++ to achieve function prototype;
  • 5. Compile local code to generate hello-world-so dynamic library file;
  • 6. Call System.loadLibrary(…) in Java code Load so file;
  • 7. Run the HelloWorld program using Java commands.

The source code is not shown here, you can download the Demo to view, download path: HelloJni. Only JNI function declarations are shown here:

JNIEXPORT void JNICALL Java_com_xurui_hellojni_HelloWorld_sayHi (JNIEnv *, jobject);
Copy the code

2.2 Details

Here are some of the most confusing questions for beginners:

  • Question 1: Why should #ifndef #define #endif be added to header files?

A: To avoid double compilation when a header file is referenced by multiple files, place the contents of the header file between #ifndef and #endif. Common templates are as follows:

#ifndef < macro > #define < macro > content...... #endifCopy the code
  • 2. Why extern “C”?

A: Extern “C” means that even in a C++ environment, all C standards should be compiled. We can find the answer in the jni.h file: because both JavaVM and JNIEnv in jNI methods end up calling JNIInvokeInterface_ and JNINativeInterface_ in C. (Todo’s argument is not sufficient)

jni.h

struct JNIEnv_; struct JavaVM_; #ifdef __cplusplus typedef JNIEnv_ JNIEnv; typedef JavaVM_ JavaVM; #else typedef const struct JNINativeInterface_ *JNIEnv; // typedef const struct JNIInvokeInterface_ *JavaVM; Struct JNIEnv_ {const struct JNINativeInterface_ *functions; // Struct JNINativeInterface_ *functions; . } struct JavaVM_ { const struct JNIInvokeInterface_ *functions; . }Copy the code
  • Question 3: Why is the JNI function name Java_com_xurui_HelloWorld_sayHi?

A: This is the function naming rule of JNI function static registration convention. When Java VIRTUAL machine calls native method, it needs to execute corresponding JNI function. JNI function registration discusses how to determine the mapping between Native method and JNI function, there are two methods: Static and dynamic registration. Static registration uses convention based naming rules, using the “short name” rule when there is no overload and the “long name” rule when there is an overload. More detailed analysis before I discussed in an article: the NDK | take the way you comb JNI function registered and timing

  • Question 4: What does the keyword JNIEXPORT mean?

A: JNIEXPORT is a macro definition that represents when a function needs to be exposed for external use in a shared library. JNIEXPORT is defined differently on Windows and Linux:

Windows: #define JNIEXPORT __declspec(dllexport) #define JNIIMPORT __declspec(dllimPort)  #define JNIIMPORT #define JNIEXPORT __attribute__ ((visibility ("default")))Copy the code
  • Question 5: What does the keyword JNICALL mean?

A: JNICALL is a macro definition that indicates that a function is a JNI function. JNICALL has different definitions on Windows and Linux:

Windows platform: #define JNICALL __stdCall // __stdCall is a convention for function call arguments that are from right to left. Linux platform: #define JNICALLCopy the code

Question 6: What is the first parameter JNIEnv*? A: The first argument is a JNIEnv pointer to a JNI function table. These JNI functions give native code access to the internal data structures of the Java virtual machine. The JNIEnv pointer also has the effect of masking the internal implementation details of the Java virtual machine, allowing the native code base to be transparently loaded into different Java virtual machine implementations (at the cost of invocation efficiency).

Question 7: What is the second parameter jobject? A: The second parameter varies depending on whether the native method is static or instance. For static Native methods, the second parameter jclass represents the Class object of the native method’s Class. For instance Native methods, the second parameter, jobject, represents the object calling Native.

2.3 Type of Mapping

Java types are mapped to JNI types in JNI. The mapping is defined in the jni.h file. Jbyte, Jint, and jlong are defined in the jni_mD. h file depending on the runtime environment. The summary is as follows:

Java type JNI type describe Length (bytes)
boolean jboolean unsigned char 1
char jchar unsigned short 2
short jshort signed short 2
float jfloat signed float 4
double jdouble signed double 8
int Jint, jsize signed int 2 or 4
long jlong signed long 4 or 8 (LP64)
byte jbyte signed char 1
Class jclass Java Class objects /
String jstrting Java string object /
Object jobject Java object /
byte[] jbyteArray Byte array /

3. JNI invokes Java code

In this section, we discuss how to access Java fields and methods in JNI, and how to access Java code in native code, using ids to access fields or methods. The process of retrieving ids frequently is time-consuming, and we often need to cache ids to optimize our performance.

3.1 JNI accessing Java fields

The process for native code to access Java fields is two-step:

  • 1. Obtain the field ID from jclass, such as:Fid = env->GetFieldId(clz, "name", "Ljava/lang/String;" );
  • 2. Access the field through the field ID, such as:Jstr = env->GetObjectField(thiz, Fid);

Note: Ljava/lang/String; Is the field descriptor for instance field name. Strictly speaking, “field descriptors” are the rules that describe fields in JVM bytecode and have nothing to do with JNI directly. Field descriptors and method descriptors can also be generated automatically using the Javap command, and Android Studio helps with that as well. The complete field descriptor rules are as follows:

Java type Field descriptor
boolean Z
byte B
char C
short S
int I
long J
float F
double D
void V
Reference types Begins with L; The end is separated by the package name and the class name.

For example, the field descriptor of String is Ljava/lang/String.

Java fields are divided into static fields and instance fields. The following six methods are used to obtain or modify Java fields by local code:

  • GetFieldId: Gets the field ID of the instance method
  • GetStaticFieldId: Gets the field ID of the static method
  • GetField: Get instance fields of Type Type (e.g. GetIntField)
  • SetField: sets an instance field of Type Type (for example, SetIntField)
  • GetStaticField: Get a static field of Type Type (such as GetStaticIntField)
  • SetStaticField: Sets a static field of Type Type (for example, SetStaticIntField)

native-lib.cpp

extern "C" JNIEXPORT void JNICALL Java_com_xurui_hellojni_HelloWorld_accessField(JNIEnv *env, Jclass CLZ = env->GetObjectClass(thiz); JfieldID sFieldId = env->GetStaticFieldID(CLZ, "sName", "Ljava/lang/String; ); JStr = static_cast<jstring>(env->GetStaticObjectField(CLZ, sFieldId)); Const char *sStr = env->GetStringUTFChars(jStr, NULL); LOGD(" static field: %s", sStr); env->ReleaseStringUTFChars(jStr, sStr); Jstring newStr = env->NewStringUTF(" static field -peng "); if (newStr) { env->SetStaticObjectField(clz, sFieldId, newStr); JfieldID mFieldId = env->GetFieldID(CLZ, "mName", "Ljava/lang/String; ); JStr = static_cast<jstring>(env->GetObjectField(thiz, mFieldId)); Const char *sStr = env->GetStringUTFChars(jStr, NULL); LOGD(" instance field: %s", sStr); env->ReleaseStringUTFChars(jStr, sStr); Jstring newStr = env->NewStringUTF(" instance string-peng "); if (newStr) { env->SetObjectField(thiz, mFieldId, newStr); }}}Copy the code

3.2 JNI invokes Java methods

Accessing Java from native code is similar to accessing Java fields in a two-step process:

  • 1. Get method ID from jclass, such as:Mid = env->GetMethodID(jclass, "helloJava", "()V");
  • 2. Call the method with the method ID, such as:env->CallVoidMethod(thiz, Mid);

Note that :()V is the method descriptor of the instance method helloJava. Strictly speaking, “method descriptors” are the rules that describe methods in the JVM bytecode and have no direct relationship to JNI.

Java methods are divided into static methods and instance methods. Native code calls Java methods mainly using the following five methods:

  • GetMethodId: obtains the ID of the instance method
  • GetStaticMethodId: Obtains the ID of a static method
  • CallMethod: Call an instance method that returns Type Type (such as GetVoidMethod)
  • CallStaticMethod: Call a static method that returns Type Type (for example, CallStaticVoidMethod)
  • CallNonvirtualMethod: Call a parent method that returns Type Type (for example, CallNonvirtualVoidMethod)

native-lib.cpp

extern "C" JNIEXPORT void JNICALL Java_com_xurui_hellojni_HelloWorld_accessMethod(JNIEnv *env, Jclass CLZ = env->GetObjectClass(thiz); JmethodID = env->GetStaticMethodID(CLZ, "sHelloJava", "()V"); if (sMethodId) { env->CallStaticVoidMethod(clz, sMethodId); JmethodID mMethodId = env->GetMethodID(CLZ, "helloJava", "()V"); if (mMethodId) { env->CallVoidMethod(thiz, mMethodId); }}Copy the code

3.3 the cache ID

  • Why cache ID: When accessing a Java layer field or method, you need to retrieve the field name/method name and descriptor to obtain jfieldID/jmethodID. This retrieval process is time-consuming, and the optimization method is to cache the field ID and method ID to reduce repeated retrieval.

  • Methods of caching ID: There are two main methods of caching field ID and method ID: in-use cache + initial-time cache. The main differences lie in the timing of caching and the timeliness of cache ID.

In-use cache:

Use-time caching means storing the field ID or method ID in a static variable the first time a field or method is accessed. This eliminates the need to retrieve the ID repeatedly when local methods are called again in the future. Such as:

Jstring MyNewString(JNIEnv* env, jchar* chars, jint len) {static jmethodID cid = NULL; jclass stringClazz = (*env)->FindClass(env,"java/lang/String"); if(NULL == cid) { cid = (*env)->GetMethodID(env,stringClazz,"<init>","([C)V"); } jcharArray elemArr = (*env)->NewCharArray(env,len); (*env)->SetCharArrayRegion(env, elemArr, 0, len, chars); jstring result = (*env)->NewObject(env, stringClazz, cid, elemArr); (*env)->DeleteLocalRef(env,elemArr); (*env)->DeleteLocalRef(env,stringClazz); return result }Copy the code

Tip: Is there a problem with multiple threads accessing this local method using the same cache ID? No, the field ID or method ID computed by multiple threads is actually the same.

Statically initialized cache:

Static initialization-time caching means that the field IDS and method ids are cached in advance when a Java class is initialized. Such as:

private static native void initIDs(); Static {// Java class initializes system. loadLibrary("InstanceMethodCall"); initIDs(); } ---------------------------------------------------- jmethodID cid; jmethoidID stringId; JNIEXPORT void JNICALL Java_InstanceMethodCall_initIDs(JNIEnv *env, jclass cls) { cid = (*env)->GetMethodID(env, cls, "callback", "()V"); jclass stringClazz = (*env)->FindClass(env,"java/lang/String"); stringId = (*env)->GetMethodID(env,stringClazz,"<init>","([C)V"); }Copy the code

3.4 Comparison and Application scenarios of the two Cache ID modes

In most cases, field ids and method ids should be cached whenever possible during static initialization, because caching has some limitations when used:

  • 1. Check the validity of the cache before each use;
  • 2. The field ID and method ID become invalid when Java classes unload, so you need to ensure that this ID is not used after the class is unloaded. Static initialization caches retrieve the ID during class load, so you don’t have to worry about ID invalidation.

Of course, caching is not without its advantages. If you cannot modify the Java source code, caching is a natural choice. Another advantage is that the use-time cache is equivalent to lazy initialization and can retrieve ids on demand, whereas the statically initialized cache is equivalent to pre-initialization and retrieves all ids at once. Still, static initialization-time caching is used in most cases.

3.5 What is AN ID and What is a Reference?

References are native code to manage resources in the JVM, and multiple references to the same object can be created at the same time; The field ID and method ID are managed by the JVM, and the ID of the same field or method is fixed and becomes invalid only when the owning class is unloaded.


4. Process of loading and unloading SO library

About the whole process of loading and unloading so library, before I wrote an article said: “the NDK | about so library from loading to unloading process”. Let me repeat it briefly:

  • 1. The general process of so library loading and unloading is mainly divided into: determining the absolute path of SO library, loading nativeLoad into memory, following the unloading of ClassLoader;
  • 2. Search path of SO library, which is divided into App path (/data/app/[packagename]/lib/arm64) and system paths (/ system/lib64, / vendor/lib64);
  • 3,JNI_OnLoadwithJNI_OnUnLoadIt is executed when the SO library is loaded and unloaded respectively.


5. Register JNI functions

And timing on JNI function registered way, before I wrote an article about: “the NDK | with the way you comb JNI function registered and timing”. Let me repeat it briefly:

  • 1. When calling a native method defined in a Java class, the virtual machine calls the corresponding JNI functions, which need to be registered before they can be used.
  • 2, the way to register JNI functions is divided into static registration & dynamic registration
  • 3. There are three opportunities to register JNI functions:
Timing of registration Corresponding registration mode
1. When the VM invokes native methods for the first time Static registration
2. When the Android VM starts Dynamic registration
3. When loading so library Dynamic registration

6. JNIEnv * and JavaVM

6.1 Function of JNIEnv * pointer

The JNIEnv* pointer points to a table of JNI functions that, in native code, can be used to access data structures in the JVM. In this sense, it can be understood that JNIEnv* points to the Java environment, but it cannot be said that JNIEnv* represents the Java environment.

Note: If a local method is called by a different thread, the JNIEnv pointer passed in is different. The JNIEnv pointer is valid only in the thread in which it exists and cannot be passed and used across threads (or even processes). But the table of functions that JNIEnv points to indirectly is shared across multiple threads.

6.2 JavaVM Functions

JavaVM represents a Java virtual machine. Each Java VIRTUAL machine corresponds to a JavaVM object, which is shared between threads. We can get a JavaVM object using JNIEnv* :

jint GetJavaVM(JNIEnv *env, JavaVM **vm); - VM: indicates the pointer used to store the obtained VM pointer. - return: 0 is returned on success and others are returned on failure.Copy the code

6.3 Obtaining the JNIEnv* pointer at any location

The JNIEnv* pointer is only valid for the thread that created it, and if we need to access the JVM from another thread, we must call AttachCurrentThread to associate the current thread with the JVM before we can get the JNIEnv* pointer. In addition, you need to call DetachCurrentThread to unlink.

jint AttachCurrentThread(JavaVM* vm , JNIEnv** env , JavaVMAttachArgs* args); - VM: indicates the VM object pointer. -env: the pointer used to hold the resulting JNIEnv pointer; -args: link parameter. The parameter structure is shown as follows. - return: 0 is returned if the connection succeeds, and others are returned if the connection fails. ----------------------------------- func() { JNIEnv *env; (*jvm)->AttachCurrentThread(jvm, (void **)&env, NULL); }Copy the code

7. To summarize

Today we discussed the basic concepts of JNI programming and the steps to use it. We also discussed the steps to call Java code from native code. We also introduced a method to make calls more efficient — cache ids. In addition, the “loading & unloading process of the SO library” and “how and when to register JNI functions” were discussed last year, hoping to help you build a system understanding of JNI programming. I will publish more articles to discuss advanced concepts of JNI programming, such as references, multithreading, exception handling, and so on. Remember to pay attention ~


The resources

  • JNI Tip — Google Developers
  • An In-depth Understanding of JVM bytecode (Chapter 10) by Ya Zhang
  • The Definitive Guide to Java Performance (Chapters 7 and 8) by Scott Oaks
  • JNI Programming Guide

Creation is not easy, your “three lian” is chouchou’s biggest motivation, we will see you next time!