JNI profile

Java Native Interface (JNI) is a Java Native Interface. Java is one of the many development techniques for Java, intended to use native code to provide more efficient and flexible extensions to Java programs. Although Java is known for being cross-platform, the true cross-platform has to be C/C++, since 90% of the world’s systems are currently written in C/C++. At the same time, Java is cross-platform at the expense of efficiency for multiple platform compatibility, so JNI is one of the mainstream implementation of this cross-platform.

In short, JNI is a technology for Java to communicate with C/C++. First, let’s review the Android architecture diagram.

! [

JNI profile

Java Native Interface (JNI) is a Java Native Interface. Java is one of the many development techniques for Java, intended to use native code to provide more efficient and flexible extensions to Java programs. Although Java is known for being cross-platform, the true cross-platform has to be C/C++, since 90% of the world’s systems are currently written in C/C++. At the same time, Java is cross-platform at the expense of efficiency for multiple platform compatibility, so JNI is one of the mainstream implementation of this cross-platform.

In short, JNI is a technology for Java to communicate with C/C++. First, let’s review the Android architecture diagram.

Let’s talk briefly about what each layer does.

Linux layer

The Linux kernel

Since the Android system is built on the base Linux kernel, Linux is the foundation of the Android system. In fact, Android’s hardware drivers, process management, memory management, and network management are all in this layer.

Hardware abstraction layer

Hardware Abstraction Layer (abbreviated as Hardware Abstraction Layer), which mainly provides the standard display interface for the upper Layer and provides display device Hardware functionality to the higher level Java API framework. HAL contains multiple library modules, each of which implements an interface for a specific type of hardware component, such as a camera or Bluetooth module. When the framework API requires access to device hardware, the Android system loads the corresponding library module for that hardware component.

System runtime and runtime environment layers

Android Runtime

Before Android 5.0 (API 21), the Dalvik virtual machine was used, then replaced by ART. ART is the operating environment of the Android operating system, which executes dex files by running virtual machines. Dex files are bytecode formats designed for Android, which packages and runs dex files, and Android Toolchain can compile Java code into DEX bytecode format, as shown below.

As shown above, Jack is a compilation tool chain that compiles Java source code into DEX bytecode to run on the Android platform.

Native C/C + + library

Many core Android system components and services are written in C and C++. In order to facilitate developers to call these native library functions, the Android Framework provides the corresponding call API. For example, you can access OpenGL ES through the Android framework’s Java OpenGL API to support drawing and manipulating 2D and 3D graphics in your application.

Application framework layer

The most commonly used components and services of Android platform are in this layer, which is a layer that every Android developer must be familiar with and master, and is the foundation of application development.

The Application layer

Android apps, such as email, SMS, calendar, Internet browsing, contacts and other system applications. We can call the system’s App directly like calling the Java API Framework layer.

Let’s take a look at how to write Android JNI and the process required.

NDK

What is the NDK

NDK (Native Development Kit) A software Development Kit based on a Native programming interface that allows you to leverage C and C++ code in Android applications. Programs developed with this tool run directly locally, rather than on a virtual machine.

In Android, the NDK is a collection of tools that extend the Android SDK. The NDK provides a set of tools to help developers quickly develop dynamic libraries in C or C++, and automatically package so and Java applications together as APK. The NDK also integrates a cross-compiler and provides mk files to isolate CPU, platform, ABI, etc. Developers can create SO files by simply modifying mk files (indicating “which files need to be compiled”, “compilation feature requirements”, etc.).

The NDK configuration

Before creating an NDK project, ensure that the NDK environment is available on the local PC. Select [Preferences…] -> [Android SDK] Download the configuration NDK, as shown below.

Then, create a new Native C++ project, as shown below.

Then check the “Include C++ support” option and click “next” to arrive at the Customize C++ support setup page, as shown below.

Then, click the Finish button.

NDK project directory

Open the newly created NDK project, as shown in the following figure.

Let’s take a look at some of the differences between Android’s NDK project and normal Android application project. First, let’s look at the build.gradle configuration.

apply plugin: 'com.android.application' android {compileSdkVersion 30 buildToolsVersion "30.0.2" defaultConfig {applicationId "Com. XZH. The NDK" minSdkVersion 16 targetSdkVersion 30 versionCode 1 versionName testInstrumentationRunner "1.0" "androidx.test.runner.AndroidJUnitRunner" externalNativeBuild { cmake { cppFlags "" } } } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard - rules. Pro externalNativeBuild}} {cmake {path "/ SRC/main/CPP CMakeLists. TXT" version "3.10.2"}}} Dependencies {// omit reference library}Copy the code

Gradle configuration has two more externalNativeBuild configuration items than the normal Android application. ExternalNativeBuild in defaultConfig is the command parameter used to configure Cmake, while externalNativeBuild defines the path to the cmakelists.txt script of Cmake.

TXT file, cmakelists. TXT is a build script for CMake, which is equivalent to android. mk in ndK-build.

# create library add_library(# set library name native-lib # set library mode # SHARED mode compiles the so file, STATIC mode will not compile SHARED code path SRC /main/ CPP /native-lib. CPP Store the library path as a variable, This variable can be used elsewhere to reference the NDK library # set the variable name log here) # associate library target_link_libraries(# associate library native lib # associate native lib and log-lib) ${log-lib} )Copy the code

For more information about CMake, check out the official CMake manual.

The official sample

When the Android NDK project is created by default, Android provides a simple EXAMPLE of JNI interaction that returns a string to the Java layer in the format java_package name_class name_method name. First, let’s take a look at the native-lib.cpp code.

#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL
Java_com_xzh_ndk_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

Copy the code

Then, let’s take a look at the Android mainactivity.java code.

package com.xzh.ndk;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    static {
        System.loadLibrary("native-lib");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TextView tv = findViewById(R.id.sample_text);
        tv.setText(stringFromJNI());
    }

    public native String stringFromJNI();
}

Copy the code

I met the Android JNI

1. JNI development process

  1. Write Java classes, declare native methods;
  2. Write native code;
  3. Compile native code into SO file;
  4. So library is introduced into Java class and native methods are called.

2. Native method naming

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

}

Copy the code

Function naming rules: Java_ class full path _ Method name. The meanings of the parameters are as follows:

  • JNIEnv* is the first argument that defines any native function and represents a pointer to the JNI environment through which to access the interface methods provided by JNI.
  • Jobject represents this in a Java object or jClass if it is a static method.
  • JNIEXPORT and JNICALL: these are macros defined in JNI and can be found in the jni.h header file.

3. Mapping between JNI data types and Java data types

First, we write a native method declaration in Java code, and then use the [Alt + Enter] shortcut key to ask AS to help us create a native method, AS shown below.

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) { }Copy the code

Next, let’s sort out the Java and JNI type comparison table, as shown below.

Java type Native type Whether conform to the Word length
boolean jboolean unsigned 8 bytes
byte jbyte A signed 8 bytes
char jchar unsigned 16 bytes
short jshort A signed 16 bytes
int jint A signed 32 bytes
long jlong A signed 64 bytes
float jfloat A signed 32 bytes
double jdouble A signed 64 bytes

The following table lists the corresponding reference types.

Java type Native type
java.lang.Class jclass
java.lang.Throwable jthrowable
java.lang.String jstring
jjava.lang.Object[] jobjectArray
Byte[] jbyteArray
Char[] jcharArray
Short[] jshortArray
int[] jintArray
long[] jlongArray
float[] jfloatArray
double[] jdoubleArray

3.1 Basic data types

The basic data type Native is essentially a C/C++ basic type with a typedef to redefine a new name, which can be accessed directly in JNI, as shown below.

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 */

