Detailed explanation of Android JNI

Table of contents

I. Introduction:

2. Introduction to JNI

3. JNI function registration

3.1 Static registration:

3.2 Dynamic registration

4. Function signature

 4.1 What is a function signature:

4.2 Why the signature of the function is needed:

4.3 How to get the signature of a function

5. JNIEnv

5.1 What is JNIEnv:

5.2 Calling java object methods through JNIEnv

5.3 How to call java methods across threads

6. Garbage collection


I. Introduction:

        This article is a theoretical analysis of the jni technology used in the android development process. It will not introduce the specific use of jni. For tutorials on how to use jni development in android, you can search online and try it yourself. This article mainly introduces such as the registration of jni functions, the thread mapping relationship between jni and java layers, etc.

2. Introduction to JNI

        2.1 JNI is the abbreviation of Java Native Interface, which means "Java local call". This can be achieved through JNI technology

                Java calls C program

                C program calls Java code

Many codes in our android source code are implemented by Jni. For example, the implementation of MediaScanner. It is through jni technology that we can scan media-related resources at the java layer.

         2.2 Steps to use JNi:

                The steps to use Jni can be roughly as follows:

                a) Java declares native function

                b) jni implements the corresponding c function

                c) Compile and generate so library

                d) java loads the so library and calls the native function

3. JNI function registration

        We know that jni functions directly call the native functions of the java layer when used. When we usually develop jni, we may follow the step by step, define a class, add a few native functions, and then use javac, javah and other commands. , and then implement the corresponding C file, and finally compile it into an so library for use. So how does the native function in the Java layer call the function in the C language? Here we have designed the registration issue of JNI functions. JNI functions are divided into static registration and dynamic registration.

3.1 Static registration:

        We usually use the static registration method more often. The way we compile the header file through javac and javah, and then implement the corresponding cpp file is a static registration method. This method of calling is because the JVM matches the corresponding native function according to the default mapping rules. If there is no match, an error will be reported. What are the specific matching rules? You can take a look at the following example


//Java 层代码JniSdk.java
public class JniSdk {
    static {
        System.loadLibrary("test_jni");
    }

    public native String showJniMessage();
}


//Native层代码 jnidemo.cpp
extern "C"
JNIEXPORT jstring JNICALL Java_com_example_dragon_androidstudy_jnidemo_JniSdk_showJniMessage
  (JNIEnv* env, jobject job) {
    return env->NewStringUTF("hello world");
}

From the above example, you can see that the showJniMessage function in the java layer corresponds to the Java_com_example_dragon_androidstudy_jnidemo_JniSdk_showJniMessage function in the c language. So how is this mapped? In fact, this mapping is implemented by the JVM. When we call the showJniMessage function, the JVM will find the corresponding function from the JNI library and call it. Follow these rules when searching:

The package name of our JniSdk.java is com.example.dragon.androidstudy.jnidemo. Then the complete path of the showMessage method is com.example.dragon.androidstudy.jnidemo.JniSdk.showJniMessage. However, there are special functions in C, so the JVM replaces the others with _. And add the Java_ logo in front, it becomes the above method

3.2 Dynamic registration

        I don’t know if you have noticed that every time you use JNI, you must first create native and then compile it into a class. Generating header files. Finally, implement the native function according to specific rules. This whole process is not only cumbersome, but also many steps are unnecessary (we do not need to generate the .h file, we just need to declare the function according to the corresponding rules in the .c file). Moreover, the JVM will be less efficient when calling functions based on static registration matching rules. Therefore, there is a method of dynamic registration. The so-called dynamic registration means that we do not use the default mapping rules and directly tell the JVM. Which function in the C file corresponds to the native function of java. Without further ado, let’s go straight to examples.

public class JniSdk {

    static {
        System.loadLibrary("test_jni");
    }


    public static native int numAdd(int a, int b);

    public native void dumpMessage();
}

JNINativeMethod g_methods[] = {
        {"numAdd", "(II)I", (void*)add},
        {"dumpMessage","()V",(void*)dump},
};

jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    j_vm = vm;
    JNIEnv *env = NULL;
    if (vm->GetEnv((void**)&env, JNI_VERSION_1_2) != JNI_OK) {
        LOGI("on jni load , get env failed");
        return JNI_VERSION_1_2;
    }
    jclass clazz = env->FindClass("com/example/dragon/androidstudy/jnidemo/JniSdk");
    //clazz对应的类名的完整路径。把.换成/   g_methods定义的全局变量     1  是g_methods的数组长度。也可以用sizeof(g_methods)/sizeof(g_methods[0])
    jint ret = env->RegisterNatives(clazz, g_methods, 2);
    if (ret != 0) {
        LOGI("register native methods failed");
    }
    return JNI_VERSION_1_2;
}

The JNI_OnLoad function above is a function that the JVM will call back when we pass the System.loadlibrary function. This is where we do the dynamic registration. As you can see, we register through env->RegisterNatives. This env is the core of jni function implementation, we will talk about it later. Here we mainly explain the g_methods object. The following is the definition of the JNINativeMethods structure.

typedef struct {
    const char* name;   //对应java中native的函数名
    const char* signature;  //java中native函数的函数签名
    void*       fnPtr;  //C这边实现的函数指针
} JNINativeMethod;

Where signature refers to the signature of the function. This will be explained in detail in the next section

4. Function signature

        According to what was discussed in the third section, we need to use the signature of the function when registering dynamically. So what is the signature of a function? Why do we need the signature of the function? How to get the signature of a function? Next, we will give an explanation on these three issues.

 4.1 What is a function signature:

        The so-called function signature can be understood simply as the unique identifier of a function, and a signature corresponds to the signature of a function. This is a one-to-one correspondence. Some people may ask: Can't function names be used as identifiers? The answer is of course no

4.2 Why the signature of the function is needed:

        We know that java supports function overloading. There can be multiple functions with the same name but different parameters in a class, so the function name + parameter name uniquely constitute a function identifier, so we need to make a signature identifier for the parameters. In this way, the jni layer can uniquely identify a function

4.3 How to get the signature of a function

        The signature of a function is composed of the parameters and return value of the function. It follows the following format (parameter type 1; parameter type 2; parameter type 3...) return value type. For example, the same as our numAdd function above. His function declaration at the java layer is

int numAdd(int a, int b)

There are two parameters here that are int, and the return value is also int. So the function signature is (II)I. The dumpMessage function has no parameters and the return value is empty, so its signature is ()V. The mapping relationship between signatures corresponding to specific function types is as follows:

type identifier Java types type identifier Java types
Z boolean F float
B      byte D double
C char L/java/language/String String
S short [I         int[]
I         int [Ljava/lang/object         Object[]
J long V void

If we write this kind of signature manually. It is easy to make mistakes. There is a tool that can easily list the signature of each function. We can first compile the class file through the javac command. Then use the javap -s -p xxx.class command to list all the function signatures of this class file.

public native java.lang.String showJniMessage();
    descriptor: ()Ljava/lang/String;

  public static native int numAdd(int, int);
    descriptor: (II)I

  public native void dumpMessage();
    descriptor: ()V

5. JNIEnv

        The previous sections have talked about the mapping and calling relationship between java functions and jni functions. Once the mapping relationship is established, calling native functions in the java layer becomes very simple. But our program is not just such a simple requirement. Most of the time, the jni function needs to call the java layer function. For example, after we perform background file operations and notify the upper layer of the results, we need to call the java function at this time. . So how to achieve this at this time? At this time, our protagonist JNIEnv will appear. This JNIEnv can be said to run through the core of the entire JNI technology, so we will focus on explaining this

5.1 What is JNIEnv:

        JNIEnv is a thread-related structure maintained internally by the JVM that represents the JNI environment. This structure is related to threads. And there is a one-to-one correspondence between the threads in the C function and the threads in the java function. In other words, if a thread in Java calls the jni interface, no matter how many JNI interfaces are called, the JNIEnv passed is the same object. Because java has only one thread at this time, the corresponding JNI also has only one thread, and JNIEnv is bound to the thread, so there is only one


mShowTextBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                JniSdk.getInstance().dumpArgs(arg);
                JniSdk.getInstance().dumpArgsBak(arg);
            }
        });


void dumpArg(JNIEnv *env, jobject call_obj, jobject arg_obj) {
    LOGI("on dump arg function, env :%p", env);
}

void dumpArgBak(JNIEnv *env, jobject call_obj, jobject arg_obj) {
    LOGI("on dump arg bak function, env :%p", env);
}

