Introduction to Android NDK development

Time:2020-10-30

Introduction to JNI

JNI (short for Java Native Interface), translated into Java local interface. It is one of the many java development technologies. It aims to provide more efficient and flexible development for Java programs by using local code. Although Java has always been known for its good cross platform performance, C / C + + is the only true cross platform, because 90% of the systems in the world are written based on C / C + +. At the same time, Java’s cross platform is to sacrifice efficiency in exchange for compatibility with multiple platforms, so JNI is one of the mainstream implementation methods of this cross platform.

In a word, JNI is a technology, a communication technology between Java and C / C + +. First, let’s review the Android system architecture.
Introduction to Android NDK development
Let’s briefly introduce the role of each layer.

Linux layer

Linux kernel

Since Android system is built on the basis of Linux kernel, Linux is the basis of Android system. In fact, Android hardware driver, process management, memory management, network management are all in this layer.

Hardware abstraction layer

Hardware abstraction layer (abbreviated as hardware abstraction layer) provides the standard display interface for the upper layer, and provides the hardware functions of display devices to 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 the device hardware, Android system will load the corresponding library module for the hardware component.

System runtime and environment layer

Android Runtime

Before Android 5.0 (API 21), it used Dalvik virtual machine, which was later replaced by art. Art is the running environment of Android operating system, which executes DEX file by running virtual machine. Among them, DEX file is a bytecode format specially designed for Android. Android packages and runs DEX files. Android toolchain (a compiler tool) can compile java code into DEX bytecode format. The conversion process is shown in the figure below.
Introduction to Android NDK development
As shown above, Jack is a compilation tool chain, which can compile java source code into DEX bytecode, so that it can run on 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 corresponding API calls. For example, you can access OpenGL es through the Java OpenGL API of the Android framework to support drawing and manipulating 2D and 3D graphics in applications.

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 basis of application development.

Application layer

Android system app, such as email, SMS, calendar, Internet browsing and contact system applications. We can directly call the app of the system just like calling the Java API framework layer.

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

NDK

What is NDK

NDK (native development kit) is a software development kit based on native program interface, which can let you use C and C + + code tools in Android applications. Programs developed with this tool run locally, not virtual machines.

In Android, NDK is a collection of tools, mainly used to extend the Android SDK. NDK provides a series of tools to help developers quickly develop C or C + + dynamic libraries, and can automatically package so and Java applications into APK. At the same time, NDK also integrates cross compiler and provides corresponding MK files to isolate CPU, platform, ABI and other differences. Developers can create so files by simply modifying MK files (indicating “which files need to be compiled” and “compilation characteristics requirements”).

NDK configuration

Before creating a NDK project, please ensure that the relevant environment of NDK has been set up locally. Select [preferences…] – > [Android SDK] to download and configure NDK, as shown below.
Introduction to Android NDK development
Then, create a new native C + + project, as shown below.
Introduction to Android NDK development
Then check the [include C + + support] option and click [next] to reach the [customize C + + support] setting page, as shown below.
Introduction to Android NDK development
Then, click the [finish] button.

NDK project directory

Open the new NDK project, and the directory is shown in the following figure.
Introduction to Android NDK development
Let’s take a look at the differences between Android NDK project and ordinary Android application project. First of all, let’s take a look build.gradle to configure.

apply plugin: 'com.android.application'

android {
    compileSdkVersion 30
    buildToolsVersion "30.0.2"

    defaultConfig {
        applicationId "com.xzh.ndk"
        minSdkVersion 16
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "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 {
  //Third party library with omitted references
}

Compared with ordinary Android applications, build.gradle There are two more external native build configuration items in the configuration. Among them, the external native build in defaultconfig is mainly used to configure the command parameters of cmake, while the external
External native build mainly defines the build script of cmake CMakeLists.txt The path of.

Then, let’s take a look CMakeLists.txt Documents, CMakeLists.txt It is the build script of cmake, which is equivalent to that of NDK build Android.mk The code is as follows.

#Set cmake minimum version
cmake_minimum_required(VERSION 3.4.1)

#Compiling Library
add_ Library (ා set the library name
             native-lib

             #Set library mode
             #Shared mode compiles so files, while static mode does not
             SHARED

             #Set native code path
             src/main/cpp/native-lib.cpp )

#Positioning Library
find_ Library (ා library name)
              log-lib

              #Store the library path as a variable that can be used to reference the NDK library elsewhere
              #Set the variable name here
              log )

