Summary of Android JNI Development

[Reprint] Link to the original text JNI_NDK Getting Started Details

In Android development, for a variety of reasons (cross-platform, high performance, sensitive data processing, etc.), a well-known JNI(Java Native Interface) is needed. This article will take you to review some common points of knowledge in JNI. So there are no explanations for basic environment configuration in this article. If you need to, you can read what I wrote earlier:

  1. JNI Begins to Know HelloWorld
  2. Mutual invocation and basic operation of JNI Java and C

Demo related to this article: https://github.com/xfhy/AllInOne/blob/master/app/src/main/cpp/native-lib.cpp

First, look at the catalog:

[External chain picture transfer failed, source station may have anti-theft chain mechanism, it is recommended to save the picture and upload it directly (img-5hhGwJX3-1627120575612).) https://i.loli.net/2020/06/13/BZuTx24cQaHX6jv.png )]

1. JNI development process

  1. Write a java class that declares the native method
  2. Write native code
  3. Compile native code into so file
  4. Introduce so Library in java class, call native method

2. native method naming

extern "C"
JNIEXPORT void JNICALL
Java_com_xfhy_jnifirst_MainActivity_callJavaMethod(JNIEnv *env, jobject thiz) {
    
}

Naming rules for functions: Java_ Class Full Path_ Method Name

Function parameters:

  1. JNIEnv* is the first parameter that defines any native function and is a pointer to the JNI environment through which to access the interface methods provided by JNI.
  2. jobject: Represents this in a Java object. If static, use jclass
  3. JNIEXPORT and JNICALL: These are macros defined in JNI and can be used in jni.h Find it in this header file.

3. JNI data types and their mapping to Java data types

First write a native method declaration in your Java code, then alt+enter asks AS to help create a native method. Look at the type correspondence

public static native void ginsengTest(short s, int i, long l, float f, double d, char c,
                                   boolean z, byte b, String str, Object obj, MyClass p, int[] arr);
Java_com_xfhy_jnifirst_MainActivity_ginsengTest(JNIEnv *env, jclass clazz, jshort s, jint i, jlong l, jfloat f, jdouble d, jchar c,
                                                jboolean z, jbyte b, jstring str, jobject obj, jobject p, jintArray arr) {

}

The method definitions above are one-to-one except for JNIEnv and jclass. Therefore, the following tables can be summarized:

Type comparison tables for Java and JNI

java typeNative typeSymbol PropertiesWord length
booleanjbooleanUnsigned8-bit
bytejbyteSigned8-bit
charjcharUnsigned16-bit
shortjshortSigned16-bit
intjintSigned32-bit
longjlongSigned64-bit
floatjfloatSigned32-bit
doublejdoubleSigned64-bit

Reference Type Reference Table

java typeNative type
java.lang.Classjclass
java.lang.Throwablejthrowable
java.lang.Stringjstring
java.lang.Object[]jobjectArray
Boolean[]jbooleanArray
Byte[]jbyteArray
Char[]jcharArray
Short[]jshortArray
int[]jintArray
long[]jlongArray
float[]jfloatArray
double[]jdoubleArray

3.1 Basic Data Types

The basic data type is actually a redefinition of the basic type in C/C++ with typedef, which can be accessed directly in JNI.

/* Primitive types that match up with Java equivalents. */
typedef uint8_t  jboolean; /* unsigned 8 bits */
typedef int8_t   jbyte;    /* signed 8 bits */
typedef uint16_t jchar;    /* unsigned 16 bits */
typedef int16_t  jshort;   /* signed 16 bits */
typedef int32_t  jint;     /* signed 32 bits */
typedef int64_t  jlong;    /* signed 64 bits */
typedef float    jfloat;   /* 32-bit IEEE 754 */
typedef double   jdouble;  /* 64-bit IEEE 754 */

3.2 Reference data types

If written in C++, all references are derived from jobject.

class _jobject {};
class _jclass : public _jobject {};
class _jstring : public _jobject {};
class _jarray : public _jobject {};
class _jobjectArray : public _jarray {};
class _jbooleanArray : public _jarray {};
class _jbyteArray : public _jarray {};
class _jcharArray : public _jarray {};
class _jshortArray : public _jarray {};
class _jintArray : public _jarray {};
class _jlongArray : public _jarray {};
class _jfloatArray : public _jarray {};
class _jdoubleArray : public _jarray {};
class _jthrowable : public _jobject {};

When JNI uses C, all reference types use jobject.

4. JNI string processing

4.1 native Operations JVM Data Structure

JNI passes all objects in Java as a C pointer to the local method, which points to the JVM's internal data structure, which is stored in memory invisible. Only the appropriate JNI function can be selected from the function table pointed to by the JNIEnv pointer to manipulate the data structure in the JVM.

