JNI (Java Native Interface英文缩写),译为Java本地接口。是Java众多开发技术中的一门技术,意在利用本地代码,为Java程序提供更高效、更灵活的拓展。尽管Java一向以其良好的跨平台性而著称,但真正的跨平台非C/C++莫属,由于当前世上90%的系统都是基于C/C++编写的。同时,Java的跨平台是以牺牲效率换来对多种平台的兼容性,于是JNI就是这种跨平台的主流实现方式之一。javascript
总之,JNI是一门技术,是Java 与C/C++ 沟通的一门技术。首先,来回顾下Android的系统架构图。
咱们来简单介绍下每一层的做用。html
因为Android 系统是基础Linux 内核构建的,因此Linux是Android系统的基础。事实上,Android 的硬件驱动、进程管理、内存管理、网络管理都是在这一层。java
硬件抽象层(Hardware Abstraction Layer缩写),硬件抽象层主要为上层提供标准显示界面,并向更高级别的 Java API 框架提供显示设备硬件功能。HAL 包含多个库模块,其中每一个模块都为特定类型的硬件组件实现一个界面,例如相机或蓝牙模块。当框架 API 要求访问设备硬件时,Android 系统将为该硬件组件加载对应的库模块。linux
Android 5.0(API 21)以前,使用的是Dalvik虚拟机,以后被ART所取代。ART是Android操做系统的运行环境,经过运行虚拟机来执行dex文件。其中,dex文件是专为安卓设计的的字节码格式,Android打包和运行的就是dex文件,而Android toolchain(一种编译工具)能够将Java代码编译为dex字节码格式,转化过程以下图。
如上所示,Jack就是一种编译工具链,能够将Java 源代码编译为 DEX 字节码,使其可在 Android 平台上运行。android
不少核心 Android 系统组件和服务都是使用C 和 C++ 编写的,为了方便开发者调用这些原生库功能,Android的Framework提供了调用相应的API。例如,您能够经过 Android 框架的 Java OpenGL API 访问 OpenGL ES,以支持在应用中绘制和操做 2D 和 3D 图形。shell
Android平台最经常使用的组件和服务都在这一层,是每一个Android开发者必须熟悉和掌握的一层,是应用开发的基础。数组
Android系统App,如电子邮件、短信、日历、互联网浏览和联系人等系统应用。咱们能够像调用Java API Framework层同样直接调用系统的App。网络
接下来咱们看一下如何编写Android JNI ,以及须要的流程。数据结构
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的相关环境。依次选择【Preferences...】->【Android SDK】下载配置NDK,以下所示。
而后,新建一个Native C++工程,以下所示。
而后勾选【Include C++ support】选项,点击【下一步】,到达【Customize C++ Support】设置页,以下所示。
而后,点击【Finish】按钮便可。
打开新建的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) # 编译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的代码。
#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 的代码。
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(); }
extern "C" JNIEXPORT void JNICALL Java_com_xfhy_jnifirst_MainActivity_callJavaMethod(JNIEnv *env, jobject thiz) { }
函数命名规则: 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类型 | 有无符合 | 字长 |
---|---|---|---|
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 |
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 */
若是使用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。
JNI会把Java中全部对象当作一个C指针传递到本地方法中,这个指针指向JVM内部数据结构,而内部的数据结构在内存中的存储方式是不可见的.只能从JNIEnv指针指向的函数表中选择合适的JNI函数来操做JVM中的数据结构。
好比native访问java.lang.String 对应的JNI类型jstring时,不能像访问基本数据类型那样使用,由于它是一个Java的引用类型,因此在本地代码中只能经过相似GetStringUTFChars这样的JNI函数来访问字符串的内容。
//调用 String result = operateString("待操做的字符串"); Log.d("xfhy", result); //定义 public native String operateString(String str);
而后在C中进行实现,代码以下。
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); }
在上面的代码中,operateString函数接收一个jstring类型的参数str,jstring是指向JVM内部的一个字符串,不能直接使用。首先,须要将jstring转为C风格的字符串类型char*后才能使用,这里必须使用合适的JNI函数来访问JVM内部的字符串数据结构。
GetStringUTFChars(jstring string, jboolean* isCopy)对应的参数的含义以下:
Java中默认是使用Unicode编码,C/C++默认使用UTF编码,因此在native层与java层进行字符串交流的时候须要进行编码转换。GetStringUTFChars就恰好能够把jstring指针(指向JVM内部的Unicode字符序列)的字符串转换成一个UTF-8格式的C字符串。
在使用GetStringUTFChars的时候,返回的值可能为NULL,这时须要处理一下,不然继续往下面走的话,使用这个字符串的时候会出现问题.由于调用这个方法时,是拷贝,JVM为新生成的字符串分配内存空间,当内存空间不够分配的时候就会致使调用失败。调用失败就会返回NULL,并抛出OutOfMemoryError。JNI遇到未决的异常不会改变程序的运行流程,仍是会继续往下走。
native不像Java,咱们须要手动释放申请的内存空间。GetStringUTFChars调用时会新申请一块空间用来装拷贝出来的字符串,这个字符串用来方便native代码访问和修改之类的。既然有内存分配,那么就必须手动释放,释放方法是ReleaseStringUTFChars。能够看到和GetStringUTFChars是一一对应配对的。
使用NewStringUTF函数能够构建出一个jstring,须要传入一个char *类型的C字符串。它会构建一个新的java.lang.String字符串对象,而且会自动转换成Unicode编码。若是JVM不能为构造java.lang.String分配足够的内存,则会抛出一个OutOfMemoryError异常并返回NULL。
strcat(buff, "xfhy");
将xfhy添加到buff的末尾。一般,GetStringUTFRegion会进行越界检查,越界会抛StringIndexOutOfBoundsException异常。GetStringUTFRegion其实和GetStringUTFChars有点类似,可是GetStringUTFRegion内部不会分配内存,不会抛出内存溢出异常。因为其内部没有分配内存,因此也没有相似Release这样的函数来释放资源。
基本类型数组就是JNI中的基本数据类型组成的数组,能够直接访问。例如,下面是int数组求和的例子,代码以下。
//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) { //数组求和 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 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则是返回原始数组指针。
对象数组中的元素是一个类的实例或其余数组的引用,不能直接访问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 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; }
接下来,咱们来分析下代码。
熟悉JVM的都应该知道,在JVM中运行一个Java程序时,会先将运行时须要用到的全部相关class文件加载到JVM中,并按需加载,提升性能和节约内存。当咱们调用一个类的静态方法以前,JVM会先判断该类是否已经加载,若是没有被ClassLoader加载到JVM中,会去classpath路径下查找该类。找到了则加载该类,没有找到则报ClassNotFoundException异常。
首先,咱们编写一个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 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静态方法仍是比较简单的,主要会经历如下几个步骤。
接下来,咱们来看一下在Native层建立Java实例并调用该实例的方法,大体上是和上面调用静态方法差很少的。首先,咱们修改下cpp文件的代码,以下所示。
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实例方法的步骤以下:
<init>
,而后后面是方法签名。因为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 JNICALL Java_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+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两款工具来协助咱们进行分析。
如今,咱们使用工具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行出的问题,咱们只须要找到这个位置而后修复这个文件便可。
除此以外,还有一种更简单的方式,直接输入命令。
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行。
众所周知,Java在新建立对象的时候,不须要考虑JVM是怎么申请内存的,也不须要在使用完以后去释放内存。而C++不一样,须要咱们手动申请和释放内存(new->delete,malloc->free)。在使用JNI时,因为本地代码不能直接经过引用操做JVM内部的数据结构,要进行这些操做必须调用相应的JNI接口间接操做JVM内部的数据内容。咱们不须要关心JVM中对象的是如何存储的,只须要学习JNI中的三种不一样引用便可。
一般,本地函数中经过NewLocalRef或调用FindClass、NewObject、GetObjectClass、NewCharArray等建立的引用,就是局部引用。局部引用具备以下一些特征:
一般是在函数中建立并使用的就是局部引用, 局部引用在函数返回以后会自动释放。那么咱们为啥还须要去手动调用DeleteLocalRef进行释放呢?
好比,开了一个for循环,里面不断地建立局部引用,那么这时就必须得使用DeleteLocalRef手动释放内存。否则局部引用会愈来愈多,最终致使崩溃(在Android低版本上局部引用表的最大数量有限制,是512个,超过则会崩溃)。
还有一种状况,本地方法返回一个引用到Java层以后,若是Java层没有对返回的局部引用使用的话,局部引用就会被JVM自动释放。
全局引用是基于局部引用建立的,使用NewGlobalRef方法建立。全局引用具备以下一些特性:
弱全局引用是基于局部引用或者全局引用建立的,使用NewWeakGlobalRef方法建立。弱全局引用具备以下一些特性:
参考:
Android Developers NDK 指南 C++ 库支持
JNI/NDK开发指南
Android 内存泄露之jni local reference table overflow