#Associate Library
target_ link_ Libraries (ා associated libraries)
                       native-lib

                       #Associate native lib and log Lib
                       ${log-lib} )

For more information about cmake, you can viewCmake official manual

Official example

When creating an Android NDK project by default, Android provides a simple JNI interaction example, and returns a string to the Java layer. The format of the method name is as follows:Java_ Package name_ Class name_ Method name。 First, let’s take a look at native- lib.cpp Code for.

#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());
}

Then, we’re looking at Android MainActivity.java Code for.

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();
}

Getting to know Android JNI

1. JNI development process

  1. Write java class and declare native method;
  2. Write native code;
  3. Compile native code into so file;
  4. So library is introduced into Java class to call native method;

2. Native method naming

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

Function naming rules:Java_ Class full path_ Method nameThe meanings of the parameters involved are as follows:

  • Jnienv * is the first parameter to define any native function, which represents a pointer to the JNI environment, through which you can access the interface methods provided by JNI.
  • Jobobject represents this in a Java object, and jclass if it is a static method.
  • Jniexport and jnicall: they are the macros defined in JNI and can be found in the JNI. H header file.

3. Correspondence between JNI data type and Java data type

First, we write a native method declaration in Java code, and then use the [Alt + Enter] shortcut key to let as 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);


//Corresponding native code
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) {

}

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

Java type Native type Is there any conformity Word length
boolean jboolean No sign 8 bytes
byte jbyte Signed 8 bytes
char jchar No sign 16 bytes
short jshort Signed 16 bytes
int jint Signed 32 bytes
long jlong Signed 64 bytes
float jfloat Signed 32 bytes
double jdouble Signed 64 bytes

The corresponding reference types are shown in the following table.

|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 of native is actually to redefine the basic type in C / C + + with typedef, 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 */

3.2 reference data type

If written in the C + + language, all references are derived from the jobobject 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 {};

When JNI uses C language, all reference types use jobobject.

4, JNI string processing

4.1 native operation JVM

JNI will pass all the objects in Java as a C pointer to the local method. This pointer points to the internal data structure of the JVM, and the storage mode of the internal data structure in memory is invisible. You can only select the appropriate JNI function from the function table pointed by the jnienv pointer to operate the data structure in the JVM.

For example, native access java.lang.String When the corresponding JNI type jstring is used, it cannot be used like accessing the basic data type. Because it is a java reference type, the contents of the string can only be accessed through JNI functions like getstringutfchars in local code.

4.2 examples of string operations

//Call
String result = operatestring ("string to be operated");
Log.d("xfhy", result);

//Definition
public native String operateString(String str);

Then implement it in C. the code is as follows.

extern "C"
JNIEXPORT jstring JNICALL
Java_com_xfhy_jnifirst_MainActivity_operateString(JNIEnv *env, jobject thiz, jstring str) {
    //Copy the string from the memory of Java and use it in native
    const char *strFromJava = (char *) env->GetStringUTFChars(str, NULL);
    if (strFromJava == NULL) {
        //Must be empty check
        return NULL;
    }

    //Copy the strfromjava to the buff and use it to generate strings later
    char buff[128] = {0};
    strcpy(buff, strFromJava);
    Strcat (buff, "add something after the string");

    //Releasing resources
    env->ReleaseStringUTFChars(str, strFromJava);

    //Automatically convert to unicode
    return env->NewStringUTF(buff);
}
4.2.1 get JVM string from native

In the above code, the operatstring function receives a parameter STR of type jstring. Jstring is a string pointing to the interior of 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. Here, the appropriate JNI function must be used to access the string data structure inside the JVM.

