JNI NDK入门详解,2024年最新安卓屏幕适配面试

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

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)

GetStringUTFChars(jstring string, jboolean* isCopy)参数说明:

  • string : jstring,Java传递给native代码的字符串指针

  • isCopy : 一般情况下传NULL. 取值是JNI_TRUEJNI_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函数. eg:strcat(buff, "xfhy"); 将xfhy添加到buff的末尾.

  5. GetStringCritical和ReleaseStringCritical: 为了增加直接传回指向Java字符串的指针的可能性(而不是拷贝).在这2个函数之间的区域,是绝对不能调用其他JNI函数或者让线程阻塞的native函数.否则JVM可能死锁. 如果有一个字符串的内容特别大,比如1M,且只需要读取里面的内容打印出来,此时比较适合用该对函数,可直接返回源字符串的指针.

  6. GetStringRegion和GetStringUTFRegion: 获取Unicode和UTF-8字符串中指定范围的内容(eg: 只需要1-3索引处的字符串),这对函数会将源字符串复制到一个预先分配的缓冲区(自己定义的char数组)内.

extern “C”

JNIEXPORT jstring JNICALL

Java_com_xfhy_jnifirst_MainActivity_operateString(JNIEnv *env, jobject thiz, jstring str) {

//方式2 用GetStringUTFRegion方法将JVM中的字符串拷贝到C/C++的缓冲区(数组)中

//获取Unicode字符串长度

int len = env->GetStringLength(str);

char buff[128];

env->GetStringUTFRegion(str, 0, len, buff);

LOGI(“-------------- %s”, buff);

//自动转为Unicode

return env->NewStringUTF(buff);

}

GetStringUTFRegion会进行越界检查,越界会抛StringIndexOutOfBoundsException异常.GetStringUTFRegion其实和GetStringUTFChars有点相似,但是GetStringUTFRegion内部不会分配内存,不会抛出内存溢出异常. 由于其内部没有分配内存,所以也没有类似Release这样的函数来释放资源.

4.2.6 字符串 小结
  1. Java字符串转C/C++字符串: 使用GetStringUTFChars函数,必须调用ReleaseStringUTFChars释放内存

  2. 创建Java层需要的Unicode字符串,使用NewStringUTF函数

  3. 获取C/C++字符串长度,使用GetStringUTFLength或者strlen函数

  4. 对于小字符串,GetStringRegion和GetStringUTFRegion这2个函数是最佳选择,因为缓冲区数组可以被编译器提取分配,不会产生内存溢出的异常.当只需要处理字符串的部分数据时,也还是不错.它们提供了开始索引和子字符串长度值,复制的消耗也是非常小

  5. 获取Unicode字符串和长度,使用GetStringChars和GetStringLength函数

5. 数组操作


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层数组中,然后求和,看起来还是so easy的.

下面还看另一种求和方式:

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则是返回原始数组指针.

这里简单小结一下: 推荐使用Get/SetArrayRegion函数来操作数组元素是效率最高的.自己动态申请数组,自己操作,免得影响Java层.

5.2 对象数组

对象数组中的元素是一个类的实例或其他数组的引用,不能直接访问Java传递给JNI层的数组.

操作对象数组稍显复杂,下面举一个例子,在native层创建一个二维数组,且赋值并返回给Java层使用.(ps: 第二维是int[],它属于对象)

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_xfhy_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引用表溢出.

ps: 在JNI中,只要是jobject的子类就属于引用变量,会占用引用表的空间. 而基础数据类型jint,jfloat,jboolean等是不会占用引用表空间的,不需要释放.

6. native调Java方法


前面已经叙述了如何在Java中调用native方法,这里将带大家看看native如何调用Java方法.

ps: 在JVM中,运行一个Java程序时,会先将运行时需要用到的所有相关class文件加载到JVM中,并按需加载,提高性能和节约内存.当我们调用一个类的静态方法之前,JVM会先判断该类是否已经加载,如果没有被ClassLoader加载到JVM中,会去classpath路径下查找该类.找到了则加载该类,没有找到则报ClassNotFoundException异常.

6.1 native调用Java静态方法

为了演示代码简洁,方便理清核心内容,已删除各种NULL判断.

我先写一个MyJNIClass.java类