Copy the code

3.2 Reference data types

If written in C++, all references are derived from the jobject root class, as shown below.

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 {};

Copy the code

When JNI uses C, all reference types use Jobject.

4, JNI string processing

4.1 Native operating JVM

JNI passes all objects in Java to local methods as a C pointer to the JVM’s internal data structures, which are not visible in memory. You can only select the appropriate JNI function from the list of functions pointed to by the JNIEnv pointer to manipulate data structures in the JVM.

For example, native accessing jString, the JNI type corresponding to Java.lang. String, cannot be used as a basic data type because it is a Java reference type. So the contents of a string can only be accessed in native code through JNI functions like GetStringUTFChars.

4.2 Examples of string operations

// Call String result = operateString(" String to be operated on "); Log.d("xfhy", result); // Define public native String operateString(String STR);Copy the code

Then implement it in C, as follows.

extern "C" JNIEXPORT jstring JNICALL Java_com_xfhy_jnifirst_MainActivity_operateString(JNIEnv *env, jobject thiz, Const char *strFromJava = (char *) env->GetStringUTFChars(STR, NULL); If (strFromJava == NULL) {// return NULL must be checked; Char buff[128] = {0}; char buff[128] = {0}; strcpy(buff, strFromJava); Strcat (buff, "add something to the end of string "); Env ->ReleaseStringUTFChars(strFromJava); Return env->NewStringUTF(buff); }Copy the code
Obtaining JVM strings in 4.2.1 Native

In the above code, the operateString function takes a jString argument STR, which refers to a string inside the JVM and cannot be used directly. First, jString needs to be converted to the C-style string type char* before it can be used, where appropriate JNI functions must be used to access string data structures within the JVM.

GetStringUTFChars(jString String, jBoolean * isCopy) has the following meanings:

  • String: JString, a string pointer that Java passes to native code.
  • IsCopy: NULL is normally passed. The value can be JNI_TRUE or JNI_FALSE. JNI_TRUE returns a copy of the source string within the JVM and allocates memory for the newly generated string. JNI_FALSE returns a pointer to the source string within the JVM, meaning that the source string can be modified at the Native layer, but is not recommended because Java string principles cannot be modified.

Java uses Unicode encoding by default, while C/C++ uses UTF encoding by default. Therefore, encoding conversion is required when the Native layer communicates with the Java layer in strings. GetStringUTFChars converts the string of a JString pointer (pointing to a sequence of Unicode characters inside the JVM) into a UTF-8 C string.

4.2.2 Exception Handling

When using GetStringUTFChars, the return value may be NULL, which needs to be handled, otherwise you will have problems using the string as you proceed. Because this method is copied, the JVM allocates memory for the newly generated string, and the call fails when it runs out of memory. If the call fails, NULL is returned and OutOfMemoryError is thrown. A pending exception encountered by JNI does not change the flow of the application, and it continues down the path.

4.2.3 Releasing String Resources

Unlike Java, Native has to manually free the requested memory space. When GetStringUTFChars is called, it requests a new space to hold the copied string, which is used for native code to access and modify things like that. Since memory is allocated, it must be freed manually, using ReleaseStringUTFChars. You can see that GetStringUTFChars is matched one-to-one.

4.2.4 Constructing a String

The NewStringUTF function builds a JString by passing in a C string of type CHAR *. It builds a new java.lang.String String object and automatically converts it to Unicode encoding. If the JVM cannot allocate enough memory to construct java.lang.String, an OutOfMemoryError is thrown and NULL is returned.

4.2.5 Other String Manipulation functions
  1. GetStringChars and ReleaseStringChars: this similar in functions and the Get/ReleaseStringUTFChars function, used to capture and release of a string in Unicode format of coding.
  2. GetStringLength: Gets the length of a Unicode string (jString). Utf-8 encoded strings end in 0, whereas Unicode strings do not, so you need to separate them here.
  3. GetStringUTFLength: Gets the length of the UTF-8 encoding string, which is the length of the DEFAULT C/C++ encoding string. You can also use the standard C function “strlen” to get its length.
  4. Strcat: concatenated string, standard C function. Such asstrcat(buff, "xfhy");Add XFHY to the end of the buff.
  5. GetStringCritical and ReleaseStringCritical: To increase the possibility of returning a pointer to a Java string directly (rather than copying it). The area between these two functions is a native function that must never call other JNI functions or block threads. Otherwise the JVM may deadlock. If you have a particularly large string, such as 1M, and only need to read it and print it out, use this pair to return a pointer to the source string.
  6. GetStringRegion and GetStringUTFRegion: Gets the contents of Unicode and UTF-8 strings in the specified range (e.g., only strings at indexes 1-3). This pair copies the source string into a pre-allocated buffer (its own char array).

Often, GetStringUTFRegion bounds checking, crossing the line would throw StringIndexOutOfBoundsException anomalies. GetStringUTFRegion is similar to GetStringUTFChars, except that GetStringUTFRegion does not allocate memory internally and does not throw an overflow exception. Since there is no internal memory allocated, there is no function like Release to free resources.

4.2.6 summary
  • Java string to C/C++ string: To use GetStringUTFChars, ReleaseStringUTFChars must be called to free memory.
  • Create Unicode strings required by the Java layer, using the NewStringUTF function.
  • To get the C/C++ string length, use GetStringUTFLength or strlen.
  • For small strings, the GetStringRegion and GetStringUTFRegion functions are the best choices because the buffer array can be extracted and allocated by the compiler without running out of memory. This works fine when you only need to process part of the string. They provide starting index and substring length values, and replication costs are minimal
  • To get Unicode strings and lengths, use the GetStringChars and GetStringLength functions.

An array of operating

5.1 Array of primitive types

An array of primitive types is an array of primitive data types in JNI that can be accessed directly. For example, here is an example of summing an int array.

//MainActivity.java
public native int sumArray(int[] array);

Copy the code
extern "C" JNIEXPORT jint JNICALL Java_com_xfhy_jnifirst_MainActivity_sumArray(JNIEnv *env, jobject thiz, JintArray array) {// Array sum int result = 0; Jint arr_len = env->GetArrayLength(array); Jint *c_array = (jint *) malloc(arr_len * sizeof(jint)); Memset (c_array, 0, sizeof(jint) * arr_len); Env ->GetIntArrayRegion(array, 0, arr_len, For (int I = 0; I < arr_len; ++ I) {result += c_array[I];} return result;Copy the code

The C layer needs to get the length of jintArray first, and then dynamically apply for an array (because the length of the array passed by the Java layer is variable, so we need to dynamically apply for the C layer array). The elements of the array are of type Jint. Malloc is a commonly used function that allocates a contiguous chunk of memory that needs to be freed manually by calling free. We then call the GetIntArrayRegion function to copy the Java layer array into the C layer array and sum it up.

Now, let’s look at another way of summing, as follows.

extern "C" JNIEXPORT jint JNICALL Java_com_xfhy_jnifirst_MainActivity_sumArray(JNIEnv *env, jobject thiz, JintArray array) {// Array sum int result = 0; Jint *c_arr = env->GetIntArrayElements(array, NULL); 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); I could write it this way, or I could write it on the bottom line result += c_arr[I]; Release env->ReleaseIntArrayElements(array, c_arr, 0); return result; }Copy the code

In the above code, we get the pointer to the elements of the original array directly through the GetIntArrayElements function. It seems a lot easier, but I personally feel that this approach is a bit dangerous, since it is not safe to modify the source array directly at the C layer. The second argument to GetIntArrayElements is usually passed NULL. Passing JNI_TRUE returns a pointer to the temporary buffer array (that is, making a copy), and passing JNI_FALSE returns a pointer to the original array.

5.2 Object Array

The elements in the object array are instances of a class or references to other arrays, and cannot directly access the arrays that Java passes to the JNI layer. Manipulating object arrays is a bit more complicated. Here’s an example: Create a two-dimensional array in the Native layer, assign the value and return it to the Java layer for use.

public native int[][] init2DArray(int size); 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]); }}Copy the code
extern "C" JNIEXPORT jobjectArray JNICALL Java_com_xzh_jnifirst_MainActivity_init2DArray(JNIEnv *env, jobject thiz, Jint size) {// create a size*size array //jobjectArray is used to hold an array of objects Java array is an object int[] jclass classIntArray = Env ->FindClass("[I"); if (classIntArray == NULL) {return NULL;} // Create an array object with classIntArray jobjectArray result = env->NewObjectArray(size, classIntArray, NULL); if (result == NULL) {return NULL;} for (int I = 0; I < size; ++ I) {jint buff[100]; // Create the two-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) { Env ->SetIntArrayRegion(intArr, 0, size, IntArr env->SetObjectArrayElement(result, I, SetObjectArrayElement); Env ->DeleteLocalRef(intArr);} return result;}Copy the code

Next, let’s take a look at the code.

  1. First, we use the FindClass function to find the class of the Java layer int[] object, which is passed to NewObjectArray to create an array of objects. After calling the NewObjectArray function, we create an array of objects of size and the element type of the previously obtained class.
  2. Enter the for loop and build the size of the int array. Build the int array using the NewIntArray function. So you can see that I’ve built a temporary array of buffs, and then I’ve set the size arbitrarily, and this is just for example, you can actually use malloc to dynamically apply for space, so you don’t have to apply for 100 Spaces, which might be too big or too small. The entire buff array is mainly used to assign values to the generated jintArray, because jintArray is a Java data structure, our native can not directly operate, we have to call the SetIntArrayRegion function, the buff array value copy into 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 reference to the jintArray generated in for in time. The jintArray created is a JNI local reference, and if there are too many local references, the JNI reference table will overflow.

6. Native Java method

Anyone familiar with the JVM should know that when running a Java program in the JVM, all relevant class files needed at runtime are first loaded into the JVM and loaded on demand to improve performance and save memory. Before we call a static method of a class, the JVM determines whether the class has been loaded, and if it has not been loaded by the ClassLoader into the JVM, it looks up the class in the classpath path. If found, the class is loaded. If not found, ClassNotFoundException is reported.

6.1 Native Invoking Java static methods

First, we write a myJniclass.java class with the following code.

public class MyJNIClass { public int age = 30; 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() +" the content is :" + text; }}Copy the code