The meanings of the parameters corresponding to getstringutfchars (jstring string, jboolean * iscopy) are as follows:

  • String: jsstring, the string pointer passed by java to native code.
  • Iscopy: transfer null in general, and the value can be JNI_ True and JNI_ False if JNI_ True returns a copy of the source string within the JVM and allocates memory space for the newly generated string. If it’s JNI_ False returns the pointer to the source string inside the JVM, which means that the source string can be modified at the native level, but it is not recommended to modify it, because the principle of Java string cannot be modified.

In Java, Unicode is used by default, while UTF is used by C / C + + by default. Therefore, code conversion is needed when string communication between native layer and Java layer. Getstringutfchars can just convert the string of jstring pointer (pointing to the Unicode character sequence inside the JVM) into a C string in UTF-8 format.

4.2.2 exception handling

When getstringutfchars is used, the returned value may be null, which needs to be dealt with. Otherwise, if you continue to go down, there will be problems when using this string. Because when this method is called, it is a copy. The JVM allocates memory space for the newly generated string. When the memory space is not enough, the call will fail. If the call fails, it will return null and throw outofmemoryerror. JNI will not change the running process of the program when encountering pending exceptions, but will continue to move forward.

4.2.3 releasing string resources

Unlike Java, native needs to release the requested memory space manually. When calling getstringutfchars, a new space will be applied to hold the copied string. This string is used to facilitate the access and modification of native code. Since there is memory allocation, it must be released manually by releasestring utfchars. You can see that getstringutfchars are one-to-one paired.

4.2.4 building strings

A jstring can be constructed by using the newstringutf function. A char * type C string needs to be passed in. It will build a new java.lang.String String object and is automatically converted to unicode encoding. If the JVM cannot be a construct java.lang.String If enough memory is allocated, an outofmemoryerror exception is thrown and null is returned.

4.2.5 other string operation functions
  1. Getstringchars and releasestringchars: this pair of functions is similar to the get / releasestringutfchars function, and the strings used for getting and releasing are encoded in Unicode format.
  2. Getstringlength: gets the length of the Unicode string (jstring). UTF-8 encodes strings that end in 0, but not in Unicode, so we need to distinguish them separately.
  3. “Getstringutflength”: to get the length of UTF-8 encoded string is to get the length of default encoding string of C / C + +. The standard C function “strlen” can also be used to obtain its length.
  4. Strcat: concatenation string, standard C function. asstrcat(buff, "xfhy");Add xfhy to the end of the buff.
  5. Getstringcritical and releasestringcritical: in order to increase the possibility of directly returning a pointer to a Java string (instead of copying it). In the area between the two functions, it is absolutely forbidden to call other JNI functions or native functions that cause threads to block. Otherwise, the JVM may deadlock. If the content of a string is particularly large, such as 1m, And only need to read the content inside and print it out. At this time, it is more suitable to use this pair of functions, which can directly return the pointer of the source string.
  6. Getstringregion and getstringutfraction: get the contents of the specified range in Unicode and UTF-8 strings (for example, only strings at indexes 1-3 are required), which will copy the source string into a pre allocated buffer (self-defined char array).

Generally, getstringutframe will check for out of bounds and throw stringindexoutofboundsexception. In fact, getstringutfraction is a little similar to getstringutfchars, but the getstringutfraction does not allocate memory and does not throw memory overflow exceptions. Since there is no memory allocated internally, there is no function like release to release resources.

4.2.6 summary
  • Java string to C / C + + string: to use getstringutfchars function, you must call releasestringutfchars to free memory.
  • Create the Unicode string required by the Java layer and use the newstringutf function.
  • To get the length of C / C + + string, use getstringutflength or strlen function.
  • For small strings, getstringregion and getstringutfraction functions are the best choice, because the buffer array can be extracted and allocated by the compiler, and there will be no memory overflow exception. It’s also good when you only need to process part of the string. They provide start index and substring length values, and the cost of replication is very small
  • Get the Unicode string and length, using the getstringchars and getstringlength functions.

Array operation

5.1 basic type array

Basic type array is an array composed of basic data types in JNI, which can be accessed directly. For example, the following is an example of the sum of int arrays. The code is as follows.