native accesses java, for example. The corresponding JNI type jstring of lang.String cannot be used like accessing the basic data type because it is a Java reference type, so the contents of the string can only be accessed in local code through JNI functions like GetStringUTFChars.

4.2 String Operation

Let's start with an example:

//call
String result = operateString("String to be operated on");
Log.d("xfhy", result);

//Definition
public native String operateString(String str);
extern "C"
JNIEXPORT jstring JNICALL
Java_com_xfhy_jnifirst_MainActivity_operateString(JNIEnv *env, jobject thiz, jstring str) {
    //Copy strings from java's memory for native use
    const char *strFromJava = (char *) env->GetStringUTFChars(str, NULL);
    if (strFromJava == NULL) {
        //Must be empty check
        return NULL;
    }

    //Copy strFromJava into buff to get the string generated later
    char buff[128] = {0};
    strcpy(buff, strFromJava);
    strcat(buff, " Add something after the string");

    //Release Resources
    env->ReleaseStringUTFChars(str, strFromJava);

    //Automatically convert to Unicode
    return env->NewStringUTF(buff);
}

Output is: the string to be operated on is followed by something

Getting the JVM string in 4.2.1 native

The operateString function receives a parameter STR of type jstring, which is a string that points to the inside of the JVM and cannot be used directly. You first need to convert jstring to C-style string type char*, before you can use it, you must use the appropriate JNI function to access the string data structure inside the JVM. (GetStringUTFChars is used in the above example)

GetStringUTFChars(jstring string, jboolean* isCopy) parameter description:

  • String: jstring, string pointer Java passes to native code
  • isCopy: Normally NULL is passed. Value is JNI_TRUE and JNI_FALSE. If it is JNI_TRUE returns a copy of the source string inside the JVM and allocates memory space for the newly generated string. If it is JNI_FALSE returns a pointer to the source string inside the JVM, meaning that the source string can be modified at the native level, but modifications are not recommended, and the principles of Java strings cannot be modified.

Unicode encoding is the default in Java, and UTF encoding is the default in C/C++, so encoding conversion is required when string communication between native and Java layers. GetStringUTFChars just converts the string of the jstring pointer (pointing to the Unicode character sequence inside the JVM) into a UTF-8 format C string.

4.2.2 Exception handling

When using GetStringUTFChars, the returned value may be NULL, which needs to be handled, otherwise you will have problems using this string if you continue down. Because this method is called with a copy, the JVM allocates memory space for the newly generated string, which can cause the call to fail if memory space is not allocated enough. If the call fails, it returns NULL and throws OutOfMemoryError. If JNI encounters an outstanding exception, it will not change the flow of the program or continue to move forward.

4.2.3 Release string resources

Unlike Java, natives need to manually free up the requested memory space. When GetStringUTFChars is called, a new space is requested to load the copied string, which is used to facilitate native code access and modification, and so on. Since there is a memory allocation, you must release it manually by ReleaseStringUTFChars. You can see that they are one-to-one, paired with GetStringUTFChars.

4.2.4 Build String

Using the NewStringUTF function, a jstring can be constructed, requiring a C string of type char *. It will build a new java. The lang.String string object is automatically converted to Unicode encoding. If the JVM cannot construct Java for you. If lang.String allocates enough memory, an OutOfMemoryError exception is thrown and NULL is returned.

4.2.5 Other string manipulation functions

  1. GetStringChars and ReleaseStringChars: This is similar to the Get/ReleaseStringUTFChars function in that strings used to get and release are encoded in Unicode format.
  2. GetStringLength: Gets the length of the Unicode string. UTF-8 encoded strings end with\0, while Unicode does not, so it needs to be separated here.
  3. GetStringUTFLength: Gets the length of the UTF-8 encoding string, which is the length of the C/C++ default encoding string. You can also use the standard C function strlen to get its length.
  4. Strcat: Split string, standard C function. Eg: strcat (buff,'xfhy'); Add xfhy to the end of buff.
  5. GetStringCritical and ReleaseStringCritical: To increase the possibility of returning a pointer directly to a Java string (instead of a copy). In the area between these two functions, it is absolutely impossible to call other JNI functions or native functions that block threads. Otherwise, the JVM may be deadlocked. If one of the strings has a very large content, such as 1M, and only needs to read the contents to print out, it is more appropriate to use this pair of functions to return a pointer to the source string directly.
  6. GetStringRegion and GetStringUTFRegion: Gets the contents of the specified range in Unicode and UTF-8 strings (eg: only strings at 1-3 indexes are required), which copies the source string to a pre-allocated buffer (self-defined char array).