Then, the getDes() method is called in Native. For complexity, the getDes() method has both input and return arguments, as shown below.

extern "C" JNIEXPORT void JNICALL Java_com_xzh_allinone_jni_CallMethodActivity_callJavaStaticMethod(JNIEnv *env, Jobject thiz) {// call a class static method //1\. From the classpath directory search MyJNIClass this Class, and returns the Class object of the Class jclass clazz = env - > FindClass (" com/XZH/jni/jni/MyJNIClass "); JmethodID mid_get_des = env->GetStaticMethodID(clazz, "GetStaticMethodID ", "(Ljava/lang/String;) Ljava/lang/String;" ); Jstring str_arg = env->NewStringUTF(" I am XZH "); jstring result = (jstring) env->CallStaticObjectMethod(clazz, mid_get_des, str_arg); const char *result_str = env->GetStringUTFChars(result, NULL); LOGI(" get data returned by Java layer: %s", result_str); Env ->DeleteLocalRef(clazz); env->DeleteLocalRef(str_arg); env->DeleteLocalRef(result); }Copy the code

It can be found that calling Java static methods from Native is relatively simple, and it mainly goes through the following steps.

  1. First, the FindClass function is called to pass in the Class descriptor, find the Class, and get the JClass type.
  2. Then, find the methodID with GetStaticMethodID, pass in the method signature, and get a reference to the jmethodID type.
  3. Call CallStaticObjectMethod to call the static method in the Java class, pass in the parameter, and return the data directly returned by the Java layer. CallStaticVoidMethod(no callback) CallStaticIntMethod(callback Int) callStaticStaticVoidMethod (callback Int) CallStaticFloatMethod, etc.
  4. Remove a local reference.

6.2 Native Invokes Java instance methods

Next, let’s look at creating a Java instance in the Native layer and calling it, in much the same way as calling static methods above. First, let’s modify the CPP file code, as shown below.

extern "C" JNIEXPORT void JNICALL Java_com_xzh_allinone_jni_CallMethodActivity_createAndCallJavaInstanceMethod(JNIEnv *env, jobject thiz) { jclass clazz = env->FindClass("com/xzh/allinone/jni/MyJNIClass"); Construct = env->GetMethodID(clazz, "<init>", "()V"); JmethodID mid_get_age = env-> methodid (clazz, "getAge", "()I"); jmethodID mid_set_age = env->GetMethodID(clazz, "setAge", "(I)V"); jobject jobj = env->NewObject(clazz, mid_construct); SetAge env->CallVoidMethod(jobj, mid_set_age, 20); Jint age = env->CallIntMethod(jobj, mid_get_age); LOGI(" age = %d", age); Env ->DeleteLocalRef(clazz); env->DeleteLocalRef(jobj); }Copy the code

As shown above, the steps for Native to invoke Java instance methods are as follows:

  1. Native calls Java instance methods.
  2. Gets the constructor ID, which gets the id of the method to call. Where to get the constructor, the method name is fixed<init>, followed by the method signature.
  3. Build a Java object using the NewObject() function.
  4. Call the setAge and getAge methods of the Java object, get the return value, and print the result.
  5. Delete the reference.

The NDK is incorrectly located

Since most of the NDK logic is done in C/C++, when the NDK error some fatal error caused the APP to flash back. It is very difficult to troubleshoot such errors, such as memory address access error, use of wild Pointers, memory leak, stack overflow and other native errors, which will lead to APP crash.

While these NDK errors are not easy to troubleshoot, we are not helpless after NDK errors occur. Specifically, when you take the stack log output from Logcat and combine it with two debugging tools, addr2Line and NDK-Stack, you can pinpoint exactly how many lines of code went wrong and quickly find the problem.

First of all, we open the NDK directory down the SDK/the NDK / 21.0.6113669 / toolchains/directory, you can see the NDK cross-compiler toolchain directory structure is shown below.

Then, take a look at the NDK file directory, as shown below.

The NDK-stack is stored in the $NDK_HOME directory, the same directory as the NDK-build directory. Addr2line is in the NDK cross-compiler toolchain directory. In addition, NDK implements multiple tools for different CPU architectures. When using adDR2line, you need to select the tool based on the current CPU architecture of the mobile phone. For example, if my phone is aARCH64, I need to use the tools in aARCH64-linux-Android-4.9. Android NDK provides commands to view the CPU information of the phone, as shown below.

adb shell cat /proc/cpuinfo

Copy the code

Before formally introducing the two debugging tools, we can first write the crashed native code so that we can see the effect. First, we fix the code in native-lib. CPP, as shown below.

void willCrash() { JNIEnv *env = NULL; int version = env->GetVersion(); } extern "C" JNIEXPORT void JNICALL Java_com_xzh_allinone_jni_CallMethodActivity_nativeCrashTest(JNIEnv *env, Jobject thiz) {LOGI(" before crash "); willCrash(); // The following code cannot be executed because LOGI crashes (" after crash "); printf("oooo"); }Copy the code

The above code is an obvious null pointer exception, and the error log is as follows.

The 2020-10-07 17:05:25. 230, 12340-12340 /? A/the DEBUG: * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 2020-10-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-10-07 17:05:25. 230, 12340-12340 /? A/DEBUG: '0' 2020-10-07 17:05:25.230 12340-12340/? A/DEBUG: ABI: 'ARM64' 2020-10-07 17:05:25.237 12340-12340/? A/DEBUG: Timestamp: 2020-06-07 17:05:25+0800 2020-10-07 17:05:25.237 12340-12340/? A/DEBUG: pid: 11527, TID: 11527, name: m.xfhy. allinOne >>> com.xfhy.allinone <<< 2020-10-07 17:05:25.237 12340-12340/? A/DEBUG: uid: 10319 2020-10-07 17:05:25.237 12340-12340/? Signal 11 (SIGSEGV), Code 1 (SEGV_MAPERR), Fault addr 0x0 2020-10-07 17:05:25.237 12340-12340/? A/DEBUG: Cause: NULL pointer dereference 2020-10-07 17:05:25.237 12340-12340/? A/DEBUG: X0 0000000000000000 x1 0000007FD29ffd40 x2 0000000000000005 x3 0000000000000003 2020-10-07 17:05:25.237 12340-12340/? A/DEBUG: X4 0000000000000000 x5 8080800000000000000 x6 fefeff6fb0ce1f1f x7 7f7f7f7fffff7f7f 2020-10-07 17:05:25.237 12340-12340/? A/DEBUG: X8 0000000000000000 x9 a95a4ec0adb574df x10 0000007FD29ffee0 x11 000000000000000A 2020-10-07 17:05:25.237 12340-12340/? A/DEBUG: X12 0000000000000018 X13 FFFFFFFFFFFFFF X14 0000000000000004 X15 FFFFFFFFFFFF 2020-10-07 17:05:25.237 12340-12340 /? A/DEBUG: X16 0000006fc6476c50 x17 0000006fc64513cc x18 00000070b21f6000 x19 000000702d069c00 2020-10-07 17:05:25.237 12340-12340 /? A/DEBUG: X20 0000000000000000 x21 000000702d069c00 x22 0000007FD2a00720 x23 0000006fc6CEB127 2020-10-07 17:05:25.237 12340-12340 /? A/DEBUG: X24 0000000000000004 x25 00000070b1CF2020 x26 000000702D069CB0 x27 0000000000000001 2020-10-07 17:05:25.237 12340-12340 /? A/DEBUG: x28 0000007fd2a004b0 x29 0000007FD2a00420 2020-10-07 17:05:25.237 12340-12340/? A/DEBUG: SP 0000007FD2A00410 LR 0000006FC64513BC PC 0000006FC64513E0 2020-10-07 17:05:25.788 12340-12340/? A/DEBUG: Backtrace: 2020-10-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-10-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-10-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: 17:05:25 b1130c28a8b45feda869397e55c5b6d754410c8d) 2020-10-07. 788, 12340-12340 /? A/DEBUG: #03 pc 000000000013f350 /apex/com.android.runtime/lib64/libart.so (art_quick_generic_jni_trampoline+144) (BuildId: 2 bc2e11d57f839316bf2a42bbfdf943a) 2020-10-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)Copy the code

First, find the key information Cause: NULL pointer dereference, but we do not know where it happened, so we need to use adDR2LINE and NDK-Stack tools to assist us in analysis.

7.1 the addr2line

Now, we use the tool addr2line to locate the location. First, run the following command.

/ Users/XZH/development/SDK/the NDK / 21.0.6113669 / toolchains/aarch64 - Linux - android 4.9 / prebuilt/Darwin - x86_64 / bin/aarch64 - linu X - android - addr2line - e/Users/XZH/development/AllInOne/app/libnative - lib. So 00000000000113 e0 00000000000113 b8 author: Xiao cold month Link: https://juejin.cn/post/6844904190586650632 source: the nuggets copyright owned by the author. Commercial reprint please contact the author for authorization, non-commercial reprint please indicate the source.Copy the code

Where -e specifies the location of the so file, and the trailing 00000000000113e0 and 00000000000113b8 are the assembly instruction addresses for the error location.

/ Users/XZH/development/SDK/the NDK / 21.0.6113669 / toolchains/LLVM/prebuilt/Darwin - x86_64 / sysroot/usr/include/jni. H: 497 /Users/xzh/development/AllInOne/app/src/main/cpp/native-lib.cpp:260Copy the code

As you can see, line 260 of native-lib. CPP is the problem. We just need to find the location and fix the file.

7.2 the NDK – stack

In addition, there is a simpler way, directly enter the command.

adb logcat | ndk-stack -sym /Users/xzh/development/AllInOne/app/build/intermediates/cmake/debug/obj/arm64-v8a

Copy the code

At the end is the location of so file. After executing the command, native errors can be generated on the mobile phone, and then the error point can be located in the SO file.

********** Crash dump: ********** Build fingerprint: 'Xiaomi/dipper/dipper: 10 / QKQ1.190828.002 / V11.0.8.0 QEACNXM: user/release - keys' # 00 0 x00000000000113e0 /data/app/com.xfhy.allinone-oVu0tjta-aW9LYa08eoK1Q==/lib/arm64/libnative-lib.so (_JNIEnv::GetVersion()+20) (BuildId: b1130c28a8b45feda869397e55c5b6d754410c8d) _JNIEnv::GetVersion() / Users/XZH/development/SDK/the 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/xzh/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_xzh_allinone_jni_CallMethodActivity_nativeCrashTest+84) (BuildId: b1130c28a8b45feda869397e55c5b6d754410c8d) Java_com_xzh_allinone_jni_CallMethodActivity_nativeCrashTest /Users/xzh/development/AllInOne/app/src/main/cpp/native-lib.cpp:267:5Copy the code

As you can see, the log above clearly indicates that the fault was with the willCrash() method, which has 260 lines of code.

8, JNI citation

As we all know, Java does not need to consider how the JVM allocated memory when creating new objects, nor does it need to release memory when it is used. C++, however, requires us to manually allocate and free memory (new->delete,malloc->free). With JNI, because native code cannot directly manipulate data structures within the JVM by reference, these operations must be performed by calling the corresponding JNI interface to manipulate data contents within the JVM indirectly. We don’t need to worry about how objects are stored in the JVM, just learn about the three different references in JNI.

8.1 JNI Local Reference

In general, references created in local functions by NewLocalRef or calls to FindClass, NewObject, GetObjectClass, NewCharArray, etc., are local references. Local references have the following characteristics:

  • Prevents the GC from reclaiming the referenced object
  • Cannot be used across threads
  • Not used across functions in local functions
  • Release: When the function returns, the object referenced by the local reference is automatically released by the JVM, or released by calling DeleteLocalRef.

Local references are usually created and used in functions, and are released automatically when the function returns. So why do we need to manually call DeleteLocalRef to release?

For example, if you have a for loop that keeps creating local references, you must manually free the memory using DeleteLocalRef. Otherwise, the number of local references will increase and eventually crash. (On lower Versions of Android, the maximum number of local reference tables is 512, and if you exceed that, you will crash.)

Alternatively, after a local method returns a reference to the Java layer, the local reference is automatically released by the JVM if the Java layer does not use the returned local reference.

8.2 JNI global references

Global references are created based on local references, using the NewGlobalRef method. Global references have the following features:

  • Prevents the GC from reclaiming the referenced object
  • It can be used across methods and threads
  • The JVM does not automatically free it; you need to call DeleteGlobalRef to manually free it

8.3 JNI weak global Reference

Weak global references are created based on local or global references, using the NewWeakGlobalRef method. Weak global references have the following properties:

  • GC is not prevented from reclaiming referenced objects
  • It can be used across methods and threads
  • References are not automatically freed, but are only reclaimed when the JVM runs out of memory, and can be manually freed by calling DeleteWeakGlobalRef.

Thumb up 2 people

diary

Author: hot is you 233 links: www.jianshu.com/p/48ac4e1fd… The copyright of the book belongs to the author. Commercial reprint please contact the author for authorization, non-commercial reprint please indicate the source. ] (//upload-images.jianshu.io/upload_images/24944255-b905f46539bd7dfd.png? imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp)

Let’s talk briefly about what each layer does.

Linux layer

The Linux kernel

Since the Android system is built on the base Linux kernel, Linux is the foundation of the Android system. In fact, Android’s hardware drivers, process management, memory management, and network management are all in this layer.

Hardware abstraction layer

Hardware Abstraction Layer (abbreviated as Hardware Abstraction Layer), which mainly provides the standard display interface for the upper Layer and provides display device Hardware functionality to the higher level Java API framework. HAL contains multiple library modules, each of which implements an interface for a specific type of hardware component, such as a camera or Bluetooth module. When the framework API requires access to device hardware, the Android system loads the corresponding library module for that hardware component.

System runtime and runtime environment layers

Android Runtime

Before Android 5.0 (API 21), the Dalvik virtual machine was used, then replaced by ART. ART is the operating environment of the Android operating system, which executes dex files by running virtual machines. Dex files are bytecode formats designed for Android, which packages and runs dex files, and Android Toolchain can compile Java code into DEX bytecode format, as shown below.

As shown above, Jack is a compilation tool chain that compiles Java source code into DEX bytecode to run on the Android platform.

Native C/C + + library

Many core Android system components and services are written in C and C++. In order to facilitate developers to call these native library functions, the Android Framework provides the corresponding call API. For example, you can access OpenGL ES through the Android framework’s Java OpenGL API to support drawing and manipulating 2D and 3D graphics in your application.

Application framework layer

The most commonly used components and services of Android platform are in this layer, which is a layer that every Android developer must be familiar with and master, and is the foundation of application development.

The Application layer

Android apps, such as email, SMS, calendar, Internet browsing, contacts and other system applications. We can call the system’s App directly like calling the Java API Framework layer.

Let’s take a look at how to write Android JNI and the process required.

NDK

What is the NDK

NDK (Native Development Kit) A software Development Kit based on a Native programming interface that allows you to leverage C and C++ code in Android applications. Programs developed with this tool run directly locally, rather than on a virtual machine.

In Android, the NDK is a collection of tools that extend the Android SDK. The NDK provides a set of tools to help developers quickly develop dynamic libraries in C or C++, and automatically package so and Java applications together as APK. The NDK also integrates a cross-compiler and provides mk files to isolate CPU, platform, ABI, etc. Developers can create SO files by simply modifying mk files (indicating “which files need to be compiled”, “compilation feature requirements”, etc.).

The NDK configuration

Before creating an NDK project, ensure that the NDK environment is available on the local PC. Select [Preferences…] -> [Android SDK] Download the configuration NDK, as shown below.

Then, create a new Native C++ project, as shown below.

Then check the “Include C++ support” option and click “next” to arrive at the Customize C++ support setup page, as shown below.

Then, click the Finish button.

NDK project directory

Open the newly created NDK project, as shown in the following figure.

Let’s take a look at some of the differences between Android’s NDK project and normal Android application project. First, let’s look at the build.gradle configuration.

apply plugin: 'com.android.application' android {compileSdkVersion 30 buildToolsVersion "30.0.2" defaultConfig {applicationId "Com. XZH. The NDK" minSdkVersion 16 targetSdkVersion 30 versionCode 1 versionName testInstrumentationRunner "1.0" "androidx.test.runner.AndroidJUnitRunner" externalNativeBuild { cmake { cppFlags "" } } } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard - rules. Pro externalNativeBuild}} {cmake {path "/ SRC/main/CPP CMakeLists. TXT" version "3.10.2"}}} Dependencies {// omit reference library}Copy the code

Gradle configuration has two more externalNativeBuild configuration items than the normal Android application. ExternalNativeBuild in defaultConfig is the command parameter used to configure Cmake, while externalNativeBuild defines the path to the cmakelists.txt script of Cmake.

TXT file, cmakelists. TXT is a build script for CMake, which is equivalent to android. mk in ndK-build.

# create library add_library(# set library name native-lib # set library mode # SHARED mode compiles the so file, STATIC mode will not compile SHARED code path SRC /main/ CPP /native-lib. CPP Store the library path as a variable, This variable can be used elsewhere to reference the NDK library # set the variable name log here) # associate library target_link_libraries(# associate library native lib # associate native lib and log-lib) ${log-lib} )Copy the code

For more information about CMake, check out the official CMake manual.

The official sample

When the Android NDK project is created by default, Android provides a simple EXAMPLE of JNI interaction that returns a string to the Java layer in the format java_package name_class name_method name. First, let’s take a look at the native-lib.cpp code.

#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL
Java_com_xzh_ndk_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

Copy the code

Then, let’s take a look at the Android mainactivity.java code.

package com.xzh.ndk;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

    static {
        System.loadLibrary("native-lib");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TextView tv = findViewById(R.id.sample_text);
        tv.setText(stringFromJNI());
    }

    public native String stringFromJNI();
}