//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) {
    //Array summation
    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);
    //The [0-arr_ Copy the element at position len to C_ Array array
    env->GetIntArrayRegion(array, 0, arr_len, c_array);
    for (int i = 0; i < arr_len; ++i) {
        result += c_array[i];
    }
    //Dynamically requested memory must be freed
    free(c_array);

    return result;
}

After the C layer gets the jintararray, it needs to obtain its length first, and then dynamically apply for an array (because the length of the array passed by the Java layer is indefinite, so we need to apply for the C-layer array dynamically). The elements of this array are of jint type. Malloc is a frequently used function to apply for a block of continuous memory. The memory after application needs to be released manually by calling free. Then, the getintarrayregion function is called to copy the Java layer array into the C layer array and sum it.

Next, let’s look at another summation. The code is as follows.

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

    //Mode 2  
    //This method is more dangerous. Getintarrayelements can get the pointer of array element directly and modify the array element 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 the one below
        result += c_arr[i];
    }
    //If you have get, you usually have release
    env->ReleaseIntArrayElements(array, c_arr, 0);

    return result;
}

In the above code, we can get the element pointer of the original array directly through the getintarrayelements function, and we can get the element summation directly. It seems to be much simpler, but I personally think it is a bit dangerous. After all, it is not very safe to modify the source array directly in the C layer. The second parameter of getintarrayelements generally passes null and JNI_ True is to return the temporary buffer array pointer (that is, copy a copy) and pass JNI_ False returns the original array pointer.

5.2 object array

The element in the object array is an instance of a class or a reference to other arrays, so the array passed by java to JNI layer cannot be accessed directly. The array of operands is slightly more complicated. Here is 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);

//Give it to the native layer to create - > java printout
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_xzh_jnifirst_MainActivity_init2DArray(JNIEnv *env, jobject thiz, jint size) {
    //Create a two-dimensional array of size * size

    //Jobobjectarray is a Java array used to hold an object array, which is an object int []
    jclass classIntArray = env->FindClass("[I");
    if (classIntArray == NULL) {
        return NULL;
    }
    //Create an array object with classintarray as the element
    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 dimension array
        jintArray intArr = env->NewIntArray(size);
        if (intArr == NULL) {
            return NULL;
        }
        for (int j = 0; j < size; ++j) {
            //Set any value here
            buff[j] = 666;
        }
        //Set the data for a jintararray
        env->SetIntArrayRegion(intArr, 0, size, buff);
        //Set the I index of the data for a jobobjectarray, and the data bit intarr
        env->SetObjectArrayElement(result, i, intArr);
        //Remove references in time
        env->DeleteLocalRef(intArr);
    }

    return result;
}

Next, let’s analyze the code.

  1. First, use the findclass function to find the class of the Java layer int [] object. This class needs to be passed into newobjectarray to create an object array. After calling the newobjectarray function, you can create an object array with the size of size and the element type of the previously obtained class.
  2. Enter the for loop to construct size int arrays, which need to use the newintarray function. As you can see, I built a temporary buff array, and then set the size randomly. Here is an example. In fact, we can use malloc to dynamically apply for space, so as not to apply for 100 spaces, which may be too large or too small. The whole buff array is mainly used to assign values to the generated jinterarray. Because jintararray is a java data structure, we cannot operate directly by native. We have to call the setintarrayregion function to copy the values of the buff array to the jintarray array.
  3. Then we call the SetObjectArrayElement function to set the data at the index of the jobjectArray array, and set the jintArray generated here.
  4. Finally, it is necessary to remove the reference from the jintararray generated in for in time. The created jinterarray is a JNI local reference. If there are too many local references, JNI reference table will overflow.

6. Native calls Java methods

Anyone who is familiar with the JVM should know that when running a java program in the JVM, it will first load all the relevant class files needed by the runtime into the JVM and load them on demand to improve performance and save memory. Before we call the static method of a class, the JVM will first determine whether the class has been loaded. If it is not loaded into the JVM by classloader, it will look for the class in the classpath path path. If it is found, the class will be loaded; if not, classnotfoundexception will be reported.

6.1 native calls Java static methods

First, let’s write one MyJNIClass.java Class, the code is as follows.

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

}

Then, the getDes () method is invoked in native. In order to be more complicated, the getDes () method not only has an input parameter, but also a return parameter, as shown below.

