JNI 简介

JNI (Java Native Interface英文缩写),译为Java本地接口。是Java泛滥开发技术中的一门技术,意在利用本地代码,为Java程序提供更高效、更灵便的拓展。只管Java一贯以其良好的跨平台性而著称,但真正的跨平台非C/C++莫属,因为以后世上90%的零碎都是基于C/C++编写的。同时,Java的跨平台是以就义效率换来对多种平台的兼容性,因此JNI就是这种跨平台的支流实现形式之一。

总之,JNI是一门技术,是Java 与C/C++ 沟通的一门技术。首先,来回顾下Android的零碎架构图。

咱们来简略介绍下每一层的作用。

Linux层

Linux 内核

因为Android 零碎是根底Linux 内核构建的,所以Linux是Android零碎的根底。事实上,Android 的硬件驱动、过程治理、内存治理、网络管理都是在这一层。

硬件形象层

硬件形象层(Hardware Abstraction Layer缩写),硬件形象层次要为下层提供规范显示界面,并向更高级别的 Java API 框架提供显示设施硬件性能。HAL 蕴含多个库模块,其中每个模块都为特定类型的硬件组件实现一个界面,例如相机或蓝牙模块。当框架 API 要求拜访设施硬件时,Android 零碎将为该硬件组件加载对应的库模块。

零碎运行库和运行环境层

Android Runtime

Android 5.0(API 21)之前,应用的是Dalvik虚拟机,之后被ART所取代。ART是Android操作系统的运行环境,通过运行虚拟机来执行dex文件。其中,dex文件是专为安卓设计的的字节码格局,Android打包和运行的就是dex文件,而Android toolchain(一种编译工具)能够将Java代码编译为dex字节码格局,转化过程如下图。

如上所示,Jack就是一种编译工具链,能够将Java 源代码编译为 DEX 字节码,使其可在 Android 平台上运行。

原生C/C++ 库

很多外围 Android 零碎组件和服务都是应用C 和 C++ 编写的,为了不便开发者调用这些原生库性能,Android的Framework提供了调用相应的API。例如,您能够通过 Android 框架的 Java OpenGL API 拜访 OpenGL ES,以反对在利用中绘制和操作 2D 和 3D 图形。

应用程序框架层

Android平台最罕用的组件和服务都在这一层,是每个Android开发者必须相熟和把握的一层,是利用开发的根底。

Application层

Android零碎App,如电子邮件、短信、日历、互联网浏览和联系人等零碎利用。咱们能够像调用Java API Framework层一样间接调用零碎的App。

接下来咱们看一下如何编写Android JNI ,以及须要的流程。

NDK

NDK是什么

NDK(Native Development Kit缩写)一种基于原生程序接口的软件开发工具包,能够让您在 Android 利用中利用 C 和 C++ 代码的工具。通过此工具开发的程序间接在本地运行,而不是虚拟机。

在Android中,NDK是一系列工具的汇合,次要用于扩大Android SDK。NDK提供了一系列的工具能够帮忙开发者疾速的开发C或C++的动静库,并能主动将so和Java利用一起打包成apk。同时,NDK还集成了穿插编译器,并提供了相应的mk文件隔离CPU、平台、ABI等差别,开发人员只须要简略批改mk文件(指出“哪些文件须要编译”、“编译个性要求”等),就能够创立出so文件。

NDK配置

创立NDK工程之前,请先保障本地曾经搭建好了NDK的相干环境。顺次抉择【Preferences...】->【Android SDK】下载配置NDK,如下所示。

而后,新建一个Native C++工程,如下所示。

而后勾选【Include C++ support】选项,点击【下一步】,达到【Customize C++ Support】设置页,如下所示。

而后,点击【Finish】按钮即可。

NDK 我的项目目录

关上新建的NDK工程,目录如下图所示。