Copy the code

I met the Android JNI

1. JNI development process

  1. Write Java classes, declare native methods;
  2. Write native code;
  3. Compile native code into SO file;
  4. So library is introduced into Java class and native methods are called.

2. Native method naming

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

}

Copy the code

Function naming rules: Java_ class full path _ Method name. The meanings of the parameters are as follows:

  • JNIEnv* is the first argument that defines any native function and represents a pointer to the JNI environment through which to access the interface methods provided by JNI.
  • Jobject represents this in a Java object or jClass if it is a static method.
  • JNIEXPORT and JNICALL: these are macros defined in JNI and can be found in the jni.h header file.

3. Mapping between JNI data types and Java data types

First, we write a native method declaration in Java code, and then use the [Alt + Enter] shortcut key to ask AS to help us create a native method, AS shown below.

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) { }Copy the code

Next, let’s sort out the Java and JNI type comparison table, as shown below.

Java type Native type Whether conform to the Word length
boolean jboolean unsigned 8 bytes
byte jbyte A signed 8 bytes
char jchar unsigned 16 bytes
short jshort A signed 16 bytes
int jint A signed 32 bytes
long jlong A signed 64 bytes
float jfloat A signed 32 bytes
double jdouble A signed 64 bytes

The following table lists the corresponding reference types.