extern "C"
JNIEXPORT jstring JNICALL
Java_com_xfhy_jnifirst_MainActivity_operateString(JNIEnv *env, jobject thiz, jstring str) {
    //Mode 2 uses the GetStringUTFRegion method to copy strings from the JVM into the C/C++ buffer (array)
    //Get Unicode String Length
    int len = env->GetStringLength(str);
    char buff[128];
    env->GetStringUTFRegion(str, 0, len, buff);
    LOGI("-------------- %s", buff);

    //Automatically convert to Unicode
    return env->NewStringUTF(buff);
}

GetStringUTFRegion checks for crossings and throws a StringIndexOutOfBoundsException exception. GetStringUTFRegion is actually a bit similar to GetStringUTFChars, but GetStringUTFRegion does not allocate memory internally and does not throw out memory overflow exceptions. There is no function like Release to free up resources because no memory is allocated internally.

4.2.6 String Summary

  1. Java string to C/C++ string: Using the GetStringUTFChars function, you must call ReleaseStringUTFChars to free memory
  2. Unicode string needed to create Java layer, using NewStringUTF function
  3. Get C/C++ string length using GetStringUTFLength or strlen function
  4. For small strings, two functions, GetStringRegion and GetStringUTFRegion, are the best choices because the buffer array can be extracted and allocated by the compiler without generating memory overflow exceptions. It's also good when you only need to work with a portion of the string's data. They provide start index and substring length values, and copy consumption is minimal
  5. Get Unicode strings and lengths, using the GetStringChars and GetStringLength functions

5. Array operations

5.1 Array of Basic Types

An array of basic types is an array of basic data types in JNI that can be accessed directly. Here's a simple example, the sum of int s:

//MainActivity.java
public native int sumArray(int[] array);
extern "C"
JNIEXPORT jint JNICALL
Java_com_xfhy_jnifirst_MainActivity_sumArray(JNIEnv *env, jobject thiz, jintArray array) {
    //Sum of Arrays
    int result = 0;

    //Mode 1 Recommended
    jint arr_len = env->GetArrayLength(array);
    //Dynamic Application Array
    jint *c_array = (jint *) malloc(arr_len * sizeof(jint));
    //Initialize array element content to 0
    memset(c_array, 0, sizeof(jint) * arr_len);
    //Copy the elements in the [0-arr_len] position of the java array into the c_array array
    env->GetIntArrayRegion(array, 0, arr_len, c_array);
    for (int i = 0; i < arr_len; ++i) {
        result += c_array[i];
    }
    //Memory for dynamic requests must be freed
    free(c_array);

    return result;
}

After getting the jintArray, layer C first needs to get its length, then dynamically request an array (because the length of the array passed in by the Java layer is not the same, so it needs to dynamically request a layer C array), whose elements are of type jint. malloc is a commonly used function that requests a contiguous block of memory after which free is manually called. Then call the GetIntArrayRegion function to copy the Java layer array into the C layer array, and sum it, which still looks so easy.

Here's another way to sum:

extern "C"
JNIEXPORT jint JNICALL
Java_com_xfhy_jnifirst_MainActivity_sumArray(JNIEnv *env, jobject thiz, jintArray array) {
    //Sum of Arrays
    int result = 0;

    //Mode 2  
    //This is a dangerous way to get IntArrayElements to get a pointer directly to an array element that can be modified directly.
    jint *c_arr = env->GetIntArrayElements(array, NULL);
    if (c_arr == NULL) {
        return 0;
    }
    c_arr[0] = 15;
    jint len = env->GetArrayLength(array);
    for (int i = 0; i < len; ++i) {
        //result += *(c_arr + i); Write in this form, or in the next line
        result += c_arr[i];
    }
    //With Get, there is usually a Release
    env->ReleaseIntArrayElements(array, c_arr, 0);

    return result;
}

GetIntArrayElements function to get the original array element pointer, direct operation, you can get the element sum. It looks a lot simpler, but I personally think it's a bit dangerous because it allows you to modify the source array directly in layer C. The second parameter of GetIntArrayElements generally passes NULL and JNI_TRUE is a pointer back to the temporary buffer array (that is, a copy of a copy), passing JNI_FALSE returns the original array pointer.

Here's a quick summary: The Get/SetArrayRegion function is the most efficient way to manipulate array elements. Apply for arrays on the fly and operate on your own without affecting the Java layer.

5.2 Array of Objects

Elements in an object array are instances of a class or references to other arrays and do not have direct access to the arrays Java passes to the JNI layer.

Operating object arrays are slightly more complex. For example, create a two-dimensional array at the native layer, assign values, and return it to the Java layer for use. (ps: The second dimension is int[], which belongs to the object)

public native int[][] init2DArray(int size);

//Give native Layer Creation - >Java Print Output
int[][] init2DArray = init2DArray(3);
for (int i = 0; i < 3; i++) {
    for (int i1 = 0; i1 < 3; i1++) {
        Log.d("xfhy", "init2DArray[" + i + "][" + i1 + "]" + " = " + init2DArray[i][i1]);
    }
}