extern "C"
JNIEXPORT void JNICALL
Java_com_xzh_allinone_jni_CallMethodActivity_callJavaStaticMethod(JNIEnv *env, jobject thiz) {
    //Call the static method of a class
    //1. Search myjniclass from the classpath path path and return the class object of this class
    jclass clazz = env->FindClass("com/xzh/jni/jni/MyJNIClass");
    //2. Find the getdes method from the clazz class to get the method id of the 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 XZH");
    jstring result = (jstring) env->CallStaticObjectMethod(clazz, mid_get_des, str_arg);
    const char *result_str = env->GetStringUTFChars(result, NULL);
    Log ("get the data returned by Java layer: s", result_ str);

    //4. Remove local references
    env->DeleteLocalRef(clazz);
    env->DeleteLocalRef(str_arg);
    env->DeleteLocalRef(result);
}

It can be found that native is relatively simple to call Java static methods, mainly through the following steps.

  1. First of all, call the findclass function to pass in the class descriptor (the full class name of Java class. Here, when you enter myjniclass in as, you will be prompted to complete it, and enter directly to complete it). Find the class and get the jclass type.
  2. Then, find the ID of the method through getstaticmethodid, pass in the method signature, and get the reference of jmethodeid type.
  3. Build a reference, then call CallStaticObjectMethod to call the static method in the Java class, and then pass in the parameters, and the returned data is directly the data returned from the Java layer. In fact, the callstaticobjectmethod here is a static method of the reference type called. Similar to it are: callstaticvoidmethod (no return parameter), callstaticintmethod (return parameter is int), callstaticfloatmethod, etc.
  4. Remove local references.

6.2 native calls Java instance method

Next, let’s take a look at the method of creating a Java instance in the native layer and calling the instance, which is roughly the same as calling static methods above. First, we modify the code of the cpp file, 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");
    //Gets the method id of the constructor
    jmethodID mid_construct = env->GetMethodID(clazz, "<init>", "()V");
    //Get the method id of 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 the method setage
    env->CallVoidMethod(jobj, mid_set_age, 20);
    //Then call the method getage to get the return value print output
    jint age = env->CallIntMethod(jobj, mid_get_age);
    Logi ("get age =% d", age);

    //If you use a subclass of jobobject, you need to remove the reference
    env->DeleteLocalRef(clazz);
    env->DeleteLocalRef(jobj);
}

As shown in the above example, call the Java Native method as follows:

  1. Native calls Java instance methods.
  2. Get the ID of the constructor and the ID of the method to be called. When the constructor is obtained, the method name is fixed<init>, followed by the method signature.
  3. Use the newobject() function to build a Java object.
  4. Call setage and getage methods of Java object to get the return value and print the result.
  5. Delete the reference.

NDK error location

Because most of the logic of NDK is completed in C / C + +, app will flash back when some fatal error occurs in NDK. For example, native errors such as memory address access error, using wild pointer, memory leak, stack overflow and other native errors will lead to app crash.

Although these NDK errors are not easy to check, we are not helpless after the NDK errors. Specifically, when you get the log from logcat, combined with two debugging tools, addr2line and NDK stack, you can accurately locate the number of lines of code with errors, and then quickly find the problem.

First of all, open the SDK / NDK / 21.0.6113669/toolchains / under the NDK directory. We can see the directory structure of the NDK cross compiler tool chain as follows.
Introduction to Android NDK development
Then, let’s take a look at the NDK file directory, as shown below.
Introduction to Android NDK development
Among them, NDK stack is placed in $NDK_ Home directory, and NDK build directory of the same level. Addr2line is in the cross compiler tool chain directory of NDK. At the same time, NDK implements several sets of tools for different CPU architectures. When using the addr2line tool, you need to choose according to the current mobile phone CPU architecture. For example, my mobile phone is aarch64, so I need to use itaarch64-linux-android-4.9Tools under the directory. Android NDK provides commands to view the CPU information of the phone, as shown below.

adb shell cat /proc/cpuinfo