Java type Native type
java.lang.Class jclass
java.lang.Throwable jthrowable
java.lang.String jstring
jjava.lang.Object[] jobjectArray
Byte[] jbyteArray
Char[] jcharArray
Short[] jshortArray
int[] jintArray
long[] jlongArray
float[] jfloatArray
double[] jdoubleArray

3.1 Basic data types

The basic data type Native is essentially a C/C++ basic type with a typedef to redefine a new name, which can be accessed directly in JNI, as shown below.

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 */

Copy the code

3.2 Reference data types

If written in C++, all references are derived from the jobject root class, as shown below.

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 {};

Copy the code

When JNI uses C, all reference types use Jobject.

4, JNI string processing

4.1 Native operating JVM

JNI passes all objects in Java to local methods as a C pointer to the JVM’s internal data structures, which are not visible in memory. You can only select the appropriate JNI function from the list of functions pointed to by the JNIEnv pointer to manipulate data structures in the JVM.

For example, native accessing jString, the JNI type corresponding to Java.lang. String, cannot be used as a basic data type because it is a Java reference type. So the contents of a string can only be accessed in native code through JNI functions like GetStringUTFChars.

4.2 Examples of string operations

// Call String result = operateString(" String to be operated on "); Log.d("xfhy", result); // Define public native String operateString(String STR);Copy the code