咱们接下来看一下,Android的NDK工程和一般的Android利用工程有哪些不一样的中央。首先,咱们来看下build.gradle配置。

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 {  // 省略援用的第三方库}

能够看到,相比一般的Android利用,build.gradle配置中多了两个externalNativeBuild配置项。其中,defaultConfig外面的的externalNativeBuild次要是用于配置Cmake的命令参数,而内部的
externalNativeBuild的次要是定义了CMake的构建脚本CMakeLists.txt的门路。

而后,咱们来看一下CMakeLists.txt文件,CMakeLists.txt是CMake的构建脚本,作用相当于ndk-build中的Android.mk,代码如下。

# 设置Cmake最小版本cmake_minimum_required(VERSION 3.4.1)# 编译libraryadd_library( # 设置library名称             native-lib             # 设置library模式             # SHARED模式会编译so文件,STATIC模式不会编译             SHARED             # 设置原生代码门路             src/main/cpp/native-lib.cpp )# 定位libraryfind_library( # library名称              log-lib              # 将library门路存储为一个变量,能够在其余中央用这个变量援用NDK库              # 在这里设置变量名称              log )# 关联librarytarget_link_libraries( # 关联的library                       native-lib                       # 关联native-lib和log-lib                       ${log-lib} )

对于CMake的更多常识,能够查看CMake官网手册。

官网示例

默认创立Android NDK工程时,Android提供了一个简略的JNI交互示例,返回一个字符串给Java层,办法名的格局为:Java_包名_类名_办法名 。首先,咱们看一下native-lib.cpp的代码。

#include <jni.h>#include <string>extern "C" JNIEXPORT jstring JNICALLJava_com_xzh_ndk_MainActivity_stringFromJNI(        JNIEnv* env,        jobject /* this */) {    std::string hello = "Hello from C++";    return env->NewStringUTF(hello.c_str());}

而后,咱们在看一下Android的MainActivity.java 的代码。

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

初识Android JNI

1,JNI开发流程

  1. 编写java类,申明了native办法;
  2. 编写native代码;
  3. 将native代码编译成so文件;
  4. 在java类中引入so库,调用native办法;

2,native办法命名

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

函数命名规定: Java_类全门路_办法名,波及的参数的含意如下:

  • JNIEnv*是定义任意native函数的第一个参数,示意指向JNI环境的指针,能够通过它来拜访JNI提供的接口办法。
  • jobject示意Java对象中的this,如果是静态方法则示意jclass。
  • JNIEXPORT和JNICALL: 它们是JNI中所定义的宏,能够在jni.h这个头文件中查找到。

3,JNI数据类型与Java数据类型的对应关系

首先,咱们在Java代码里编写一个native办法申明,而后应用【alt+enter】快捷键让AS帮忙咱们创立一个native办法,如下所示。

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);//对应的Native代码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) {}

上面,咱们整顿下Java和JNI的类型对照表,如下所示。

Java 类型Native类型有无合乎字长
booleanjboolean无符号8字节
bytejbyte有符号8字节
charjchar无符号16字节
shortjshort有符号16字节
intjint有符号32字节
longjlong有符号64字节
floatjfloat有符号32字节
doublejdouble有符号64字节

对应的援用类型如下表所示。

| Java 类型 | Native类型 |
|--|--|
| 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根本数据类型

Native的根本数据类型其实就是将C/C++中的根本类型用typedef从新定义了一个新的名字,在JNI中能够间接拜访,如下所示。

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 援用数据类型

如果应用C++语言编写,则所有援用派生自jobject根类,如下所示。

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

JNI应用C语言时,所有援用类型都应用jobject。

4,JNI的字符串解决

4.1 native操作JVM

JNI会把Java中所有对象当做一个C指针传递到本地办法中,这个指针指向JVM外部数据结构,而外部的数据结构在内存中的存储形式是不可见的.只能从JNIEnv指针指向的函数表中抉择适合的JNI函数来操作JVM中的数据结构。

比方native拜访java.lang.String 对应的JNI类型jstring时,不能像拜访根本数据类型那样应用,因为它是一个Java的援用类型,所以在本地代码中只能通过相似GetStringUTFChars这样的JNI函数来拜访字符串的内容。

4.2 字符串操作的示例

//调用String result = operateString("待操作的字符串");Log.d("xfhy", result);//定义public native String operateString(String str);

而后在C中进行实现,代码如下。