public class MyJNIClass {

public int age = 18;

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_xfhy_allinone_jni_CallMethodActivity_callJavaStaticMethod(JNIEnv *env, jobject thiz) {

//调用某个类的static方法

//JVM使用一个类时,是需要先判断这个类是否被加载了,如果没被加载则还需要加载一下才能使用

//1. 从classpath路径下搜索MyJNIClass这个类,并返回该类的Class对象

jclass clazz = env->FindClass(“com/xfhy/allinone/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(“我是xfhy”);

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的代码其实是短小精悍,这里面涉及的知识点挺多.

  1. 首先是调用FindClass函数,传入Class描述符(Java类的全类名,这里在AS中输入MyJNIClass时会有提示补全,直接enter即可补全),找到该类,得到jclass类型.

  2. 然后通过GetStaticMethodID找到该方法的id,传入方法签名,得到jmethodID类型的引用(存储方法的引用).(这里输入getDes时,AS也会有补全功能,按enter直接把签名带出来了,真tm方便). 这里先使用AS的自动补全功能把方法签名带出来,后面会详细说这个方法签名是什么.

  3. 构建入参,然后调用CallStaticObjectMethod去调用Java类里面的静态方法,然后传入参数,返回的直接就是Java层返回的数据. 其实这里的CallStaticObjectMethod是调用的引用类型的静态方法,与之相似的还有: CallStaticVoidMethod(无返参),CallStaticIntMethod(返参是Int),CallStaticFloatMethod,CallStaticShortMethod.他们的用法是一致的.

  4. 移除局部引用

ps: 函数结束后,JVM会自动释放所有局部引用变量所占的内存空间. 这里还是手动释放一下比较安全,因为在JVM中维护着一个引用表,用于存储局部和全局引用变量. 经测试发现在Android低版本(我测试的是Android 4.1)上,这个表的最大存储空间是512个引用,当超出这个数量时直接崩溃.当我在高版本,比如小米 8(安卓10)上,这个引用个数可以达到100000也不会崩溃,只是会卡顿一下.可能是硬件比当年更牛逼了,默认值也跟着改了.

下面是引用表溢出时所报的错误(要得到这个错还挺难的,得找一个比较老旧的设备才行):

E/dalvikvm: JNI ERROR (app bug): local reference table overflow (max=512)

E/dalvikvm: Failed adding to JNI local ref table (has 512 entries)

E/dalvikvm: VM aborting

A/libc: Fatal signal 11 (SIGSEGV) at 0xdeadd00d (code=1), thread 2561 (m.xfhy.allinone)

下面是我拿来还原错误的native代码,这里刚好是513个就会异常,低于513就不会.说明早年的一些设备确实是以512作为最大限度.当我们APP需要兼容老设备(一般都需要吧,哈哈)的话,这肯定是需要注意的点.

extern “C”

JNIEXPORT jobject JNICALL

Java_com_xfhy_allinone_jni_CallMethodActivity_testMaxQuote(JNIEnv *env, jobject thiz) {

//测试Android虚拟机引用表最大限度数量

jclass clazz = env->FindClass(“java/util/ArrayList”);

jmethodID constrId = env->GetMethodID(clazz, “”, “(I)V”);

jmethodID addId = env->GetMethodID(clazz, “add”, “(ILjava/lang/Object;)V”);

jobject arrayList = env->NewObject(clazz, constrId, 513);

for (int i = 0; i < 513; ++i) {

jstring test_str = env->NewStringUTF(“test”);

env->CallVoidMethod(arrayList, addId, 0, test_str);

//这里应该删除的 局部引用

//env->DeleteLocalRef(test_str);

}

return arrayList;

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
img

文末

面试:如果不准备充分的面试,完全是浪费时间,更是对自己的不负责!

不管怎么样,不论是什么样的大小面试,要想不被面试官虐的不要不要的,只有刷爆面试题题做好全面的准备,当然除了这个还需要在平时把自己的基础打扎实,这样不论面试官怎么样一个知识点里往死里凿,你也能应付如流啊

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
img
5654)]
[外链图片转存中…(img-SLZUsGXq-1712738915655)]
[外链图片转存中…(img-0bxaFubE-1712738915655)]
[外链图片转存中…(img-8zROy4Hx-1712738915655)]
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
[外链图片转存中…(img-ZvXB5FAk-1712738915656)]

文末

面试:如果不准备充分的面试,完全是浪费时间,更是对自己的不负责!

不管怎么样,不论是什么样的大小面试,要想不被面试官虐的不要不要的,只有刷爆面试题题做好全面的准备,当然除了这个还需要在平时把自己的基础打扎实,这样不论面试官怎么样一个知识点里往死里凿,你也能应付如流啊

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值