//实际输出
//on dump arg function, env :0xb7976980
//on dump arg bak function, env :0xb7976980

It can be seen that the JNIEnv printed in two different JNI interfaces is the same. If they are different threads, the JNIEnv printed is different. Interested friends can do experiments by themselves

5.2 Calling java object methods through JNIEnv

        Calling methods through JNIEnv can be roughly divided into the following two steps:

        a. Obtain the class of the object and obtain the member attributes through the class

        b. Get the corresponding value through member attribute setting or call the corresponding method

There are many examples on the Internet, and the specific ones are beyond the scope of this article. You can experiment on Baidu yourself



public class JniSdk {
    private int mIntArg = 5;
    public int getArg() {
        return mIntArg;
    }
}

void dump(JNIEnv *env, jobject obj) {
    LOGI("this is dump message call: %p", obj);
    jclass jc = env->GetObjectClass(obj);
    jmethodID  jmethodID1 = env->GetMethodID(jc,"getArg","()I");
    jfieldID  jfieldID1 = env->GetFieldID(jc,"mIntArg","I");
    jint  arg1 = env->GetIntField(obj,jfieldID1);
    jint arg = env->CallIntMethod(obj, jmethodID1);
    LOGI("show int filed: %d, %d",arg, arg1);
}

One thing to note here is: if the jni method is called in static mode, the jobject here represents a jclass object, which needs to be forced and does not represent an independent object.

5.3 How to call java methods across threads

The above method of java calling jni or jni calling java seems to be nothing special, right? It's nothing more than calling several interfaces of JNIEnv. But in fact this is not the case. The reason why the above can be called directly is that when java calls to the jni layer, it is always in the same thread. Therefore, the jni layer can directly operate the JNIEnv object passed down from the java layer to implement various operations. But what if an additional thread created in the JNI layer wants to call a Java method? How should we operate at this time?

As mentioned above, JNIEnv corresponds to threads one-to-one. In fact, the one-to-one correspondence here refers to the common correspondence with the threads of java and jni. What does that mean? Here we can use several pictures to represent

The above figure shows that the JVM internally maintains a table about thread mapping. A java thread and a jni thread jointly own a JNIEnv. If the JVM has not established a mapping relationship between the two threads when the java thread calls the native function, a new JNIEnv will be created and passed to the jni thread, if a mapping relationship has been created before. Then just use the original JNIEnv directly. As described in 5.1, the two JNIEnv objects are identical. Vice versa, if jni calls a java thread, you need to apply to the JVM to obtain the mapped JNIEnv, if it has not been mapped before. Then create a new one. This method is AttachCurrentThread.



JNIEnv *g_env;

void *func1(void* arg) {
    LOGI("into another thread");
    //使用全局保存的g_env,进行操作java对象的时候程序会崩溃
    jmethodID  jmethodID1 = g_env->GetMethodID(jc,"getArg","()I");
    jint arg = g_env->CallIntMethod(obj, jmethodID1);
    
    //通过这种方法获取的env,然后再进行获取方法进行操作不会崩溃
    JNIEnv *env;
    j_vm->AttachCurrentThread(&env,NULL);

}

void dumpArg(JNIEnv *env, jobject call_obj, jobject arg_obj) {
    LOGI("on dump arg function, env :%p", env);
    g_env = env;
    pthread_t *thread;
    pthread_create(thread,NULL, func1, NULL);
}

The above demo indicates that JNIEnv is bundled with each thread, and thread A's JNIEnv cannot be accessed in thread B. So it is not possible to use it by saving g_env. Instead, you should obtain the new JNIEnv through the AttachCurrentThread method, and then call it

6. Garbage collection

        We all know that objects created by java are reclaimed and released by the garbage collector. So is the Java method feasible on the JNI side? The answer is no? At the JNI layer. If you use ObjectA = ObjectB to save variables. There is no way to save variables in this way. It will be recycled at any time, we must create it through env->NewGlobalRef and env->NewLocalRef, and there is also env->NewWeakGlobalRef (this is rarely used)

        So what are the life cycles of these two types? The conclusion is given directly here:

        The variables created by NewLocalRef will be released after the function call is completed.

        Variables created by NewGlobalRef will always exist unless deleted manually.

At this point, the understanding of the operating mechanism of JNI has been summarized.

Guess you like

Origin blog.csdn.net/u014296267/article/details/127850485