extern "C"JNIEXPORT jstring JNICALLJava_com_xfhy_jnifirst_MainActivity_operateString(JNIEnv *env, jobject thiz, jstring str) {    //从java的内存中把字符串拷贝进去  在native应用    const char *strFromJava = (char *) env->GetStringUTFChars(str, NULL);    if (strFromJava == NULL) {        //必须空查看        return NULL;    }    //将strFromJava拷贝到buff中,待会儿好拿去生成字符串    char buff[128] = {0};    strcpy(buff, strFromJava);    strcat(buff, " 在字符串前面加点货色");    //开释资源    env->ReleaseStringUTFChars(str, strFromJava);    //主动转为Unicode    return env->NewStringUTF(buff);}
4.2.1 native中获取JVM字符串

在下面的代码中,operateString函数接管一个jstring类型的参数str,jstring是指向JVM外部的一个字符串,不能间接应用。首先,须要将jstring转为C格调的字符串类型char*后能力应用,这里必须应用适合的JNI函数来拜访JVM外部的字符串数据结构。

GetStringUTFChars(jstring string, jboolean* isCopy)对应的参数的含意如下:

  • string : jstring,Java传递给native代码的字符串指针。
  • isCopy : 个别状况下传NULL,取值能够是JNI_TRUE和JNI_FALSE,如果是JNI_TRUE则会返回JVM外部源字符串的一份拷贝,并为新产生的字符串分配内存空间。如果是JNI_FALSE则返回JVM外部源字符串的指针,意味着能够在native层批改源字符串,然而不举荐批改,因为Java字符串的准则是不能批改的。

Java中默认是应用Unicode编码,C/C++默认应用UTF编码,所以在native层与java层进行字符串交换的时候须要进行编码转换。GetStringUTFChars就刚好能够把jstring指针(指向JVM外部的Unicode字符序列)的字符串转换成一个UTF-8格局的C字符串。

4.2.2 异样解决

在应用GetStringUTFChars的时候,返回的值可能为NULL,这时须要解决一下,否则持续往下面走的话,应用这个字符串的时候会呈现问题.因为调用这个办法时,是拷贝,JVM为新生成的字符串分配内存空间,当内存空间不够调配的时候就会导致调用失败。调用失败就会返回NULL,并抛出OutOfMemoryError。JNI遇到未决的异样不会改变程序的运行流程,还是会持续往下走。

4.2.3 开释字符串资源

native不像Java,咱们须要手动开释申请的内存空间。GetStringUTFChars调用时会新申请一块空间用来装拷贝进去的字符串,这个字符串用来不便native代码拜访和批改之类的。既然有内存调配,那么就必须手动开释,开释办法是ReleaseStringUTFChars。能够看到和GetStringUTFChars是一一对应配对的。

4.2.4 构建字符串

应用NewStringUTF函数能够构建出一个jstring,须要传入一个char *类型的C字符串。它会构建一个新的java.lang.String字符串对象,并且会主动转换成Unicode编码。如果JVM不能为结构java.lang.String调配足够的内存,则会抛出一个OutOfMemoryError异样并返回NULL。

4.2.5 其余字符串操作函数
  1. GetStringChars和ReleaseStringChars:这对函数和Get/ReleaseStringUTFChars函数性能相似,用于获取和开释的字符串是以Unicode格局编码的。
  2. GetStringLength:获取Unicode字符串(jstring)的长度。 UTF-8编码的字符串是以0结尾,而Unicode的不是,所以这里须要独自辨别开。
  3. 「GetStringUTFLength」: 获取UTF-8编码字符串的长度,就是获取C/C++默认编码字符串的长度.还能够应用规范C函数「strlen」来获取其长度。
  4. strcat: 拼接字符串,规范C函数。如strcat(buff, "xfhy"); 将xfhy增加到buff的开端。
  5. GetStringCritical和ReleaseStringCritical: 为了减少间接传回指向Java字符串的指针的可能性(而不是拷贝).在这2个函数之间的区域,是相对不能调用其余JNI函数或者让线程阻塞的native函数.否则JVM可能死锁. 如果有一个字符串的内容特地大,比方1M,且只须要读取外面的内容打印进去,此时比拟适宜用该对函数,可间接返回源字符串的指针。
  6. GetStringRegion和GetStringUTFRegion: 获取Unicode和UTF-8字符串中指定范畴的内容(如: 只须要1-3索引处的字符串),这对函数会将源字符串复制到一个事后调配的缓冲区(本人定义的char数组)内。

通常,GetStringUTFRegion会进行越界查看,越界会抛StringIndexOutOfBoundsException异样。GetStringUTFRegion其实和GetStringUTFChars有点类似,然而GetStringUTFRegion外部不会分配内存,不会抛出内存溢出异样。因为其外部没有分配内存,所以也没有相似Release这样的函数来开释资源。

4.2.6 小结
  • Java字符串转C/C++字符串: 应用GetStringUTFChars函数,必须调用ReleaseStringUTFChars开释内存。
  • 创立Java层须要的Unicode字符串,应用NewStringUTF函数。
  • 获取C/C++字符串长度,应用GetStringUTFLength或者strlen函数。
  • 对于小字符串,GetStringRegion和GetStringUTFRegion这2个函数是最佳抉择,因为缓冲区数组能够被编译器提取调配,不会产生内存溢出的异样。当只须要解决字符串的局部数据时,也还是不错。它们提供了开始索引和子字符串长度值,复制的耗费也是十分小
  • 获取Unicode字符串和长度,应用GetStringChars和GetStringLength函数。

数组操作

5.1 根本类型数组

根本类型数组就是JNI中的根本数据类型组成的数组,能够间接拜访。例如,上面是int数组求和的例子,代码如下。

//MainActivity.javapublic native int sumArray(int[] array);
extern "C"JNIEXPORT jint JNICALLJava_com_xfhy_jnifirst_MainActivity_sumArray(JNIEnv *env, jobject thiz, jintArray array) {    //数组求和    int result = 0;    //形式1  举荐应用    jint arr_len = env->GetArrayLength(array);    //动静申请数组    jint *c_array = (jint *) malloc(arr_len * sizeof(jint));    //初始化数组元素内容为0    memset(c_array, 0, sizeof(jint) * arr_len);    //将java数组的[0-arr_len)地位的元素拷贝到c_array数组中    env->GetIntArrayRegion(array, 0, arr_len, c_array);    for (int i = 0; i < arr_len; ++i) {        result += c_array[i];    }    //动静申请的内存 必须开释    free(c_array);    return result;}

C层拿到jintArray之后首先须要获取它的长度,而后动静申请一个数组(因为Java层传递过去的数组长度是不定的,所以这里须要动静申请C层数组),这个数组的元素是jint类型的。malloc是一个常常应用的拿来申请一块间断内存的函数,申请之后的内存是须要手动调用free开释的。而后就是调用GetIntArrayRegion函数将Java层数组拷贝到C层数组中并进行求和。

接下来,咱们来看另一种求和形式,代码如下。

extern "C"JNIEXPORT jint JNICALLJava_com_xfhy_jnifirst_MainActivity_sumArray(JNIEnv *env, jobject thiz, jintArray array) {    //数组求和    int result = 0;    //形式2      //此种形式比拟危险,GetIntArrayElements会间接获取数组元素指针,是能够间接对该数组元素进行批改的.    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); 写成这种模式,或者上面一行那种都行        result += c_arr[i];    }    //有Get,个别就有Release    env->ReleaseIntArrayElements(array, c_arr, 0);    return result;}

在下面的代码中,咱们间接通过GetIntArrayElements函数拿到原数组元素指针,间接操作就能够拿到元素求和。看起来要简略很多,然而这种形式我集体感觉是有点危险,毕竟这种能够在C层间接进行源数组批改不是很保险的。GetIntArrayElements的第二个参数个别传NULL,传递JNI_TRUE是返回长期缓冲区数组指针(即拷贝一个正本),传递JNI_FALSE则是返回原始数组指针。

5.2 对象数组

对象数组中的元素是一个类的实例或其余数组的援用,不能间接拜访Java传递给JNI层的数组。操作对象数组稍显简单,上面举一个例子:在native层创立一个二维数组,且赋值并返回给Java层应用。

public native int[][] init2DArray(int size);//交给native层创立->Java打印输出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 JNICALLJava_com_xzh_jnifirst_MainActivity_init2DArray(JNIEnv *env, jobject thiz, jint size) {    //创立一个size*size大小的二维数组    //jobjectArray是用来装对象数组的   Java数组就是一个对象 int[]    jclass classIntArray = env->FindClass("[I");    if (classIntArray == NULL) {        return NULL;    }    //创立一个数组对象,元素为classIntArray    jobjectArray result = env->NewObjectArray(size, classIntArray, NULL);    if (result == NULL) {        return NULL;    }    for (int i = 0; i < size; ++i) {        jint buff[100];        //创立第二维的数组 是第一维数组的一个元素        jintArray intArr = env->NewIntArray(size);        if (intArr == NULL) {            return NULL;        }        for (int j = 0; j < size; ++j) {            //这里轻易设置一个值            buff[j] = 666;        }        //给一个jintArray设置数据        env->SetIntArrayRegion(intArr, 0, size, buff);        //给一个jobjectArray设置数据 第i索引,数据位intArr        env->SetObjectArrayElement(result, i, intArr);        //及时移除援用        env->DeleteLocalRef(intArr);    }    return result;}

接下来,咱们来剖析下代码。

  1. 首先,是利用FindClass函数找到java层int[]对象的class,这个class是须要传入NewObjectArray创建对象数组的。调用NewObjectArray函数之后,即可创立一个对象数组,大小是size,元素类型是后面获取到的class。
  2. 进入for循环构建size个int数组,构建int数组须要应用NewIntArray函数。能够看到我构建了一个长期的buff数组,而后大小是轻易设置的,这里是为了示例,其实能够用malloc动静申请空间,省得申请100个空间,可能太大或者太小了。整buff数组次要是拿来给生成进去的jintArray赋值的,因为jintArray是Java的数据结构,咱native不能间接操作,得调用SetIntArrayRegion函数,将buff数组的值复制到jintArray数组中。
  3. 而后调用SetObjectArrayElement函数设置jobjectArray数组中某个索引处的数据,这里将生成的jintArray设置进去。
  4. 最初须要将for外面生成的jintArray及时移除援用。创立的jintArray是一个JNI部分援用,如果部分援用太多的话,会造成JNI援用表溢出。

6,Native调Java办法

相熟JVM的都应该晓得,在JVM中运行一个Java程序时,会先将运行时须要用到的所有相干class文件加载到JVM中,并按需加载,进步性能和节约内存。当咱们调用一个类的静态方法之前,JVM会先判断该类是否曾经加载,如果没有被ClassLoader加载到JVM中,会去classpath门路下查找该类。找到了则加载该类,没有找到则报ClassNotFoundException异样。

6.1 Native调用Java静态方法

首先,咱们编写一个MyJNIClass.java类,代码如下。

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 "传入的字符串长度是 :" + text.length() + "  内容是 : " + text;    }}

而后,在native中调用getDes()办法,为了简单一点,这个getDes()办法不仅有入参,还有返参,如下所示。

extern "C"JNIEXPORT void JNICALLJava_com_xzh_allinone_jni_CallMethodActivity_callJavaStaticMethod(JNIEnv *env, jobject thiz) {    //调用某个类的static办法    //1. 从classpath门路下搜寻MyJNIClass这个类,并返回该类的Class对象    jclass clazz = env->FindClass("com/xzh/jni/jni/MyJNIClass");    //2. 从clazz类中查找getDes办法 失去这个静态方法的办法id    jmethodID mid_get_des = env->GetStaticMethodID(clazz, "getDes", "(Ljava/lang/String;)Ljava/lang/String;");    //3. 构建入参,调用static办法,获取返回值    jstring str_arg = env->NewStringUTF("我是xzh");    jstring result = (jstring) env->CallStaticObjectMethod(clazz, mid_get_des, str_arg);    const char *result_str = env->GetStringUTFChars(result, NULL);    LOGI("获取到Java层返回的数据 : %s", result_str);    //4. 移除部分援用    env->DeleteLocalRef(clazz);    env->DeleteLocalRef(str_arg);    env->DeleteLocalRef(result);}

能够发现,Native调用Java静态方法还是比较简单的,次要会经验以下几个步骤。

  1. 首先,调用FindClass函数传入Class描述符(Java类的全类名,这里在AS中输出MyJNIClass时会有提醒补全,间接enter即可补全),找到该类并失去jclass类型。
  2. 而后,通过GetStaticMethodID找到该办法的id,传入办法签名,失去jmethodID类型的援用。
  3. 构建入参,而后调用CallStaticObjectMethod去调用Java类外面的静态方法,而后传入参数,返回的间接就是Java层返回的数据。其实,这里的CallStaticObjectMethod是调用的援用类型的静态方法,与之类似的还有:CallStaticVoidMethod(无返参),CallStaticIntMethod(返参是Int),CallStaticFloatMethod等。
  4. 移除部分援用。

6.2 Native调用Java实例办法

接下来,咱们来看一下在Native层创立Java实例并调用该实例的办法,大抵上是和下面调用静态方法差不多的。首先,咱们批改下cpp文件的代码,如下所示。

extern "C"JNIEXPORT void JNICALLJava_com_xzh_allinone_jni_CallMethodActivity_createAndCallJavaInstanceMethod(JNIEnv *env, jobject thiz) {        jclass clazz = env->FindClass("com/xzh/allinone/jni/MyJNIClass");    //获取构造方法的办法id    jmethodID mid_construct = env->GetMethodID(clazz, "<init>", "()V");    //获取getAge办法的办法id    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);    //调用办法setAge    env->CallVoidMethod(jobj, mid_set_age, 20);    //再调用办法getAge 获取返回值 打印输出    jint age = env->CallIntMethod(jobj, mid_get_age);    LOGI("获取到 age = %d", age);    //但凡应用是jobject的子类,都须要移除援用    env->DeleteLocalRef(clazz);    env->DeleteLocalRef(jobj);}

如上所示,Native调用Java实例办法的步骤如下:

  1. Native调用Java实例办法。
  2. 获取构造方法的id,获取须要调用办法的id。其中获取构造方法时,办法名称固定写法就是<init>,而后前面是办法签名。
  3. 应用NewObject()函数构建一个Java对象。
  4. 调用Java对象的setAge和getAge办法,获取返回值,打印后果。
  5. 删除援用。

NDK谬误定位

因为NDK大部分的逻辑是在C/C++实现的,当NDK产生谬误某种致命的谬误的时候导致APP闪退。对于这类谬误问题是十分不好排查的,比方内存地址拜访谬误、应用野指针、内存泄露、堆栈溢出等native谬误都会导致APP解体。

尽管这些NDK谬误不好排查,然而咱们在NDK谬误产生后也不是毫无办法可言。具体来说,当拿到Logcat输入的堆栈日志,再联合addr2line和ndk-stack两款调试工具,就能够很够准确地定位到相应产生谬误的代码行数,进而迅速找到问题。

首先,咱们关上ndk目录下下的sdk/ndk/21.0.6113669/toolchains/目录,能够看到NDK穿插编译器工具链的目录构造如下所示。

而后,咱们再看一下ndk的文件目录,如下所示。

其中,ndk-stack放在$NDK_HOME目录下,与ndk-build同级目录。addr2line在ndk的穿插编译器工具链目录下。同时,NDK针对不同的CPU架构实现了多套工具,在应用addr2line工具时,须要依据以后手机cpu架构来抉择。比方,我的手机是aarch64的,那么须要应用aarch64-linux-android-4.9目录下的工具。Android NDK提供了查看手机的CPU信息的命令,如下所示。

adb shell cat /proc/cpuinfo

在正式介绍两款调试工具之前,咱们能够先写好解体的native代码不便咱们查看成果。首先,咱们修复native-lib.cpp外面的代码,如下所示。

void willCrash() {    JNIEnv *env = NULL;    int version = env->GetVersion();}extern "C"JNIEXPORT void JNICALLJava_com_xzh_allinone_jni_CallMethodActivity_nativeCrashTest(JNIEnv *env, jobject thiz) {    LOGI("解体前");    willCrash();    //前面的代码是执行不到的,因为解体了    LOGI("解体后");    printf("oooo");}

下面的这段代码是很显著的空指针异样,运行后谬误日志如下。

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+08002020-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: 103192020-10-07 17:05:25.237 12340-12340/? A/DEBUG: signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x02020-10-07 17:05:25.237 12340-12340/? A/DEBUG: Cause: null pointer dereference2020-10-07 17:05:25.237 12340-12340/? A/DEBUG:     x0  0000000000000000  x1  0000007fd29ffd40  x2  0000000000000005  x3  00000000000000032020-10-07 17:05:25.237 12340-12340/? A/DEBUG:     x4  0000000000000000  x5  8080800000000000  x6  fefeff6fb0ce1f1f  x7  7f7f7f7fffff7f7f2020-10-07 17:05:25.237 12340-12340/? A/DEBUG:     x8  0000000000000000  x9  a95a4ec0adb574df  x10 0000007fd29ffee0  x11 000000000000000a2020-10-07 17:05:25.237 12340-12340/? A/DEBUG:     x12 0000000000000018  x13 ffffffffffffffff  x14 0000000000000004  x15 ffffffffffffffff2020-10-07 17:05:25.237 12340-12340/? A/DEBUG:     x16 0000006fc6476c50  x17 0000006fc64513cc  x18 00000070b21f6000  x19 000000702d069c002020-10-07 17:05:25.237 12340-12340/? A/DEBUG:     x20 0000000000000000  x21 000000702d069c00  x22 0000007fd2a00720  x23 0000006fc6ceb1272020-10-07 17:05:25.237 12340-12340/? A/DEBUG:     x24 0000000000000004  x25 00000070b1cf2020  x26 000000702d069cb0  x27 00000000000000012020-10-07 17:05:25.237 12340-12340/? A/DEBUG:     x28 0000007fd2a004b0  x29 0000007fd2a004202020-10-07 17:05:25.237 12340-12340/? A/DEBUG:     sp  0000007fd2a00410  lr  0000006fc64513bc  pc  0000006fc64513e02020-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)

首先,找到要害信息Cause: null pointer dereference,然而咱们不晓得产生在具体哪里,所以接下来咱们须要借助addr2line和ndk-stack两款工具来帮助咱们进行剖析。

7.1 addr2line

当初,咱们应用工具addr2line来定位地位。首先,执行如下命令。

/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作者:潇风寒月链接:https://juejin.im/post/6844904190586650632起源:掘金著作权归作者所有。商业转载请分割作者取得受权,非商业转载请注明出处。

其中-e是指定so文件的地位,而后开端的00000000000113e0和00000000000113b8是出错地位的汇编指令地址。

/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

能够看到,是native-lib.cpp的260行出的问题,咱们只须要找到这个地位而后修复这个文件即可。

7.2 ndk-stack

除此之外,还有一种更简略的形式,间接输出命令。

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

开端是so文件的地位,执行完命令后就能够在手机上产生native谬误,而后就能在这个so文件中定位到这个谬误点。

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

能够看到,下面的日志明确指出了是willCrash()办法出的错,它的代码行数是260行。

8,JNI援用

家喻户晓,Java在新创建对象的时候,不须要思考JVM是怎么申请内存的,也不须要在应用完之后去开释内存。而C++不同,须要咱们手动申请和开释内存(new->delete,malloc->free)。在应用JNI时,因为本地代码不能间接通过援用操作JVM外部的数据结构,要进行这些操作必须调用相应的JNI接口间接操作JVM外部的数据内容。咱们不须要关怀JVM中对象的是如何存储的,只须要学习JNI中的三种不同援用即可。

8.1 JNI 部分援用

通常,本地函数中通过NewLocalRef或调用FindClass、NewObject、GetObjectClass、NewCharArray等创立的援用,就是部分援用。部分援用具备如下一些特色:

  • 会阻止GC回收所援用的对象
  • 不能跨线程应用
  • 不在本地函数中跨函数应用
  • 开释: 函数返回后部分援用所援用的对象会被JVM主动开释,也能够调用DeleteLocalRef开释。

通常是在函数中创立并应用的就是部分援用, 部分援用在函数返回之后会主动开释。那么咱们为啥还须要去手动调用DeleteLocalRef进行开释呢?

比方,开了一个for循环,外面一直地创立部分援用,那么这时就必须得应用DeleteLocalRef手动开释内存。不然部分援用会越来越多,最终导致解体(在Android低版本上部分援用表的最大数量有限度,是512个,超过则会解体)。

还有一种状况,本地办法返回一个援用到Java层之后,如果Java层没有对返回的部分援用应用的话,部分援用就会被JVM主动开释。

8.2 JNI 全局援用

全局援用是基于部分援用创立的,应用NewGlobalRef办法创立。全局援用具备如下一些个性:

  • 会阻止GC回收所援用的对象
  • 能够跨办法、跨线程应用
  • JVM不会主动开释,需调用DeleteGlobalRef手动开释

8.3 JNI 弱全局援用

弱全局援用是基于部分援用或者全局援用创立的,应用NewWeakGlobalRef办法创立。弱全局援用具备如下一些个性:

  • 不会阻止GC回收所援用的对象
  • 能够跨办法、跨线程应用
  • 援用不会主动开释,只有在JVM内存不足时才会进行回收而被开释.,还有就是能够调用DeleteWeakGlobalRef手动开释。

参考:
Android Developers NDK 指南 C++ 库反对
JNI/NDK开发指南
Android 内存泄露之jni local reference table overflow