2024年安卓最新Android-NDK开发入门(1),2024年最新安卓 高级 面试

面试复习笔记:

这份资料我从春招开始,就会将各博客、论坛。网站上等优质的Android开发中高级面试题收集起来,然后全网寻找最优的解答方案。每一道面试题都是百分百的大厂面经真题+最优解答。包知识脉络 + 诸多细节。
节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

《960页Android开发笔记》

《1307页Android开发面试宝典》

包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。

《507页Android开发相关源码解析》

只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。

真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

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 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 其他字符串操作函数
  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.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则是返回原始数组指针。

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

接下来,我们来分析下代码。

  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 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静态方法还是比较简单的,主要会经历以下几个步骤。

  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 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, “”, “()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 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两款工具来协助我们进行分析。

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中的三种不同引用即可。

文末

那么对于想坚持程序员这行的真的就一点希望都没有吗?
其实不然,在互联网的大浪淘沙之下,留下的永远是最优秀的,我们考虑的不是哪个行业差哪个行业难,就逃避掉这些,无论哪个行业,都会有他的问题,但是无论哪个行业都会有站在最顶端的那群人。我们要做的就是努力提升自己,让自己站在最顶端,学历不够那就去读,知识不够那就去学。人之所以为人,不就是有解决问题的能力吗?挡住自己的由于只有自己。
Android希望=技能+面试

  • 技能
  • 面试技巧+面试题

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

远是最优秀的,我们考虑的不是哪个行业差哪个行业难,就逃避掉这些,无论哪个行业,都会有他的问题,但是无论哪个行业都会有站在最顶端的那群人。我们要做的就是努力提升自己,让自己站在最顶端,学历不够那就去读,知识不够那就去学。人之所以为人,不就是有解决问题的能力吗?挡住自己的由于只有自己。
Android希望=技能+面试

  • 技能
    [外链图片转存中…(img-Tsi6fUsG-1715803607974)]
  • 面试技巧+面试题
    [外链图片转存中…(img-u6cI1ZKC-1715803607974)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 30
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值