Recently, I was researching something related to Android camera. Because I wanted to make a package for the camera, I came up with the idea of providing interfaces that support filters and image dynamic recognition. I found some information on their implementation: one is based on OpenGL, one is based on OpenCV. Both can be developed directly in Java and are limited by the Java language, so when the performance requirements of the program are high, Java has a bit of a disadvantage. So, some ways to implement OpenCV are handled in the Native layer. This requires some knowledge of JNI.

Of course, JNI is not a concept introduced in Android, but was originally provided in Java. Therefore, in this article, we first try to use JNI development in IDEA, to understand the principle of JNI operation and some basic knowledge. Then, I’ll look at more efficient ways to use development in AS.

1. Declare native methods

1.1 Static Registration

First, declare a Java class,

package me.shouheng.jni;

public class JNIExample {

    static {
        // The system.loadLibrary () function is used to load DLL (Windows) or so (Linux) libraries.
        // No need to add the filename suffix (.dll or.so)
        System.loadLibrary("JNIExample");
        init_native();
    }

    private static native void init_native(a);

    public static native void hello_world(a);

    public static void main(String... args) { JNIExample.hello_world(); }}Copy the code

Native methods can be defined as static or non-static, and are used in the same way as normal methods. Here we use System.loadLibrary(“JNIExample”) to load the JNI library. On Windows it’s DLL, on Linux it’s so. Here JNIExample is just the name of the library and doesn’t even contain the suffix for the file type, so how does IDEA know where to load the library? This requires us to specify the vm parameters when running the JVM. The way in IDEA is to use Edit Configuration… Path =F:\Codes\Java\Project\ java-advanced\ java-advanced\lib, where the path is where my library file is located.

The first step in using JNI is to generate a header file. We can use the following command,

Javah-jni-classpath (search class directory) -d (output directory) (class name)Copy the code

Or, simply compile Java files to class and use class to generate h header files,

javac me/shouheng/jni/JNIExample.java
javah me.shouheng.jni.JNIExample
Copy the code

The above two commands are ok, just be careful about the file path. (Maybe we can use Java or some other language to write programs that call the executable to make it easier to use!)

The generated header file code is as follows,

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class me_shouheng_jni_JNIExample */