Then implement it in C, as follows.

extern "C" JNIEXPORT jstring JNICALL Java_com_xfhy_jnifirst_MainActivity_operateString(JNIEnv *env, jobject thiz, Const char *strFromJava = (char *) env->GetStringUTFChars(STR, NULL); If (strFromJava == NULL) {// return NULL must be checked; Char buff[128] = {0}; char buff[128] = {0}; strcpy(buff, strFromJava); Strcat (buff, "add something to the end of string "); Env ->ReleaseStringUTFChars(strFromJava); Return env->NewStringUTF(buff); }Copy the code
Obtaining JVM strings in 4.2.1 Native

In the above code, the operateString function takes a jString argument STR, which refers to a string inside the JVM and cannot be used directly. First, jString needs to be converted to the C-style string type char* before it can be used, where appropriate JNI functions must be used to access string data structures within the JVM.

GetStringUTFChars(jString String, jBoolean * isCopy) has the following meanings:

  • String: JString, a string pointer that Java passes to native code.
  • IsCopy: NULL is normally passed. The value can be JNI_TRUE or JNI_FALSE. JNI_TRUE returns a copy of the source string within the JVM and allocates memory for the newly generated string. JNI_FALSE returns a pointer to the source string within the JVM, meaning that the source string can be modified at the Native layer, but is not recommended because Java string principles cannot be modified.

Java uses Unicode encoding by default, while C/C++ uses UTF encoding by default. Therefore, encoding conversion is required when the Native layer communicates with the Java layer in strings. GetStringUTFChars converts the string of a JString pointer (pointing to a sequence of Unicode characters inside the JVM) into a UTF-8 C string.

4.2.2 Exception Handling

When using GetStringUTFChars, the return value may be NULL, which needs to be handled, otherwise you will have problems using the string as you proceed. Because this method is copied, the JVM allocates memory for the newly generated string, and the call fails when it runs out of memory. If the call fails, NULL is returned and OutOfMemoryError is thrown. A pending exception encountered by JNI does not change the flow of the application, and it continues down the path.

4.2.3 Releasing String Resources

Unlike Java, Native has to manually free the requested memory space. When GetStringUTFChars is called, it requests a new space to hold the copied string, which is used for native code to access and modify things like that. Since memory is allocated, it must be freed manually, using ReleaseStringUTFChars. You can see that GetStringUTFChars is matched one-to-one.

4.2.4 Constructing a String

The NewStringUTF function builds a JString by passing in a C string of type CHAR *. It builds a new java.lang.String String object and automatically converts it to Unicode encoding. If the JVM cannot allocate enough memory to construct java.lang.String, an OutOfMemoryError is thrown and NULL is returned.

4.2.5 Other String Manipulation functions
  1. GetStringChars and ReleaseStringChars: this similar in functions and the Get/ReleaseStringUTFChars function, used to capture and release of a string in Unicode format of coding.
  2. GetStringLength: Gets the length of a Unicode string (jString). Utf-8 encoded strings end in 0, whereas Unicode strings do not, so you need to separate them here.
  3. GetStringUTFLength: Gets the length of the UTF-8 encoding string, which is the length of the DEFAULT C/C++ encoding string. You can also use the standard C function “strlen” to get its length.
  4. Strcat: concatenated string, standard C function. Such asstrcat(buff, "xfhy");Add XFHY to the end of the buff.
  5. GetStringCritical and ReleaseStringCritical: To increase the possibility of returning a pointer to a Java string directly (rather than copying it). The area between these two functions is a native function that must never call other JNI functions or block threads. Otherwise the JVM may deadlock. If you have a particularly large string, such as 1M, and only need to read it and print it out, use this pair to return a pointer to the source string.
  6. GetStringRegion and GetStringUTFRegion: Gets the contents of Unicode and UTF-8 strings in the specified range (e.g., only strings at indexes 1-3). This pair copies the source string into a pre-allocated buffer (its own char array).

Often, GetStringUTFRegion bounds checking, crossing the line would throw StringIndexOutOfBoundsException anomalies. GetStringUTFRegion is similar to GetStringUTFChars, except that GetStringUTFRegion does not allocate memory internally and does not throw an overflow exception. Since there is no internal memory allocated, there is no function like Release to free resources.

4.2.6 summary
  • Java string to C/C++ string: To use GetStringUTFChars, ReleaseStringUTFChars must be called to free memory.
  • Create Unicode strings required by the Java layer, using the NewStringUTF function.
  • To get the C/C++ string length, use GetStringUTFLength or strlen.
  • For small strings, the GetStringRegion and GetStringUTFRegion functions are the best choices because the buffer array can be extracted and allocated by the compiler without running out of memory. This works fine when you only need to process part of the string. They provide starting index and substring length values, and replication costs are minimal
  • To get Unicode strings and lengths, use the GetStringChars and GetStringLength functions.

An array of operating

5.1 Array of primitive types

An array of primitive types is an array of primitive data types in JNI that can be accessed directly. For example, here is an example of summing an int array.

//MainActivity.java
public native int sumArray(int[] array);

Copy the code
extern "C" JNIEXPORT jint JNICALL Java_com_xfhy_jnifirst_MainActivity_sumArray(JNIEnv *env, jobject thiz, JintArray array) {// Array sum int result = 0; Jint arr_len = env->GetArrayLength(array); Jint *c_array = (jint *) malloc(arr_len * sizeof(jint)); Memset (c_array, 0, sizeof(jint) * arr_len); Env ->GetIntArrayRegion(array, 0, arr_len, For (int I = 0; I < arr_len; ++ I) {result += c_array[I];} return result;Copy the code

The C layer needs to get the length of jintArray first, and then dynamically apply for an array (because the length of the array passed by the Java layer is variable, so we need to dynamically apply for the C layer array). The elements of the array are of type Jint. Malloc is a commonly used function that allocates a contiguous chunk of memory that needs to be freed manually by calling free. We then call the GetIntArrayRegion function to copy the Java layer array into the C layer array and sum it up.

Now, let’s look at another way of summing, as follows.

extern "C" JNIEXPORT jint JNICALL Java_com_xfhy_jnifirst_MainActivity_sumArray(JNIEnv *env, jobject thiz, JintArray array) {// Array sum int result = 0; Jint *c_arr = env->GetIntArrayElements(array, NULL); 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); I could write it this way, or I could write it on the bottom line result += c_arr[I]; Release env->ReleaseIntArrayElements(array, c_arr, 0); return result; }Copy the code

In the above code, we get the pointer to the elements of the original array directly through the GetIntArrayElements function. It seems a lot easier, but I personally feel that this approach is a bit dangerous, since it is not safe to modify the source array directly at the C layer. The second argument to GetIntArrayElements is usually passed NULL. Passing JNI_TRUE returns a pointer to the temporary buffer array (that is, making a copy), and passing JNI_FALSE returns a pointer to the original array.

5.2 Object Array

The elements in the object array are instances of a class or references to other arrays, and cannot directly access the arrays that Java passes to the JNI layer. Manipulating object arrays is a bit more complicated. Here’s an example: Create a two-dimensional array in the Native layer, assign the value and return it to the Java layer for use.

public native int[][] init2DArray(int size); 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]); }}Copy the code
extern "C" JNIEXPORT jobjectArray JNICALL Java_com_xzh_jnifirst_MainActivity_init2DArray(JNIEnv *env, jobject thiz, Jint size) {// create a size*size array //jobjectArray is used to hold an array of objects Java array is an object int[] jclass classIntArray = Env ->FindClass("[I"); if (classIntArray == NULL) {return NULL;} // Create an array object with classIntArray jobjectArray result = env->NewObjectArray(size, classIntArray, NULL); if (result == NULL) {return NULL;} for (int I = 0; I < size; ++ I) {jint buff[100]; // Create the two-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) { Env ->SetIntArrayRegion(intArr, 0, size, IntArr env->SetObjectArrayElement(result, I, SetObjectArrayElement); Env ->DeleteLocalRef(intArr);} return result;}Copy the code

Next, let’s take a look at the code.

  1. First, we use the FindClass function to find the class of the Java layer int[] object, which is passed to NewObjectArray to create an array of objects. After calling the NewObjectArray function, we create an array of objects of size and the element type of the previously obtained class.
  2. Enter the for loop and build the size of the int array. Build the int array using the NewIntArray function. So you can see that I’ve built a temporary array of buffs, and then I’ve set the size arbitrarily, and this is just for example, you can actually use malloc to dynamically apply for space, so you don’t have to apply for 100 Spaces, which might be too big or too small. The entire buff array is mainly used to assign values to the generated jintArray, because jintArray is a Java data structure, our native can not directly operate, we have to call the SetIntArrayRegion function, the buff array value copy into 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 reference to the jintArray generated in for in time. The jintArray created is a JNI local reference, and if there are too many local references, the JNI reference table will overflow.

6. Native Java method

Anyone familiar with the JVM should know that when running a Java program in the JVM, all relevant class files needed at runtime are first loaded into the JVM and loaded on demand to improve performance and save memory. Before we call a static method of a class, the JVM determines whether the class has been loaded, and if it has not been loaded by the ClassLoader into the JVM, it looks up the class in the classpath path. If found, the class is loaded. If not found, ClassNotFoundException is reported.

6.1 Native Invoking Java static methods

First, we write a myJniclass.java class with the following code.

public class MyJNIClass { public int age = 30; 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() +" the content is :" + text; }}Copy the code

