Android NDK开发:JNI实战篇

紧接上篇:Android NDK开发:JNI基础篇 | cfanr,这篇主要介绍 JNI Native 层调用Java 层的代码(涉及JNI 数据类型映射和描述符的使用)和如何动态注册 JNI。

1. Hello World NDK

在开始实战练习前,你需要先大致了解运行一个 Hello World 的项目大概需要做什么,有哪些配置以及配置的具体意思。 Android Studio(2.2以上版本)提供两种方式编译原生库:CMake( 默认方式) 和 ndk-build。对于初学者可以先了解 CMake 的方式,另外,对于本文可以暂时不用了解 so 库如何编译和使用。

一个 Hello World 的 NDK 项目很简单,按照流程新建一个 native 库工程就可以,由于太简单,而且网上也有很多教程,这里就没必要浪费时间再用图文介绍了。详细操作方法,可以参考这篇文章,AS2.2使用CMake方式进行JNI/NDK开发-于连林- CSDN博客

列出项目中涉及 NDK 的内容或配置几点需要注意的地方:

build.gradle 文件,注意两个 externalNativeBuild {}的配置

local.properties 文件会多了 ndk 路径的配置

MainActivity 调用 so 库

另外,还需要回顾上篇 JNI 基础篇-静态注册也提到 JNI 的函数命名规则:JNIEXPORT 返回值 JNICALL Java_全路径类名_方法名_参数签名(JNIEnv* , jclass, 其它参数);其中第二个参数,当 java native 方法是 static 时,为 jclass,当为非静态方法时,为 jobject,为了简单起见,下面的例子 JNI 函数都标记extern "C",函数名就不需要写参数签名了

2. JNI 函数访问 Java 对象的变量

注:以下练习中 Java native 方法都是非静态的 步骤:

2.1 访问某个变量,并通过某个方法对其处理后返回

native方法定义和调用

C++层:

输出结果:

由于 jclass 也是继承 jobject,所以使用 GetIntField 时不要混淆两个参数

2.2 访问一个 static 变量,并对其修改

native方法定义和调用

C++代码:

输出结果:

需要注意的是,获取 java 静态变量,都是调用 JNI 相应静态的函数,不能调用非静态的,同时留意传入的参数是 jclass,而不是 jobject

2.3 访问一个 private 变量,并对其修改

native方法定义和调用

C++:

输出结果:

3. JNI 函数调用 Java 对象的方法

步骤:(和访问 Java 对象的变量有点类型)

3.1 调用 Java 公有方法

native方法定义和调用

C++

结果:

调用 java private 方法也是类似, Java 的访问域修饰符对 C++无效

3.2 调用 Java 静态方法

native方法定义和调用

C++

输出结果: MainActivity: 调用静态方法:getHeight() = 170

注意调用的静态方法要一致。

3.3 调用 Java 父类方法

native方法定义和调用

C++

注意两点不同的地方,

4. Java 方法传递参数给 JNI 函数

native 方法既可以传递基本类型参数给 JNI(可以不经过转换直接使用),也可以传递复杂的类型(需要转换为 C/C++ 的数据结构才能使用),如数组,String 或自定义的类等。 基础类型,这里就不举例子了,详细可以看 GitHub 上的源码: AndroidTrainingDemo/JNISample 要用到的 JNI 函数:

4.1 数组参数的传递

计算整型数组参数的和 native方法定义和调用

C++

输出结果: MainActivity: intArrayMethod: 39

4.2 自定义对象参数的传递

Person 定义,native方法定义和调用

C++:

输出结果 MainActivity: objectMethod: Person :{ name: cfanr, age: 21}

注意:传递对象时,获取的 jclass 是获取该参数对象的 jobject 获取,而不是第二个参数(定义该 native 方法的对象)取;

4.3 自定义对象的集合参数的传递

native方法定义和调用

C++

输出结果:

复杂的集合参数也是需要通过获取集合的 class 和对应的方法来调用实现的

5. JNI 函数的字符串处理

其中 isCopy 是取值为JNI_TRUE和JNI_FALSE(或者1,0),值为JNI_TRUE,表示返回JVM内部源字符串的一份拷贝,并为新产生的字符串分配内存空间。如果值为JNI_FALSE,表示返回JVM内部源字符串的指针,意味着可以通过指针修改源字符串的内容,不推荐这么做,因为这样做就打破了Java字符串不能修改的规定;Java默认使用Unicode编码,而C/C++默认使用UTF编码,所以在本地代码中操作字符串的时候,必须使用合适的JNI函数把jstring转换成C风格的字符串 - UTF-8字符:const char* GetStringUTFChars(jstring string, jboolean* isCopy) - Unicode字符:const jchar* GetStringChars(jstring string, jboolean* isCopy)

代码示例就不写了,其他详细可参考: JNI开发之旅(9)JNI函数字符串处理 - 猫的阁楼 - CSDN博客 JNI/NDK开发指南(四)——字符串处理 - 技术改变生活- CSDN博客

6. 动态注册 JNI

学了上面的练习,发现静态注册的方式还是挺麻烦的,生成的 JNI 函数名太长,文件、类名、变量或方法重构时,需要重新修改头文件或 C/C++ 内容代码(而且还是各个函数都要修改,没有一个统一的地方),动态注册 JNI 的方法就可以解决这个问题。

由上篇回顾下,Android NDK开发:JNI基础篇 | cfanr 动态注册 JNI 的原理:直接告诉 native 方法其在JNI 中对应函数的指针。通过使用 JNINativeMethod 结构来保存 Java native 方法和 JNI 函数关联关系,步骤:

代码实例: native 方法和调用:

C++动态注册 JNI 代码:

输出结果:

上面代码涉及到 JNI 调用 Android 的 Log,只需要引入#include "android/log.h"头文件和对函数别名命名即可。其他具体说明见上面代码。

实际开发中可以采取动态和静态注册结合的方式,写一个Java 的 native 方法完成调用动态注册的代码,大概代码如下:

C++

7. 小结

虽然都是按照网上的例子做的练习记录,但还是遇到不少小问题的,不过只要仔细查找,也比较容易发现问题的所在,以前觉得 JNI 挺难懂的,但这次练习下来,觉得 JNI 也只不过是一套语法规则而已,按照规则去实现代码也不算特别难,当然这只是 JNI 的一小部分内容,JNI 还有很多内容,如反射、异常处理、多线程、NIO 等。虽然这次练习比较简单,但建议还是自己亲自敲一遍代码,在练习中发现问题,并解决,以后遇到同类型的问题也比较容易解决。

注意一些报错的问题:

本文完整代码可以到 GitHub 查看源码: AndroidTrainingDemo/JNISample

参考资料: 专栏:JNI开发之旅 -猫的阁楼- CSDN博客 Andoid NDK编程1- 动态注册native函数 // Coding Life Android Stuido Ndk-Jni 开发:Jni中打印log信息 - 简书

原文