extern "C"
JNIEXPORT jobjectArray JNICALL
Java_com_xfhy_jnifirst_MainActivity_init2DArray(JNIEnv *env, jobject thiz, jint size) {
    //Create a two-dimensional array of size*size

    //jobjectArray is a Java array used to hold an array of objects, which is an object int[]
    jclass classIntArray = env->FindClass("[I");
    if (classIntArray == NULL) {
        return NULL;
    }
    //Create an array object with an element of classIntArray
    jobjectArray result = env->NewObjectArray(size, classIntArray, NULL);
    if (result == NULL) {
        return NULL;
    }
    for (int i = 0; i < size; ++i) {
        jint buff[100];
        //Creating a second-dimensional array is an element of the first-dimensional array
        jintArray intArr = env->NewIntArray(size);
        if (intArr == NULL) {
            return NULL;
        }
        for (int j = 0; j < size; ++j) {
            //Set a value here
            buff[j] = 666;
        }
        //Set up data for a jintArray
        env->SetIntArrayRegion(intArr, 0, size, buff);
        //Set data index i I for a jobjectArray, data bit intArr
        env->SetObjectArrayElement(result, i, intArr);
        //Remove references in a timely manner
        env->DeleteLocalRef(intArr);
    }

    return result;
}

It's more complex, let's analyze it

  1. The first is to use the FindClass function to find the class of the java layer int[] object that needs to be passed in NewObjectArray to create an array of objects. After calling the NewObjectArray function, you can create an array of objects with a size and an element type of the class you obtained earlier.
  2. Enter the for loop to build size int arrays, which require the NewIntArray function. You can see that I've built a temporary buff array, and then the size is set arbitrarily. For example, you can use malloc to dynamically request space so that you don't have to request 100 spaces, which may be too large or too small. Integer buff arrays are mainly used to assign values to the generated jintArray, because jintArray is a Java data structure, we native s can not directly operate on it. We have to call the SetIntArrayRegion function to copy the values of the buff array to the jintArray array.
  3. The SetObjectArrayElement function is then called to set the data at an index in the jobjectArray array, where the generated jintArray is set.
  4. Finally, you need to remove the references from the jintArray generated in for in time. The jintArray created is a JNI local reference, which can overflow the JNI reference table if there are too many local references.

ps: In JNI, whenever a subclass of jobject belongs to a reference variable, it takes up space in the reference table. The basic data types, jint,jfloat,jboolean, and so on, do not occupy reference table space and do not need to be released.

6. native Tuning Java Method

You have already described how to call native methods in Java. Here you will see how native calls Java methods.

ps: In a JVM, when you run a Java program, all the relevant class files that need to be used at runtime are loaded into the JVM first, and then loaded on demand to improve performance and save memory. Before we invoke a static method of a class, the JVM will determine if the class is loaded or, if it is not loaded into the JVM by ClassLoader, it will look for the class in the classpath path path. If found, load the class, and if not found, report a ClassNotFoundException exception.

6.1 native calls Java static methods

Various NULL judgments have been removed for the sake of concise demonstration code and easy clarification of core content.

Let me start with a MyJNIClass.java class

public class MyJNIClass {

    public int age = 18;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public static String getDes(String text) {
        if (text == null) {
            text = "";
        }
        return "The length of the string passed in is :" + text.length() + "  Content is : " + text;
    }

}

Then de-native ly calls the getDes method, which is not only included but also returned for complexities.

extern "C"
JNIEXPORT void JNICALL
Java_com_xfhy_allinone_jni_CallMethodActivity_callJavaStaticMethod(JNIEnv *env, jobject thiz) {
    //Call the static method of a class

    //When a class is used by the JVM, it needs to be loaded before it can be used
    //1. Search the MyJNIClass class from the classpath path path and return its Class object
    jclass clazz = env->FindClass("com/xfhy/allinone/jni/MyJNIClass");
    //2. Find the getDes method from the clazz class to get the method id of this static method
    jmethodID mid_get_des = env->GetStaticMethodID(clazz, "getDes", "(Ljava/lang/String;)Ljava/lang/String;");
    //3. Build the input parameter, call the static method, and get the return value
    jstring str_arg = env->NewStringUTF("I am xfhy");
    jstring result = (jstring) env->CallStaticObjectMethod(clazz, mid_get_des, str_arg);
    const char *result_str = env->GetStringUTFChars(result, NULL);
    LOGI("Get Java Layer returned data : %s", result_str);

    //4. Remove Local References
    env->DeleteLocalRef(clazz);
    env->DeleteLocalRef(str_arg);
    env->DeleteLocalRef(result);
}