Then, the getDes() method is called in Native. For complexity, the getDes() method has both input and return arguments, as shown below.

extern "C" JNIEXPORT void JNICALL Java_com_xzh_allinone_jni_CallMethodActivity_callJavaStaticMethod(JNIEnv *env, Jobject thiz) {// call a class static method //1\. From the classpath directory search MyJNIClass this Class, and returns the Class object of the Class jclass clazz = env - > FindClass (" com/XZH/jni/jni/MyJNIClass "); JmethodID mid_get_des = env->GetStaticMethodID(clazz, "GetStaticMethodID ", "(Ljava/lang/String;) Ljava/lang/String;" ); Jstring str_arg = env->NewStringUTF(" I am XZH "); jstring result = (jstring) env->CallStaticObjectMethod(clazz, mid_get_des, str_arg); const char *result_str = env->GetStringUTFChars(result, NULL); LOGI(" get data returned by Java layer: %s", result_str); Env ->DeleteLocalRef(clazz); env->DeleteLocalRef(str_arg); env->DeleteLocalRef(result); }Copy the code

It can be found that calling Java static methods from Native is relatively simple, and it mainly goes through the following steps.

  1. First, the FindClass function is called to pass in the Class descriptor, find the Class, and get the JClass type.
  2. Then, find the methodID with GetStaticMethodID, pass in the method signature, and get a reference to the jmethodID type.
  3. Call CallStaticObjectMethod to call the static method in the Java class, pass in the parameter, and return the data directly returned by the Java layer. CallStaticVoidMethod(no callback) CallStaticIntMethod(callback Int) callStaticStaticVoidMethod (callback Int) CallStaticFloatMethod, etc.
  4. Remove a local reference.

6.2 Native Invokes Java instance methods

Next, let’s look at creating a Java instance in the Native layer and calling it, in much the same way as calling static methods above. First, let’s modify the CPP file code, as shown below.

extern "C" JNIEXPORT void JNICALL Java_com_xzh_allinone_jni_CallMethodActivity_createAndCallJavaInstanceMethod(JNIEnv *env, jobject thiz) { jclass clazz = env->FindClass("com/xzh/allinone/jni/MyJNIClass"); Construct = env->GetMethodID(clazz, "<init>", "()V"); JmethodID mid_get_age = env-> methodid (clazz, "getAge", "()I"); jmethodID mid_set_age = env->GetMethodID(clazz, "setAge", "(I)V"); jobject jobj = env->NewObject(clazz, mid_construct); SetAge env->CallVoidMethod(jobj, mid_set_age, 20); Jint age = env->CallIntMethod(jobj, mid_get_age); LOGI(" age = %d", age); Env ->DeleteLocalRef(clazz); env->DeleteLocalRef(jobj); }Copy the code

As shown above, the steps for Native to invoke Java instance methods are as follows:

  1. Native calls Java instance methods.
  2. Gets the constructor ID, which gets the id of the method to call. Where to get the constructor, the method name is fixed<init>, followed by the method signature.
  3. Build a Java object using the NewObject() function.
  4. Call the setAge and getAge methods of the Java object, get the return value, and print the result.
  5. Delete the reference.

The NDK is incorrectly located

Since most of the NDK logic is done in C/C++, when the NDK error some fatal error caused the APP to flash back. It is very difficult to troubleshoot such errors, such as memory address access error, use of wild Pointers, memory leak, stack overflow and other native errors, which will lead to APP crash.

While these NDK errors are not easy to troubleshoot, we are not helpless after NDK errors occur. Specifically, when you take the stack log output from Logcat and combine it with two debugging tools, addr2Line and NDK-Stack, you can pinpoint exactly how many lines of code went wrong and quickly find the problem.

First of all, we open the NDK directory down the SDK/the NDK / 21.0.6113669 / toolchains/directory, you can see the NDK cross-compiler toolchain directory structure is shown below.

Then, take a look at the NDK file directory, as shown below.

The NDK-stack is stored in the $NDK_HOME directory, the same directory as the NDK-build directory. Addr2line is in the NDK cross-compiler toolchain directory. In addition, NDK implements multiple tools for different CPU architectures. When using adDR2line, you need to select the tool based on the current CPU architecture of the mobile phone. For example, if my phone is aARCH64, I need to use the tools in aARCH64-linux-Android-4.9. Android NDK provides commands to view the CPU information of the phone, as shown below.

adb shell cat /proc/cpuinfo

Copy the code

Before formally introducing the two debugging tools, we can first write the crashed native code so that we can see the effect. First, we fix the code in native-lib. CPP, as shown below.

void willCrash() { JNIEnv *env = NULL; int version = env->GetVersion(); } extern "C" JNIEXPORT void JNICALL Java_com_xzh_allinone_jni_CallMethodActivity_nativeCrashTest(JNIEnv *env, Jobject thiz) {LOGI(" before crash "); willCrash(); // The following code cannot be executed because LOGI crashes (" after crash "); printf("oooo"); }Copy the code

The above code is an obvious null pointer exception, and the error log is as follows.

The 2020-10-07 17:05:25. 230, 12340-12340 /? A/the DEBUG: * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 2020-10-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-10-07 17:05:25. 230, 12340-12340 /? A/DEBUG: '0' 2020-10-07 17:05:25.230 12340-12340/? A/DEBUG: ABI: 'ARM64' 2020-10-07 17:05:25.237 12340-12340/? A/DEBUG: Timestamp: 2020-06-07 17:05:25+0800 2020-10-07 17:05:25.237 12340-12340/? A/DEBUG: pid: 11527, TID: 11527, name: m.xfhy. allinOne >>> com.xfhy.allinone <<< 2020-10-07 17:05:25.237 12340-12340/? A/DEBUG: uid: 10319 2020-10-07 17:05:25.237 12340-12340/? Signal 11 (SIGSEGV), Code 1 (SEGV_MAPERR), Fault addr 0x0 2020-10-07 17:05:25.237 12340-12340/? A/DEBUG: Cause: NULL pointer dereference 2020-10-07 17:05:25.237 12340-12340/? A/DEBUG: X0 0000000000000000 x1 0000007FD29ffd40 x2 0000000000000005 x3 0000000000000003 2020-10-07 17:05:25.237 12340-12340/? A/DEBUG: X4 0000000000000000 x5 8080800000000000000 x6 fefeff6fb0ce1f1f x7 7f7f7f7fffff7f7f 2020-10-07 17:05:25.237 12340-12340/? A/DEBUG: X8 0000000000000000 x9 a95a4ec0adb574df x10 0000007FD29ffee0 x11 000000000000000A 2020-10-07 17:05:25.237 12340-12340/? A/DEBUG: X12 0000000000000018 X13 FFFFFFFFFFFFFF X14 0000000000000004 X15 FFFFFFFFFFFF 2020-10-07 17:05:25.237 12340-12340 /? A/DEBUG: X16 0000006fc6476c50 x17 0000006fc64513cc x18 00000070b21f6000 x19 000000702d069c00 2020-10-07 17:05:25.237 12340-12340 /? A/DEBUG: X20 0000000000000000 x21 000000702d069c00 x22 0000007FD2a00720 x23 0000006fc6CEB127 2020-10-07 17:05:25.237 12340-12340 /? A/DEBUG: X24 0000000000000004 x25 00000070b1CF2020 x26 000000702D069CB0 x27 0000000000000001 2020-10-07 17:05:25.237 12340-12340 /? A/DEBUG: x28 0000007fd2a004b0 x29 0000007FD2a00420 2020-10-07 17:05:25.237 12340-12340/? A/DEBUG: SP 0000007FD2A00410 LR 0000006FC64513BC PC 0000006FC64513E0 2020-10-07 17:05:25.788 12340-12340/? A/DEBUG: Backtrace: 2020-10-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-10-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-10-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: 17:05:25 b1130c28a8b45feda869397e55c5b6d754410c8d) 2020-10-07. 788, 12340-12340 /? A/DEBUG: #03 pc 000000000013f350 /apex/com.android.runtime/lib64/libart.so (art_quick_generic_jni_trampoline+144) (BuildId: 2 bc2e11d57f839316bf2a42bbfdf943a) 2020-10-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)Copy the code

First, find the key information Cause: NULL pointer dereference, but we do not know where it happened, so we need to use adDR2LINE and NDK-Stack tools to assist us in analysis.

7.1 the addr2line

Now, we use the tool addr2line to locate the location. First, run the following command.

/ Users/XZH/development/SDK/the NDK / 21.0.6113669 / toolchains/aarch64 - Linux - android 4.9 / prebuilt/Darwin - x86_64 / bin/aarch64 - linu X - android - addr2line - e/Users/XZH/development/AllInOne/app/libnative - lib. So 00000000000113 e0 00000000000113 b8 author: Xiao cold month Link: https://juejin.cn/post/6844904190586650632 source: the nuggets copyright owned by the author. Commercial reprint please contact the author for authorization, non-commercial reprint please indicate the source.Copy the code

Where -e specifies the location of the so file, and the trailing 00000000000113e0 and 00000000000113b8 are the assembly instruction addresses for the error location.

/ Users/XZH/development/SDK/the NDK / 21.0.6113669 / toolchains/LLVM/prebuilt/Darwin - x86_64 / sysroot/usr/include/jni. H: 497 /Users/xzh/development/AllInOne/app/src/main/cpp/native-lib.cpp:260Copy the code

As you can see, line 260 of native-lib. CPP is the problem. We just need to find the location and fix the file.

7.2 the NDK – stack

In addition, there is a simpler way, directly enter the command.

adb logcat | ndk-stack -sym /Users/xzh/development/AllInOne/app/build/intermediates/cmake/debug/obj/arm64-v8a

Copy the code

At the end is the location of so file. After executing the command, native errors can be generated on the mobile phone, and then the error point can be located in the SO file.

********** Crash dump: ********** Build fingerprint: 'Xiaomi/dipper/dipper: 10 / QKQ1.190828.002 / V11.0.8.0 QEACNXM: user/release - keys' # 00 0 x00000000000113e0 /data/app/com.xfhy.allinone-oVu0tjta-aW9LYa08eoK1Q==/lib/arm64/libnative-lib.so (_JNIEnv::GetVersion()+20) (BuildId: b1130c28a8b45feda869397e55c5b6d754410c8d) _JNIEnv::GetVersion() / Users/XZH/development/SDK/the 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/xzh/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_xzh_allinone_jni_CallMethodActivity_nativeCrashTest+84) (BuildId: b1130c28a8b45feda869397e55c5b6d754410c8d) Java_com_xzh_allinone_jni_CallMethodActivity_nativeCrashTest /Users/xzh/development/AllInOne/app/src/main/cpp/native-lib.cpp:267:5Copy the code

As you can see, the log above clearly indicates that the fault was with the willCrash() method, which has 260 lines of code.

8, JNI citation

As we all know, Java does not need to consider how the JVM allocated memory when creating new objects, nor does it need to release memory when it is used. C++, however, requires us to manually allocate and free memory (new->delete,malloc->free). With JNI, because native code cannot directly manipulate data structures within the JVM by reference, these operations must be performed by calling the corresponding JNI interface to manipulate data contents within the JVM indirectly. We don’t need to worry about how objects are stored in the JVM, just learn about the three different references in JNI.

8.1 JNI Local Reference

In general, references created in local functions by NewLocalRef or calls to FindClass, NewObject, GetObjectClass, NewCharArray, etc., are local references. Local references have the following characteristics:

  • Prevents the GC from reclaiming the referenced object
  • Cannot be used across threads
  • Not used across functions in local functions
  • Release: When the function returns, the object referenced by the local reference is automatically released by the JVM, or released by calling DeleteLocalRef.

Local references are usually created and used in functions, and are released automatically when the function returns. So why do we need to manually call DeleteLocalRef to release?

For example, if you have a for loop that keeps creating local references, you must manually free the memory using DeleteLocalRef. Otherwise, the number of local references will increase and eventually crash. (On lower Versions of Android, the maximum number of local reference tables is 512, and if you exceed that, you will crash.)

Alternatively, after a local method returns a reference to the Java layer, the local reference is automatically released by the JVM if the Java layer does not use the returned local reference.

8.2 JNI global references

Global references are created based on local references, using the NewGlobalRef method. Global references have the following features:

  • Prevents the GC from reclaiming the referenced object
  • It can be used across methods and threads
  • The JVM does not automatically free it; you need to call DeleteGlobalRef to manually free it

8.3 JNI weak global Reference

Weak global references are created based on local or global references, using the NewWeakGlobalRef method. Weak global references have the following properties:

  • GC is not prevented from reclaiming referenced objects
  • It can be used across methods and threads
  • References are not automatically freed, but are only reclaimed when the JVM runs out of memory, and can be manually freed by calling DeleteWeakGlobalRef.

At the end of the article

Welcome to follow my CSDN, share Android dry goods, exchange Android technology. If you have any opinions or technical questions about this article, please leave a comment in the comments section and I will answer them for you. Finally, if you want to know more Android knowledge or need other information I are free to share here, just you can support me oh!

– You can directly click here to see all the content of the free package to receive.