[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:
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
- Write a java class that declares the native method
- Write native code
- Compile native code into so file
- 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:
- 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.
- jobject: Represents this in a Java object. If static, use jclass
- 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 type | Native type | Symbol Properties | Word length |
---|---|---|---|
boolean | jboolean | Unsigned | 8-bit |
byte | jbyte | Signed | 8-bit |
char | jchar | Unsigned | 16-bit |
short | jshort | Signed | 16-bit |
int | jint | Signed | 32-bit |
long | jlong | Signed | 64-bit |
float | jfloat | Signed | 32-bit |
double | jdouble | Signed | 64-bit |
Reference Type Reference Table
java type | Native type |
---|---|
java.lang.Class | jclass |
java.lang.Throwable | jthrowable |
java.lang.String | jstring |
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
- GetStringChars and ReleaseStringChars: This is similar to the Get/ReleaseStringUTFChars function in that strings used to get and release are encoded in Unicode format.
- 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.
- 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.
- Strcat: Split string, standard C function. Eg: strcat (buff,'xfhy'); Add xfhy to the end of buff.
- 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.
- 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
- Java string to C/C++ string: Using the GetStringUTFChars function, you must call ReleaseStringUTFChars to free memory
- Unicode string needed to create Java layer, using NewStringUTF function
- Get C/C++ string length using GetStringUTFLength or strlen function
- 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
- 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
- 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.
- 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.
- The SetObjectArrayElement function is then called to set the data at an index in the jobjectArray array, where the generated jintArray is set.
- 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.
- 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.
- 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.
- 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.
- 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
- Find the class of this class
- 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.
- Constructing a Java object using the NewObject() function
- 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.
- 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:
Java | Field Descriptor |
---|---|
boolean | Z |
byte | B |
char | C |
short | S |
int | I |
long | J |
float | F |
double | D |
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
- 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.
- Creating an instance of a class uses the NewObject method, passing in the class reference and the id of the construction method.
- 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).
- Method Signature Format: (List of Parameter Types) Return Value
- When using FindClass and GetMethodID, the decision must be null. Fundamentals.
- 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>
- 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