It looks like it's simpler. native's code is really short and tough, and there's a lot of knowledge involved.

  1. First, call the FindClass function, pass in the Class descriptor (the full class name of the Java class, where MyJNIClass is entered in AS with a prompt to complete, and enter to complete), find the class, and get the jclass type.
  2. The method id is then found by GetStaticMethodID, the method signature is passed in, and a reference of type jmethodID (a reference to a stored method) is obtained. (When you type getDes here, AS also has a completion function. It's really convenient to press enter to bring the signature out directly.) The method signature is first brought out using the auto-completion function of AS, and then what is the method signature in detail.
  3. Build the input parameter, then call CallStaticObjectMethod to call the static method inside the Java class, and pass in the parameter, which returns the data directly from the Java layer. In fact, CallStaticObjectMethod here is a static method of invoked reference type, similar to CallStaticVoidMethod, CallStaticIntMethod, CallStaticFloatMethod, CallStaticShortMethod. Their usage is the same.
  4. Remove Local References

ps: At the end of the function, the JVM automatically frees up the memory space occupied by all locally referenced variables. It's still safe to release it manually because a reference table is maintained in the JVM to store local and global reference variables. Testing found that on low Android versions (I tested Android 4.1), the maximum storage space for this table was 512 references, which crashed when exceeded. When I'm on a high version, like Millet 8 (Android 10), this number of references can reach 100,000 and won't crash, just a little bit. Maybe the hardware is better than it was then, and the default value has changed.

Here are the errors reported when the reference table overflowed (it's hard to get this one by looking for an older device):

E/dalvikvm: JNI ERROR (app bug): local reference table overflow (max=512)
E/dalvikvm: Failed adding to JNI local ref table (has 512 entries)
E/dalvikvm: VM aborting
A/libc: Fatal signal 11 (SIGSEGV) at 0xdeadd00d (code=1), thread 2561 (m.xfhy.allinone)

Here's the native code I brought to restore the error. It happens to be 513 here, but no less than 513. It shows that some devices in the early years did use 512 as the maximum. This is certainly something to watch out for when our APP needs to be compatible with older devices (generally, haha).

extern "C"
JNIEXPORT jobject JNICALL
Java_com_xfhy_allinone_jni_CallMethodActivity_testMaxQuote(JNIEnv *env, jobject thiz) {
    //Test maximum number of Android virtual machine reference tables

    jclass clazz = env->FindClass("java/util/ArrayList");
    jmethodID constrId = env->GetMethodID(clazz, "<init>", "(I)V");
    jmethodID addId = env->GetMethodID(clazz, "add", "(ILjava/lang/Object;)V");
    jobject arrayList = env->NewObject(clazz, constrId, 513);
    for (int i = 0; i < 513; ++i) {
        jstring test_str = env->NewStringUTF("test");
        env->CallVoidMethod(arrayList, addId, 0, test_str);
        //Local references that should be deleted here
        //env->DeleteLocalRef(test_str);
    }

    return arrayList;
}

Supplement:

Later I was in a Article Found in,

  • Local reference overflow behaves differently in different versions of Android. The upper limit of local reference tables before Android 8.0 was 512 references, and after Android 8.0 the upper limit of local reference tables was raised to 8388608 references. (I don't have an actual test here, but it basically meets the test above).
  • Oracle Java does not have an upper limit on local reference tables, and as local reference tables grow, they eventually become OOM.

6.2 native calls Java instance methods

The following demonstrates how to create a Java instance at the native layer and call it, roughly the same as calling a static method above.

extern "C"
JNIEXPORT void JNICALL
Java_com_xfhy_allinone_jni_CallMethodActivity_createAndCallJavaInstanceMethod(JNIEnv *env, jobject thiz) {
    //Build an instance of a class and call its methods

    //Special Note: Normally, every acquisition here needs to be void handled, and I will not void it here to show the core code
    //AS will prompt you after entering MyJNIClass here and press Enter to complete it automatically
    jclass clazz = env->FindClass("com/xfhy/allinone/jni/MyJNIClass");
    //Get the method id of the construction method
    jmethodID mid_construct = env->GetMethodID(clazz, "<init>", "()V");
    //Gets the method id of the getAge method
    jmethodID mid_get_age = env->GetMethodID(clazz, "getAge", "()I");
    jmethodID mid_set_age = env->GetMethodID(clazz, "setAge", "(I)V");
    jobject jobj = env->NewObject(clazz, mid_construct);

    //Call method setAge
    env->CallVoidMethod(jobj, mid_set_age, 20);
    //Re-call method getAge to get return value Print Output
    jint age = env->CallIntMethod(jobj, mid_get_age);
    LOGI("Get age = %d", age);

    //Any use that is a subclass of jobject requires removing references
    env->DeleteLocalRef(clazz);
    env->DeleteLocalRef(jobj);
}

Use the MyJNIClass class above here as well

  1. Find the class of this class
  2. Get the id of the construction method, get the id of the method you need to call. When you get the construction method, the method name is fixed in <init>, followed by the method signature.
  3. Constructing a Java object using the NewObject() function
  4. Call the setAge and getAge methods of the Java object to get the return value and print the result. CallIntMethod and CallVoidMethod are called here, so you can guess what it is by name. Is to call a method whose return values are ints and void s. Similar to CallStaticIntMethod above.
  5. Delete local references!!!

6.3 Method Signature

Calling a Java method requires a jmethodID, and each time a jmethodID is obtained, a method signature is passed in. Clearly the method name has already been passed in, so I feel I can adjust this method. Why do I need to pass in a method signature? Because Java methods are overloaded, it is possible that the method names are the same, but the parameters are different. So you have to pass in a method signature here to make a difference.

The method signature format is: (list of parameter type) return value, reference type starts with L, followed by full path name of class.

Definition relationships in Java basic types and method signatures:

JavaField Descriptor
booleanZ
byteB
charC
shortS
intI
longJ
floatF
doubleD
int[][I

An example is given below:

public String[] testString(boolean a, byte b, float f, double d, int i, int[] array)

env->GetMethodID(clazz,"testString", "(ZBFDI[I)[Ljava/lang/String;")

6.4 Summary of native calls to Java methods

  1. Get a Class instance of a class and use the FindClass function to pass in the class descriptor. The JVM looks for this class from the classpath directory.
  2. Creating an instance of a class uses the NewObject method, passing in the class reference and the id of the construction method.
  3. Local references (as long as a subclass of jobject is a reference variable and takes up space in the reference table) must be removed in time or the reference table will overflow. It's easy to overflow on low versions (low version cap 512).
  4. Method Signature Format: (List of Parameter Types) Return Value
  5. When using FindClass and GetMethodID, the decision must be null. Fundamentals.
  6. Get the instance method ID, using the GetMethodID function; Get the static method ID, using the GetStaticMethodID function; Get the construction method ID using <init>
  7. Call instance method uses CallXXMethod function, XX means return data type. eg:CallIntMethod(); Call static method using CallStaticXXMethod function.

7. NDK Crash Error Location

When NDK makes a fatal error, it causes APP to flip. Such errors are very difficult to check for, such as memory address access errors, using wild pointers, memory leaks, stack overflows, number divides by zero and other native errors can cause APP to crash.

Although these NDK errors are not easy to troubleshoot, we get the stack log of the logcat output after the NDK errors occur. Combined with the following two debugging tools, addr2line and ndk-stack, we can pinpoint the number of lines of code where the errors occurred and quickly find the problem.

First, go to the NDK directory and go to the sdk/ndk/21.0.6113669/toolchains/directory. My local directory is as follows. You can see the directory structure of the NDK cross-compiler tool chain.

aarch64-linux-android-4.9
arm-linux-androideabi-4.9
llvm
renderscript
x86-4.9
x86_64-4.9

Where ndk-stack is placed at $NDK_ In the HOME directory, the same directory as ndk-build. Addr2line is located in the NDK cross-compiler tool chain directory. NDK implements multiple tools for different CPU architectures. When using the addr2line tool, you need to select based on the current mobile CPU architecture. My mobile phone is aarch64, use the tools in the aarch64-linux-android-4.9 directory. Command to view CPU information for mobile phone: ADB shell cat/proc/cpuinfo

Before introducing the two debugging tools, we need to write native code that crashes to see the effect. I'm at demo's native-lib. The following code is written in cpp:

void willCrash() {
    JNIEnv *env = NULL;
    int version = env->GetVersion();
}

extern "C"
JNIEXPORT void JNICALL
Java_com_xfhy_allinone_jni_CallMethodActivity_nativeCrashTest(JNIEnv *env, jobject thiz) {
    LOGI("Before the crash");
    willCrash();
    //Later code could not be executed because it crashed
    LOGI("After the crash");
    printf("oooo");
}

This code is an obvious null pointer error. I wanted to make a divide-by-0 error, but divide-by-0 won't crash. I don't know what (has evolved?). Then run with the error log as follows:

2020-06-07 17:05:25.230 12340-12340/? A/DEBUG: *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
2020-06-07 17:05:25.230 12340-12340/? A/DEBUG: Build fingerprint: 'Xiaomi/dipper/dipper:10/QKQ1.190828.002/V11.0.8.0.QEACNXM:user/release-keys'
2020-06-07 17:05:25.230 12340-12340/? A/DEBUG: Revision: '0'
2020-06-07 17:05:25.230 12340-12340/? A/DEBUG: ABI: 'arm64'
2020-06-07 17:05:25.237 12340-12340/? A/DEBUG: Timestamp: 2020-06-07 17:05:25+0800
2020-06-07 17:05:25.237 12340-12340/? A/DEBUG: pid: 11527, tid: 11527, name: m.xfhy.allinone  >>> com.xfhy.allinone <<<
2020-06-07 17:05:25.237 12340-12340/? A/DEBUG: uid: 10319
2020-06-07 17:05:25.237 12340-12340/? A/DEBUG: signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
2020-06-07 17:05:25.237 12340-12340/? A/DEBUG: Cause: null pointer dereference
2020-06-07 17:05:25.237 12340-12340/? A/DEBUG:     x0  0000000000000000  x1  0000007fd29ffd40  x2  0000000000000005  x3  0000000000000003
2020-06-07 17:05:25.237 12340-12340/? A/DEBUG:     x4  0000000000000000  x5  8080800000000000  x6  fefeff6fb0ce1f1f  x7  7f7f7f7fffff7f7f
2020-06-07 17:05:25.237 12340-12340/? A/DEBUG:     x8  0000000000000000  x9  a95a4ec0adb574df  x10 0000007fd29ffee0  x11 000000000000000a
2020-06-07 17:05:25.237 12340-12340/? A/DEBUG:     x12 0000000000000018  x13 ffffffffffffffff  x14 0000000000000004  x15 ffffffffffffffff
2020-06-07 17:05:25.237 12340-12340/? A/DEBUG:     x16 0000006fc6476c50  x17 0000006fc64513cc  x18 00000070b21f6000  x19 000000702d069c00
2020-06-07 17:05:25.237 12340-12340/? A/DEBUG:     x20 0000000000000000  x21 000000702d069c00  x22 0000007fd2a00720  x23 0000006fc6ceb127
2020-06-07 17:05:25.237 12340-12340/? A/DEBUG:     x24 0000000000000004  x25 00000070b1cf2020  x26 000000702d069cb0  x27 0000000000000001
2020-06-07 17:05:25.237 12340-12340/? A/DEBUG:     x28 0000007fd2a004b0  x29 0000007fd2a00420
2020-06-07 17:05:25.237 12340-12340/? A/DEBUG:     sp  0000007fd2a00410  lr  0000006fc64513bc  pc  0000006fc64513e0
2020-06-07 17:05:25.788 12340-12340/? A/DEBUG: backtrace:
2020-06-07 17:05:25.788 12340-12340/? A/DEBUG:       #00 pc 00000000000113e0  /data/app/com.xfhy.allinone-4VScOmUWz8wLqqwBWZCP2w==/lib/arm64/libnative-lib.so (_JNIEnv::GetVersion()+20) (BuildId: b1130c28a8b45feda869397e55c5b6d754410c8d)
2020-06-07 17:05:25.788 12340-12340/? A/DEBUG:       #01 pc 00000000000113b8  /data/app/com.xfhy.allinone-4VScOmUWz8wLqqwBWZCP2w==/lib/arm64/libnative-lib.so (willCrash()+24) (BuildId: b1130c28a8b45feda869397e55c5b6d754410c8d)
2020-06-07 17:05:25.788 12340-12340/? A/DEBUG:       #02 pc 0000000000011450  /data/app/com.xfhy.allinone-4VScOmUWz8wLqqwBWZCP2w==/lib/arm64/libnative-lib.so (Java_com_xfhy_allinone_jni_CallMethodActivity_nativeCrashTest+84) (BuildId: b1130c28a8b45feda869397e55c5b6d754410c8d)
2020-06-07 17:05:25.788 12340-12340/? A/DEBUG:       #03 pc 000000000013f350  /apex/com.android.runtime/lib64/libart.so (art_quick_generic_jni_trampoline+144) (BuildId: 2bc2e11d57f839316bf2a42bbfdf943a)
2020-06-07 17:05:25.788 12340-12340/? A/DEBUG:       #04 pc 0000000000136334  /apex/com.android.runtime/lib64/libart.so (art_quick_invoke_stub+548) (BuildId: 2bc2e11d57f839316bf2a42bbfdf943a)

When we see this error, we first find the key information Cause: null pointer dereference, but we don't know where it happened, we just know it.

7.1 addr2line

With the error log, we now use the tool addr2line to locate the location.

Command/Users/xfhy/development/sdk/ndk/21.0.6113669/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_needs to be executed 64/bin/aarch64-linux-android-addr2line-e/Users/xfhy/development/AllInOne/app/libnative-lib. So 00000000000113e0 00000000000113b8, where -e is the location of the specified so file, then 00000000113e0 at the end and 00000000000113b8 is the assembly instruction address of the error location.

After execution, the results are as follows:

/Users/xfhy/development/sdk/ndk/21.0.6113669/toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/include/jni.h:497
/Users/xfhy/development/AllInOne/app/src/main/cpp/native-lib.cpp:260

You can see native-lib. Problems with 260 rows of cpp. We just need to find the location and fix the bug. Perfect, instantly found.

7.2 ndk-stack

A simpler way is to enter the command adb logcat | ndk-stack -sym /Users/xfhy/development/AllInOne/app/build/intermediates/cmake/debug/obj/arm64-v8a directly, ending with the location of the so file. After executing the command and generating a native error on your phone, you can locate this error point in this so file as follows:

********** Crash dump: **********
Build fingerprint: 'Xiaomi/dipper/dipper:10/QKQ1.190828.002/V11.0.8.0.QEACNXM:user/release-keys'
#00 0x00000000000113e0 /data/app/com.xfhy.allinone-oVu0tjta-aW9LYa08eoK1Q==/lib/arm64/libnative-lib.so (_JNIEnv::GetVersion()+20) (BuildId: b1130c28a8b45feda869397e55c5b6d754410c8d)
                                                                                                        _JNIEnv::GetVersion()
                                                                                                        /Users/xfhy/development/sdk/ndk/21.0.6113669/toolchains/llvm/prebuilt/darwin-x86_64/sysroot/usr/include/jni.h:497:14
#01 0x00000000000113b8 /data/app/com.xfhy.allinone-oVu0tjta-aW9LYa08eoK1Q==/lib/arm64/libnative-lib.so (willCrash()+24) (BuildId: b1130c28a8b45feda869397e55c5b6d754410c8d)
                                                                                                        willCrash()
                                                                                                        /Users/xfhy/development/AllInOne/app/src/main/cpp/native-lib.cpp:260:24
#02 0x0000000000011450 /data/app/com.xfhy.allinone-oVu0tjta-aW9LYa08eoK1Q==/lib/arm64/libnative-lib.so (Java_com_xfhy_allinone_jni_CallMethodActivity_nativeCrashTest+84) (BuildId: b1130c28a8b45feda869397e55c5b6d754410c8d)
                                                                                                        Java_com_xfhy_allinone_jni_CallMethodActivity_nativeCrashTest
                                                                                                        /Users/xfhy/development/AllInOne/app/src/main/cpp/native-lib.cpp:267:5

You can see that there was an error with the willCrash() method, which has 260 lines of code (even columns printed out, column 24).

8. JNI References

Java normally does not need to care how the JVM requests memory when it creates new objects or how it releases memory after it has been used. C++ differs, requiring us to manually request and free memory (new->delete, malloc->free). When using JNI, because local code cannot manipulate data structures within the JVM directly by reference, these operations must indirectly manipulate data content within the JVM by calling the corresponding JNI interface. We don't need to worry about how objects are stored in the JVM. We just need to learn about three different references in the JVI.

8.1 JNI Local References

References created in local functions by NewLocalRef or by calling FindClass, NewObject, GetObjectClass, NewCharArray, etc., are local references.

  • Prevents GC from recycling referenced objects
  • Cannot be used across threads
  • Not used across functions in local functions
  • Release: Objects referenced by local references are automatically released by the JVM after the function returns, or DeleteLocalRef can be called to release

Usually created and used in functions. As mentioned above: Local references are automatically released after the function returns, so why should we leave it alone (manually calling DeleteLocalRef to release)?

For example, if a for loop is opened in which local references are constantly created, you must manually free memory using DeleteLocalRef. Otherwise, more and more local references will cause crashes (the maximum number of local reference tables on low Android versions is limited to 512, or more will crash).

In another case, after a local method returns a reference to the Java layer, if the Java layer does not use the returned local reference, the local reference will be automatically released by the JVM.

8.2 JNI Global References

Global references are created based on local references and call the NewGlobalRef method.

  • Prevents GC from recycling referenced objects
  • Can be used across methods and threads
  • JVM will not be released automatically, you need to call DeleteGlobalRef to release manually

8.3 JNI Weak Global Reference

Weak global references are created based on local or global references and call the NewWeakGlobalRef method.

  • Will not prevent GC from recycling referenced objects
  • Can be used across methods and threads
  • References are not automatically freed and are only freed for recycling when the JVM is out of memory. Or you can call DeleteWeakGlobalRef to release manually.

Reference resources

  • Android Developers NDK Guide C++ Library Support https://developer.android.com/ndk/guides/cpp-support.html
  • JNI/NDK Development Guide https://blog.csdn.net/xyang81/column/info/blogjnindk
  • JNI local reference table overflow for android memory leak (max=512) https://www.cnblogs.com/lzl-sml/p/3520052.html
  • Android JNI local reference table overflow (max=512)) https://blog.csdn.net/lylwo317/article/details/103105413
  • Introduction to Android JNI (7) - Reference Management https://juejin.im/post/5e04b2236fb9a0162c487ef8#heading-2
  • Local Reference, Global Reference and Weak Global Reference for JNI Development (3) https://zhuanlan.zhihu.com/p/93115458

Keywords: C Android JNI NDK

Added by jaydeesmalls on Sat, 15 Jan 2022 07:19:53 +0200