Before introducing the two debugging tools, we can write the native code of the crash to facilitate us to check the effect. First, we fix native- lib.cpp The code inside is 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 later code cannot be executed because it crashes
    Logi ("after crash");
    printf("oooo");
}

The above code is an obvious null pointer exception. The error log after running is as follows.

2020-10-07 17:05:25.230 12340-12340/? A/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: Revision: '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/? A/DEBUG: 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  8080800000000000  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 ffffffffffffffff  x14 0000000000000004  x15 ffffffffffffffff
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: b1130c28a8b45feda869397e55c5b6d754410c8d)
2020-10-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-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)

First, find the key informationCause: null pointer dereferenceBut we don’t know where it happened, so we need to use addr2line and NDK stack tools to help us analyze.

7.1 addr2line

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

/Users/xzh/development/sdk/ndk/21.0.6113669/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-addr2line -e /Users/xzh/development/AllInOne/app/libnative-lib.so 00000000000113e0 00000000000113b8

Author: cool wind and cold moon
Link: https://juejin.im/post/6844904190586650632
Source: Nuggets
The copyright belongs to the author. For commercial reprint, please contact the author for authorization. For non-commercial reprint, please indicate the source.

Where – E is the location of the so file, and then the assembly instruction address of the location where the error occurred is located.

/Users/xzh/development/sdk/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:260

As you can see, it’s native- lib.cpp We just need to find the location and fix the file.

7.2 ndk-stack

In addition, there is a simpler way to enter commands directly.

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

At the end is the location of the so file. After executing the command, the native error can be generated on the 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 0x00000000000113e0 /data/app/com.xfhy.allinone-oVu0tjta-aW9LYa08eoK1Q==/lib/arm64/libnative-lib.so (_JNIEnv::GetVersion()+20) (BuildId: b1130c28a8b45feda869397e55c5b6d754410c8d)
                                                                                                        _JNIEnv::GetVersion()
                                                                                                        /Users/xzh/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/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:5

As you can see, the above log clearly points out that the error is caused by the willcrash() method, and the number of lines of code is 260 lines.

8. JNI reference

As we all know, when Java creates new objects, it does not need to consider how the JVM applies for memory, nor does it need to release memory after using it. Unlike C + +, we need to manually request and release memory (New > delete, malloc > free). When using JNI, because the local code can’t operate the data structure inside the JVM directly by reference, the corresponding JNI interface must be called to operate the data content inside the JVM indirectly. We don’t need to care how objects are stored in the JVM, we just need to learn three different references in JNI.

8.1 JNI local reference

Generally, the references created by newlocalref or by calling findclass, newobject, getobjectclass, newchararray in local functions are local references. Local references have the following characteristics:

  • Prevents GC from recycling referenced objects
  • Cannot be used across threads
  • Not used across functions in local functions
  • Release: after the function returns, the object referenced by the local reference will be automatically released by the JVM, or it can be released by calling deletelocalref.

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

For example, if a for loop is opened in which local references are continuously created, then you must use the deletelocalref to release the memory manually. Otherwise, there will be more and more local references, which will eventually lead to a crash (in the lower version of Android, the maximum number of local reference tables is 512. If it exceeds the limit, it 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 reference

Global references are created based on local references, using the newglobalref method. Global references have the following characteristics:

  • Prevents GC from recycling referenced objects
  • It can be used across methods and threads
  • The JVM will not be released automatically. You need to call deleteglobalref to release it manually

8.3 JNI weak global reference

Weak global references are created based on local or global references and are created using the newweakglobalref method. Weak global references have the following characteristics:

  • GC is not prevented from recycling referenced objects
  • It can be used across methods and threads
  • The reference will not be released automatically. It will only be released when the JVM runs out of memory. In addition, you can call the deleteweakglobalref to manually release.

reference resources:
Android developers NDK guide C + + library support
JNI / NDK Development Guide
JNI local reference table overflow for Android memory leak

Recommended Today

Java security framework

The article is mainly divided into three parts1. The architecture and core components of spring security are as follows: (1) authentication; (2) authority interception; (3) database management; (4) authority caching; (5) custom decision making; and;2. To build and use the environment, the current popular spring boot is used to build the environment, and the actual […]