#ifndef _Included_me_shouheng_jni_JNIExample
#define _Included_me_shouheng_jni_JNIExample
#ifdef __cplusplus
extern "C" {
#endif
/* * Class: me_shouheng_jni_JNIExample * Method: init_native * Signature: ()V */
JNIEXPORT void JNICALL Java_me_shouheng_jni_JNIExample_init_1native
  (JNIEnv *, jclass);

/* * Class: me_shouheng_jni_JNIExample * Method: hello_world * Signature: ()V */
JNIEXPORT void JNICALL Java_me_shouheng_jni_JNIExample_hello_1world
  (JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif
#endif
Copy the code

As you can see, it has two more directives JNIEXPORT and JNICALL than ordinary C header files, and the rest of the stuff completely conforms to the rules of ordinary C header files. Java_me_shouheng_jni_JNIExample_init_1native Java_me_shouheng_jni_JNIExample_init_1native Java_me_shouheng_jni_JNIExample_init_1native Java_me_shouheng_jni_JNIExample_init_1native Java_me_shouheng_jni_JNIExample_init_1native In addition, the underline of Java layer is replaced by _1, because the underline of Native layer has been used to replace the comma of Java layer, so the underline of Java layer can only be represented by _1.

JNIEnv here is a pointer type that we can use to access Java layer code, and it cannot be called across processes. You can find its definition in jni.h in the include folder below the JDK. Jclass corresponds to the Class Class of the Java layer. The mapping between Java layer classes and Native layer classes is carried out according to the specified rules, as well as the mapping relationship of method signatures. Method signatures, such as ()V above, are visible when you decompile a class using Javap. They are actually a simplified way of describing class files, mainly to save memory in class files. In addition, method signatures are used to dynamically register JNI methods.

The mapping between reference types is as follows,

The above JNI registration method belongs to static registration, which can be understood as the method of registering Native in Java layer. In addition, there is dynamic registration, which is the method of registering the Java layer in the Native layer.

1.2 Dynamic Registration

In addition to statically registering native methods as described above, we can also register them dynamically. The dynamic registration approach requires that we use method signatures. Here is the mapping between Java types and method signatures:

Note that fully qualified class names here are separated by /, not by. Or _. The rules for method signature are as follows: Parameter 1 Type signature Parameter 2 Type signature…… Parameter n Type signature) returns the type signature. For example, long fun(int n, String STR, int[] arr) corresponds to a method whose signature is (ILjava/lang/String; [I] J.

The general JNI method dynamic registration flow is as follows:

  1. Utilized structureJNINativeMethodArray records the correspondence between Java methods and JNI functions;
  2. implementationJNI_OnLoadAfter loading the dynamic library, dynamic registration is performed.
  3. callFindClassMethod to get a Java object;
  4. callRegisterNativesMethod, pass in the Java object, and the JNINativeMethod array, and the number of registrations to complete the registration.

For example, the code above would look like this if dynamic registration was used:

void init_native(JNIEnv *env, jobject thiz) {
    printf("native_init\n");
    return;
}

void hello_world(JNIEnv *env, jobject thiz) {
    printf("Hello World!");
    return;
}

static const JNINativeMethod gMethods[] = {
        {"init_native"."()V", (void*)init_native},
        {"hello_world"."()V", (void*)hello_world}
};

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;
    jclass clazz = env->FindClass("me/shouheng/jni/JNIExample");
    if(! clazz){ __android_log_print(ANDROID_LOG_INFO,"native"."cannot get class: com/example/efan/jni_learn2/MainActivity");
        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

2. Execute the JNI program

Now that we know how to load, all that remains is how to get the DLL and so. On the Window platform, we compile the code into a DLL using VS or GCC. GCC has two options, MinGW and Cygwin. Can’t load IA 32-bit. DLL on a AMD 64-bit platform exception.

To check the bits of the VM, use Java-version. If the bits are 64-bit, the vm is 64-bit. If the bits are 32-bit, the VM is 32-bit. (See: How to identify JKD version numbers and bits, operating system bits.) MinGW can be downloaded at the following link: MinGW Distro – nuwen.net. After the installation is complete, enter GCC -v. If the version information is displayed, the installation is successful.

With header files, we need to implement native layer methods. We create a new C file jniExample. c and implement the following functions:

#include<jni.h>
#include <stdio.h>
#include "me_shouheng_jni_JNIExample.h"

JNIEXPORT void JNICALL Java_me_shouheng_jni_JNIExample_init_1native(JNIEnv * env, jclass cls) {
    printf("native_init\n");
    return;
}

JNIEXPORT void JNICALL Java_me_shouheng_jni_JNIExample_hello_1world(JNIEnv * env, jclass cls) {
    printf("Hello World!");
    return;
}
Copy the code

It looks pretty clear. Except for the JNIEXPORT and JNICALL symbols, everything else is basic C. Then we simply print an old friend Hello World in the method. Note that in addition to the basic input/output header stdio.h, we also include the generated header, as well as the jni.h, which is defined in the JDK and which we need to reference when building DLLS using GCC.

We use the following command to create an O file,

gcc -c -I"E:\JDK\include" -I"E:\JDK\include\win32" jni/JNIExample.c
Copy the code

The two -i’s here specify the path to the header file in the JDK. Because, as we said above, we refer to jni.h in the C file, which is in the JDK’s include directory. Since the header file in include references the header file in win32, we need to reference both.

Then, we use the following command to convert the above O files into DLL files,

gcc -Wl,--add-stdcall-alias -shared -o JNIExample.dll JNIExample.o
Copy the code

If you find that PowerShell can’t execute after you use it, you can replace, and then execute again.

Once the DLL is generated, we put it into our custom lib directory. As we mentioned above, this directory needs to be specified in the parameters of the virtual machine.

Then run and print the long-lost Hello World! Can.

3. Get closer to JNI: Call Java layer methods in Native

We define the following class,

public class JNIInteraction {

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

    private static native String outputStringFromJava(a);

    public static String getStringFromJava(String fromString) {
        return "String from Java " + fromString;
    }

    public static void main(String... args) { System.out.println(outputStringFromJava()); }}Copy the code

The desired result here is that the Java layer calls the outputStringFromJava() method of the Native layer. In the Native layer, this method calls the Java layer’s static method getStringFromJava() and passes in a string, and finally the entire concatenated string is passed to the Java layer via outputStringFromJava().

Above is the Java layer code, below is the Native layer code. The steps for Native layer to call Java layer methods are basically fixed:

  1. Through the JNIEnvFindClass()Function gets the class of the Java layer to call;
  2. Through the JNIEnvGetStaticMethodID()Function and above Java layer class, method name and method signature, get the Java layer method ID;
  3. Through the JNIEnvCallStaticObjectMethod()Function, the obtained class above, and the id of the method above, calling the Java layer method.

There are two points to note here:

  1. Here, since we are calling a static function from the Java layer, the function we use isGetStaticMethodID()CallStaticObjectMethod(). If you need to call an instance method of a class, then you need toGetMethodID()CallObjectMethod(). And so on, there are many other useful functions in JNIEnv that you can see by looking at the jni.h header file.
  2. The Java layer and Native layer methods calling each other is not difficult in itself, and the logic used is very clear. The only complication is that you need to spend extra time handling the conversion between the two environments. For example, for our purposes above, we need to implement a function that converts strings passed in from the Java layer to Native layer strings. It is defined as follows,
char* Jstring2CStr(JNIEnv* env, jstring jstr) {
    char* rtn = NULL;
    jclass clsstring = (*env)->FindClass(env, "java/lang/String");
    jstring strencode = (*env)->NewStringUTF(env,"GB2312");
    jmethodID mid = (*env)->GetMethodID(env, clsstring, "getBytes"."(Ljava/lang/String;) [B");
    
    // String.getByte("GB2312");
    jbyteArray barr = (jbyteArray)(*env)->CallObjectMethod(env, jstr, mid, strencode);
    jsize alen = (*env)->GetArrayLength(env, barr);
    jbyte* ba = (*env)->GetByteArrayElements(env, barr, JNI_FALSE);
    
    if(alen > 0) {
        rtn = (char*)malloc(alen+1); / / \ 0 ""
        memcpy(rtn, ba, alen);
        rtn[alen]=0;
    }
    (*env)->ReleaseByteArrayElements(env,barr,ba,0); //
    return rtn;
}
Copy the code

In the above function, we get the Java layer character array by calling String.getBytes() on the Java layer, and then copy it into the character array by memory copy. Memory is allocated by malloc() and the character pointer points to the first address of the requested memory. Finally, the JNIEnv method is called to free the memory of the character array. Here is also a Native call Java function process, but here the call String class instance method. (As you can see from this, there are many more factors to consider when writing code in Native layer than in Java layer. Fortunately, this is C language, and it may be better handled if C++ is modified.)

Returning to the previous discussion, we need to continue implementing Native layer functions:

JNIEXPORT jstring JNICALL Java_me_shouheng_jni_interaction_JNIInteraction_outputStringFromJava (JNIEnv *env, jclass _cls) {
    jclass clsJNIInteraction = (*env)->FindClass(env, "me/shouheng/jni/interaction/JNIInteraction"); / / class
    jmethodID mid = (*env)->GetStaticMethodID(env, clsJNIInteraction, "getStringFromJava"."(Ljava/lang/String;) Ljava/lang/String;"); // Get the method
    jstring params = (*env)->NewStringUTF(env, "Hello World!");
    jstring result = (jstring)(*env)->CallStaticObjectMethod(env, clsJNIInteraction, mid, params);
    return result;
}
Copy the code

In fact, its logic is relatively simple. This is basically the same procedure we used above to call the instance method of String, except that the static method is called.

The effect of this procedure is that when the Java layer calls the Native layer’s outputStringFromJava() function: First, the Native layer gets String from Java Hello World by calling the Java layer’s JNIInteraction static method getStringFromJava() and passing in the argument. This is then returned as the result of the outputStringFromJava() function.

4. Use JNI in Android Studio

The above approach to using JNI in an application is clunky, but thankfully Android Studio simplifies a lot of the process. This allows us to focus more on implementing Native layer and Java layer code logic without having to worry too much about the complexity of compilation.

The way to enable JNI in AS is simple: check include C++ support when creating a new project using AS. The other steps are no different from creating a normal Android project. Then you need to do a simple configuration of the development environment. You will need to install the following libraries, namely CMake, LLDB and NDK:

AS simplifies our compilation process largely thanks to the compilation tool CMake. CMake is a cross-platform installation (compilation) tool that describes installation (compilation) on all platforms in a simple statement. We simply describe the entire compilation process using its specific syntax in the cmakelists.txt file it specifies, and then use the CMake instructions. You can read the documentation to learn how to use CMake: add-native code in AS. Or through the following article a simple introduction to CMake: CMake entry combat.

Android projects that support JNI development are not very different from normal projects. Apart from specifying the NDK directory in local.properties, the main differences between the project structure and Gradle configuration are as follows:

It can be seen that the main differences lie in:

  1. There is a CPP directory under the main directory for writing C++ code;
  2. Cmakelists. TXT is the configuration file of CMake mentioned above.
  3. Gradle specifies the location of the cmakelists. TXT file in one place and the compilation of CMake in the other.

In addition to CMake, the advantages of JNI development in AS include:

  1. There is no need to manually register methods dynamically and statically. When you define a Native method in the Java layer, you can directly generate the corresponding method in the Native layer by right-clicking.
  2. In addition, AS can establish the connection between Native layer and Java layer methods, you can directly jump between the two methods;
  3. When programming with AS, prompt options will be given when calling Native layer classes. For example, JNIEnv above can give hints of its internal methods.

In addition, from the initial project and Android Native layer source code, Google is supporting us to use C++ development. So, that long-forgotten C++ book can come in handy again…

conclusion

The above.


Android from basic to advanced, pay attention to the author to get more knowledge in time

This and other articles in this series are hosted on Github: Github/Android-Notes. Welcome to Star & Fork. If you liked this article and would like to support the author’s work, please like this article 👍!