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配置。
<code class="javascript">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,代码如下。
<code class="javascript"># 设置Cmake最小版本 cmake_minimum_required(VERSION 3.4.1) # 编译library add_library( # 设置library名称 native-lib # 设置library模式 # SHARED模式会编译so文件,STATIC模式不会编译 SHARED # 设置原生代码门路 src/main/cpp/native-lib.cpp ) # 定位library find_library( # library名称 log-lib # 将library门路存储为一个变量,能够在其余中央用这个变量援用NDK库 # 在这里设置变量名称 log ) # 关联library target_link_libraries( # 关联的library native-lib # 关联native-lib和log-lib ${log-lib} )
对于CMake的更多常识,能够查看CMake官网手册。
官网示例
默认创立Android NDK工程时,Android提供了一个简略的JNI交互示例,返回一个字符串给Java层,办法名的格局为:Java_包名_类名_办法名
。首先,咱们看一下native-lib.cpp的代码。
<code class="javascript">#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()); }
而后,咱们在看一下Android的MainActivity.java 的代码。
<code class="javascript">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开发流程
- 编写java类,申明了native办法;
- 编写native代码;
- 将native代码编译成so文件;
- 在java类中引入so库,调用native办法;
2,native办法命名
<code class="javascript">extern "C" JNIEXPORT void JNICALL Java_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办法,如下所示。
<code class="javascript">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类型 | 有无合乎 | 字长 |
---|---|---|---|
boolean | jboolean | 无符号 | 8字节 |
byte | jbyte | 有符号 | 8字节 |
char | jchar | 无符号 | 16字节 |
short | jshort | 有符号 | 16字节 |
int | jint | 有符号 | 32字节 |
long | jlong | 有符号 | 64字节 |
float | jfloat | 有符号 | 32字节 |
double | jdouble | 有符号 | 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中能够间接拜访,如下所示。
<code class="javascript">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根类,如下所示。
<code class="javascript">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 字符串操作的示例
<code class="javascript"> //调用 String result = operateString("待操作的字符串"); Log.d("xfhy", result); //定义 public native String operateString(String str);
而后在C中进行实现,代码如下。
<code class="javascript">extern "C" JNIEXPORT jstring JNICALL Java_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 其余字符串操作函数
- GetStringChars和ReleaseStringChars:这对函数和Get/ReleaseStringUTFChars函数性能相似,用于获取和开释的字符串是以Unicode格局编码的。
- GetStringLength:获取Unicode字符串(jstring)的长度。 UTF-8编码的字符串是以0结尾,而Unicode的不是,所以这里须要独自辨别开。
- 「GetStringUTFLength」: 获取UTF-8编码字符串的长度,就是获取C/C++默认编码字符串的长度.还能够应用规范C函数「strlen」来获取其长度。
- strcat: 拼接字符串,规范C函数。如
strcat(buff, "xfhy");
将xfhy增加到buff的开端。 - GetStringCritical和ReleaseStringCritical: 为了减少间接传回指向Java字符串的指针的可能性(而不是拷贝).在这2个函数之间的区域,是相对不能调用其余JNI函数或者让线程阻塞的native函数.否则JVM可能死锁. 如果有一个字符串的内容特地大,比方1M,且只须要读取外面的内容打印进去,此时比拟适宜用该对函数,可间接返回源字符串的指针。
- 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数组求和的例子,代码如下。
<code class="javascript">//MainActivity.java public native int sumArray(int[] array);
<code class="javascript">extern "C" JNIEXPORT jint JNICALL Java_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层数组中并进行求和。
接下来,咱们来看另一种求和形式,代码如下。
<code class="javascript">extern "C" JNIEXPORT jint JNICALL Java_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层应用。
<code class="javascript">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]); } }
<code class="javascript">extern "C" JNIEXPORT jobjectArray JNICALL Java_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; }
接下来,咱们来剖析下代码。
- 首先,是利用FindClass函数找到java层int[]对象的class,这个class是须要传入NewObjectArray创建对象数组的。调用NewObjectArray函数之后,即可创立一个对象数组,大小是size,元素类型是后面获取到的class。
- 进入for循环构建size个int数组,构建int数组须要应用NewIntArray函数。能够看到我构建了一个长期的buff数组,而后大小是轻易设置的,这里是为了示例,其实能够用malloc动静申请空间,省得申请100个空间,可能太大或者太小了。整buff数组次要是拿来给生成进去的jintArray赋值的,因为jintArray是Java的数据结构,咱native不能间接操作,得调用SetIntArrayRegion函数,将buff数组的值复制到jintArray数组中。
- 而后调用SetObjectArrayElement函数设置jobjectArray数组中某个索引处的数据,这里将生成的jintArray设置进去。
- 最初须要将for外面生成的jintArray及时移除援用。创立的jintArray是一个JNI部分援用,如果部分援用太多的话,会造成JNI援用表溢出。
6,Native调Java办法
相熟JVM的都应该晓得,在JVM中运行一个Java程序时,会先将运行时须要用到的所有相干class文件加载到JVM中,并按需加载,进步性能和节约内存。当咱们调用一个类的静态方法之前,JVM会先判断该类是否曾经加载,如果没有被ClassLoader加载到JVM中,会去classpath门路下查找该类。找到了则加载该类,没有找到则报ClassNotFoundException异样。
6.1 Native调用Java静态方法
首先,咱们编写一个MyJNIClass.java类,代码如下。
<code class="javascript">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()办法不仅有入参,还有返参,如下所示。
<code class="javascript">extern "C" JNIEXPORT void JNICALL Java_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静态方法还是比较简单的,次要会经验以下几个步骤。
- 首先,调用FindClass函数传入Class描述符(Java类的全类名,这里在AS中输出MyJNIClass时会有提醒补全,间接enter即可补全),找到该类并失去jclass类型。
- 而后,通过GetStaticMethodID找到该办法的id,传入办法签名,失去jmethodID类型的援用。
- 构建入参,而后调用CallStaticObjectMethod去调用Java类外面的静态方法,而后传入参数,返回的间接就是Java层返回的数据。其实,这里的CallStaticObjectMethod是调用的援用类型的静态方法,与之类似的还有:CallStaticVoidMethod(无返参),CallStaticIntMethod(返参是Int),CallStaticFloatMethod等。
- 移除部分援用。
6.2 Native调用Java实例办法
接下来,咱们来看一下在Native层创立Java实例并调用该实例的办法,大抵上是和下面调用静态方法差不多的。首先,咱们批改下cpp文件的代码,如下所示。
<code class="javascript">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"); //获取构造方法的办法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实例办法的步骤如下:
- Native调用Java实例办法。
- 获取构造方法的id,获取须要调用办法的id。其中获取构造方法时,办法名称固定写法就是
<init>
,而后前面是办法签名。 - 应用NewObject()函数构建一个Java对象。
- 调用Java对象的setAge和getAge办法,获取返回值,打印后果。
- 删除援用。
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信息的命令,如下所示。
<code class="javascript">adb shell cat /proc/cpuinfo
在正式介绍两款调试工具之前,咱们能够先写好解体的native代码不便咱们查看成果。首先,咱们修复native-lib.cpp外面的代码,如下所示。
<code class="javascript">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("解体前"); willCrash(); //前面的代码是执行不到的,因为解体了 LOGI("解体后"); printf("oooo"); }
下面的这段代码是很显著的空指针异样,运行后谬误日志如下。
<code class="javascript">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)
首先,找到要害信息Cause: null pointer dereference
,然而咱们不晓得产生在具体哪里,所以接下来咱们须要借助addr2line和ndk-stack两款工具来帮助咱们进行剖析。
7.1 addr2line
当初,咱们应用工具addr2line来定位地位。首先,执行如下命令。
<code class="javascript">/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是出错地位的汇编指令地址。
<code class="javascript">/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
除此之外,还有一种更简略的形式,间接输出命令。
<code class="javascript">adb logcat | ndk-stack -sym /Users/xzh/development/AllInOne/app/build/intermediates/cmake/debug/obj/arm64-v8a
开端是so文件的地位,执行完命令后就能够在手机上产生native谬误,而后就能在这个so文件中定位到这个谬误点。
<code class="javascript">********** 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