上篇文章讲述了如何生成so/aar/aar的源码包,本章节主要讲述真实开发中jni常用的动态注册Native方法 以及JNI的语法 ,需要一定的C/C++基础.
一.关于头文件和(.c/.cpp)
文件。
1.1 头文件
的摆放位置除了src/main/cpp
,还可以在cpp目录
下新建include目录
统一摆放,如果是简单的jni可能只有1个头文件甚至没有头文件,但是复杂的案例下可能有10个8个,这种情况下放到include比较合适。 1.2 注意,需要在CMakeList.txt中配置路径include_directories
:
javaScript
project(projectforblog VERSION 1.0)
#配置include的路径:CMAKE_CURRENT_SOURCE_DIR是默认的src/main/cpp路径
include_directories("${CMAKE_CURRENT_SOURCE_DIR}/include")
1.3 .c/.cpp文件
就放在cpp目录
里下面就可以了,截图可以看出比文章二的截图多了一个drv.c
,也是丢到cpp目录,但是要注意,这个文件需要在CMakeList.txt进行配置,配置如下:
javaScript
...
add_library(
# so库名字
projectforblog
# 这行代码指定了库的类型为共享库(在Unix系统如Linux中,这会产生一个.so文件;在Windows中,这会产生一个.dll文件)。
SHARED
# 组成projectforblog库文件的路径,多个文件用空格或者,分开都可以
native-lib.cpp drv.c)
文章第四节 最后会补上完整的CMakeList.txt代码
二. JNI 静态注册方法和动态注册方法
Java中Native声明的JNI函数/方法在jni中有两种注册方法,一种是静态
,一种动态
,静态注册就是文章一中提到的命名规则是Java+包名+类名+方法名
的那个例子。
2.1 静态注册:
特点是:命名规则固定,编写简单,但是如果声明类修改了名字或者路径修改,需要对Cpp文件中实现的方法名字进行修改
,不理解这段话的回去看Android Jni开发-生成so库和aar详解(二)-6.So初始化 的图片
2.2 动态注册:
1) 特点是:灵活性高, 更改类名,包名或方法时, 只需对更改模块进行少量修改, 效率高,但是第一次写稍微麻烦,不容易理解。
2)如果确定有动态注册的方法,则JNI_OnLoad
方法是JNI 库的初始化的入口点
,这个方法会指定声明Native方法所在类的路径并进行注册,这个类可能是Java/Kotlin类,但是声明方式都是类似的,System.loadLibrary("solib")调用时就会自动调用JNI_OnLoad函数
:
Java
// native-lib.cpp
// 注册方法要用的声明,c/c++中,代码默认是从上往下读的,所以下面要调用的gMethods[]要提前声明。
static const JNINativeMethod gMethods[] = {
// {"方法名","(罗列所有参数类型)返回类型",(void *) 方法名 }
{"open_file", "()Z", (void *) open_file},
{"close_file", "()Z", (void *) native_close_ak7709},
{"writeStringAndIntData", "(Ljava/lang/String;I)Z", (void *) writeStringAndIntData}
};
JNIEXPORT jint
JNI_OnLoad(JavaVM *vm,void *reserved) {
// Android平台特有的函数,调用复杂,可以进行重命名简化调用。
__android_log_print(ANDROID_LOG_INFO,"native", "Jni_OnLoad");
// 从JavaVM获取JNIEnv,一般使用1.6的版本
JNIEnv *env = NULL;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK)
return -1;
// Native方法声明类的真实路径
jclass clazz = env->FindClass("com/android/solib/JniUtils");
if (!clazz) {
__android_log_print(ANDROID_LOG_INFO,"native","cannot get class: com/android/solib/JniUtils");
return -1;
}
// 注册本地方法,gMethods要先声明
if (env->
RegisterNatives(clazz, gMethods,sizeof(gMethods) / sizeof(gMethods[0]))) {
__android_log_print(ANDROID_LOG_INFO,"native", "register native method failed!\n");
return -1;
}
return JNI_VERSION_1_6;
}
3)这一步的gMethods[]
需要解释下,gMethods中声明方法的格式代码备注也有写,是按照
{"方法名","(罗列所有参数类型)返回类型",(void *) 方法名 }
{"方法名","(所有参数的类型签名)返回值的类型签名",(void *) 方法名 }
的规则写的(类型签名参考 2.2.3 类型签名映射表)。
4)这里涉及到一个JNI语言的知识点,要知道JNI相当于一个中介,Java和C++的数据交互是它负责转换,因此他有自己的语法结构和基本数据类型 ,从而支持jni种调用java数据或者c/c++数据,例如java的int在jni中是jint
,jni给我们提供了若3个映射表
,将java中的类型与jni中的类型进行了一 一映射,其中包括2.2.1基本数据类型映射
,2.2.2引用数据类型映射
,2.2.3 类型签名映射表
。
2.2.1基本数据类型映射表
Java数据类型 | Jni数据类型 | 位大小 |
---|---|---|
boolean | jboolean | 无符号,8 位 |
byte | jbyte | 无符号,8 位 |
char | jchar | 无符号,16 位 |
short | jshort | 有符号,16 位 |
int | jint | 有符号,32 位 |
long | jlong | 有符号,64 位 |
float | jfloat | 32 位 |
double | jdouble | 64 位 |
void | void | N/A |
为了使用方便,特提供以下定义。
#define JNI_FALSE 0
#define JNI_TRUE 1
typedef jint jsize :jsize 整数类型用于描述主要指数和大小:
2.2.2 引用数据类型映射表
Java数据类型 | Jni数据类型 | 备注-引用数据表1 |
---|---|---|
Object | jobject |
包括引用数据表2 jclass jstring jarray |
Java数据类型 | Jni数据类型 | 备注-引用数据表2 |
---|---|---|
java.lang.Class | jclass |
NA |
java.lang.String | jstring |
NA |
Array | jarray |
包括下面的 引用数据表3 |
Java数据类型 | Jni数据类型 | 备注-引用数据表3 |
---|---|---|
object 数组 | jobjectArray |
NA |
boolean 数组 | jbooleanArray |
NA |
byte 数组 | jbyteArray |
NA |
char 数组 | jcharArray |
NA |
short 数组 | jshortArray |
NA |
int 数组 | jintArray |
NA |
long 数组 | jlongArray |
NA |
float 数组 | jfloatArray |
NA |
double 数组 | jdoubleArray |
NA |
2.2.3 类型签名映射表(包含参数和返回值)
类型签名 | Java数据类型 | 备注 |
---|---|---|
V | void | NA |
Z |
boolean |
Z是大写的 |
B | byte | NA |
C | char | NA |
S | short | NA |
J | long | NA |
F | float | NA |
D | double | NA |
I |
int | NA |
[I |
int[] | 所有数组都可以类比,比如[D [F [B 一个[ 代表1维数组,两个[[ 代表 二维数组 |
L fully-qualified-class ; | 全限定的类 | 这个规则看最后两行举例 |
Ljava/lang/String; |
String |
;号不要忘记 |
[Ljava/lang/object; |
Object[] |
第一个字母是大写的L |
上面5个表格是jni和java的各种类型的对应关系,所以再回到上面gMethods[]
的代码,比如:
java
// {"方法名","(所有参数的类型签名)返回值的类型签名",(void *) 方法名 }
// String类型对应Ljava/lang/String; | int类型对应I | 括号是固定的 | Boolean类型对应 Z
{"writeStringAndIntData", "(Ljava/lang/String;I)Z", (void *) writeStringAndIntData},
// String类型对应Ljava/lang/String; | int[]类型对应[I | int对应I| 括号是固定的 | Boolean类型对应 Z
{"writeIntArrayAudioData", "(Ljava/lang/String;[II)Z", (void *) writeIntArrayAudioData},
// String类型对应Ljava/lang/String; | double[[]]类型对应[[D | 括号是固定的 | Boolean类型对应 Z
{"writeTwoDimensionalArrayData", "(Ljava/lang/String;[[D)Z", (void *) writeTwoDimensionalArrayData}
相信到此你应该能理解了什么是类型签名以及他的用途了。
三. JNI 中访问Java代码
3.1 jni类操作
方法名 | 作用 | 备注 |
---|---|---|
jclass DefineClass(JNIEnv *env, jobject loader, const jbyte *buf, jsize bufLen) | 返回 Java 类对象。如果出错则返回 NULL。 |
在本地代码中动态定义一个新的 Java 类,并将其加载到 JVM 中。这意味着可以通过提供类文件的字节码数据来创建一个新的 Java 类 |
jclass FindClass(JNIEnv *env, const char *name); | 返回类对象全名。如果找不到该类,则返回 NULL |
该函数用于查找和加载已存在的类 |
DefineClass
常情况下函数被用于实现自定义的类加载器,以实现动态加载和生成类的功能。这使得在运行时动态地创建类成为可能,这对于某些特定的应用场景非常有用;
FindClass
主要用于获取已存在的 Java 类的引用。
java
// Java端代码
public class DynamicClassLoader {
static {
System.loadLibrary("native-lib");
}
// 调用 native 方法动态加载类
public native void defineAndLoadClass(byte[] classData, String className);
// 调用 native 方法使用已存在的类
public native void useExistingClass(String className);
public static void main(String[] args) {
DynamicClassLoader loader = new DynamicClassLoader();
// 获取类文件的字节码数据
byte[] classData = getClassData();
// 调用 native 方法动态加载并定义类
loader.defineAndLoadClass(classData, "DynamicClass");
// 调用 native 方法使用已存在的类
loader.useExistingClass("java/lang/String");
}
}
C++
// C/C++端代码
#include <jni.h>
JNIEXPORT void JNICALL Java_DynamicClassLoader_defineAndLoadClass(JNIEnv *env, jobject obj, jbyteArray classData, jstring className) {
const char* name = (*env)->GetStringUTFChars(env, className, 0);
jsize len = (*env)->GetArrayLength(env, classData);
jbyte* data = (*env)->GetByteArrayElements(env, classData, 0);
jclass clazz = (*env)->DefineClass(env, name, obj, data, len);
if (clazz == NULL) {
// 类加载失败,抛出异常
jclass exceptionClazz = (*env)->FindClass(env, "java/lang/Exception");
(*env)->ThrowNew(env, exceptionClazz, "Failed to define and load class");
return;
}
// 在这里可以使用 clazz 进行后续操作
}
JNIEXPORT void JNICALL Java_DynamicClassLoader_useExistingClass(JNIEnv *env, jobject obj, jstring className) {
const char* name = (*env)->GetStringUTFChars(env, className, 0);
jclass clazz = (*env)->FindClass(env, name);
if (clazz == NULL) {
// 查找类失败,抛出异常
jclass exceptionClazz = (*env)->FindClass(env, "java/lang/ClassNotFoundException");
(*env)->ThrowNew(env, exceptionClazz, "Class not found");
return;
}
// 在这里可以使用 clazz 进行后续操作
}
3.2 jni 对象操作
方法名 | 作用 | 备注 |
---|---|---|
jclass GetObjectClass(JNIEnv *env, jobject obj) | 返回 Java 类对象 |
obj是java对象 |
jboolean IsInstanceOf(JNIEnv *env, jobject obj, jclass clazz); | 测试对象是否为某个类的实例 |
|
jboolean IsSameObject(JNIEnv *env, jobject ref1, jobject ref2); | 测试两个引用是否引用同一 Java 对象 |
java
// Java端代码
public class JNIDemo {
static {
System.loadLibrary("native-lib");
}
public native void checkObjectClass(Object obj);
public static void main(String[] args) {
JNIDemo demo = new JNIDemo();
String str = new String("Hello, JNI");
demo.checkObjectClass(str);
}
}
cpp
// C/C++端代码
#include <jni.h>
#include <stdio.h>
JNIEXPORT void JNICALL Java_JNIDemo_checkObjectClass(JNIEnv *env, jobject obj, jobject object) {
// 获取 object 的类信息
jclass clazz = (*env)->GetObjectClass(env, object);
// 检查 object 是否为 String 类的实例
jboolean isStringInstance = (*env)->IsInstanceOf(env, object, (*env)->FindClass(env, "java/lang/String"));
// 创建另一个对象,并检查两个对象是否相同
jobject anotherObject = (*env)->NewObject(env, (*env)->FindClass(env, "java/lang/String"), (*env)->GetMethodID(env, (*env)->FindClass(env, "java/lang/String"), "<init>", "()V"));
jboolean isSame = (*env)->IsSameObject(env, object, anotherObject);
if (isStringInstance) {
printf("The object is an instance of String class\n");
} else {
printf("The object is not an instance of String class\n");
}
if (isSame) {
printf("The two objects are the same\n");
} else {
printf("The two objects are not the same\n");
}
}
四个创建Java对象的方法
方法名 | 作用 | 备注 |
---|---|---|
jclass AllocObject(JNIEnv *env, jobject obj) | 返回 Java 对象。如果无法构造该对象,则返回 NULL |
分配新 Java 对象而不调用该对象的任何构造函数 |
jclass NewObject(JNIEnv *env, jobject obj) | 返回 Java 类对象 |
obj是java对象 |
jclass NewObjectA(JNIEnv *env, jobject obj) | 返回 Java 类对象 |
使用数组来传递参数 |
jclass NewObjectV(JNIEnv *env, jobject obj) | 返回 Java 类对象 |
使用变长参数列表 (va_list)来传递参数 |
演示四个方法的用法:
java
public class JniDemo {
static {
System.loadLibrary("jni_demo"); // 加载本地库
}
// 声明本地方法
public native long createPersonWithAllocObject();
public static void main(String[] args) {
JniDemo demo = new JniDemo();
// 调用本地方法创建Person对象并返回指针
long pointer1 = demo.createPersonWithAllocObject();
}
java
package com.example;
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void sayHello() {
System.out.println("Hello, my name is " + name + " and I am " + age + " years old.");
}
}
cpp
// C/C++端代码
#include <jni.h>
#include <stdio.h>
jobject createPersonWithAllocObject(JNIEnv *env) {
jclass personClass = (*env)->FindClass(env, "com/example/Person");
jmethodID constructor = (*env)->GetMethodID(env, personClass, "<init>", "(Ljava/lang/String;I)V");
// AllocObject 分配内存并返回对象
jobject personObject = (*env)->AllocObject(env, personClass);
// NewObject 创建对象的参数
jstring name = (*env)->NewStringUTF(env, "John");
jint age = 30;
// NewObject 创建对象
personObject = (*env)->NewObject(env, personClass, constructor, name, age);
// NewObjectA 建对象的参数
jvalue args[2];
args[0].l = (*env)->NewStringUTF(env, "Emily"); // 参数1: name
args[1].i = 25; // 参数2: age
// NewObjectA 建对象
personObject = (*env)->NewObjectA(env, personClass, constructor, args);
// NewObjectV建对象的参数
va_list args;
va_start(args, env);
jstring name = va_arg(args, jstring);
jint age = va_arg(args, jint);
va_end(args);
// NewObjectV建对象
personObject = (*env)->NewObjectV(env, personClass, constructor, args);
// 调用 sayHello 方法
jmethodID sayHelloMethod = (*env)->GetMethodID(env, personClass, "sayHello", "()V");
(*env)->CallVoidMethod(env, personObject, sayHelloMethod);
}
3.3 jni访问对象的域
方法名 | 作用 |
---|---|
GetFieldID |
假设我们有一个 Java 类含有一个名为 "age" 的 int 类型字段。通过 GetFieldID,我们可以获取到这个字段的 ID ,然后在本地方法中使用这个 ID 来访问 age 字段,而不必每次都进行字段搜索 |
Get<Type>Field |
在本地代码中获取 Java 对象的字段值 |
Set<Type>Field |
在本地代码中为 Java 对象的字段赋值 |
上面的 <Type>
,可以是Object和8种基本数据类型。
JNI 例程名 | 本地类型 | 间隔 | JNI 例程名 | 本地类型 |
---|---|---|---|---|
GetObjectField() | jobject | SetObjectField() | jobject | |
GetBooleanField() | jboolean | SetBooleanField() | jboolean | |
GetByteField() | jbyte | SetByteField() | jbyte | |
GetCharField() | jchar | SetCharField() | jchar | |
GetShortField() | jshort | SetShortField() | jshort | |
GetIntField() | jint | SetIntField() | jint | |
GetLongField() | jlong | SetLongField() | jlong | |
GetFloatField() | jfloat | SetFloatField() | jfloat | |
GetDoubleField() | jdouble | SetDoubleField() | jdouble |
jni访问对象的域-----实际使用demo
java
// Java类
public class Person {
private int age;
public native void updateAge(); // 声明本地方法
}
c++
// C/C++ 本地代码
#include <jni.h>
JNIEXPORT void JNICALL Java_Person_updateAge(JNIEnv *env, jobject obj) {
// 获取Person类
jclass personClass = (*env)->GetObjectClass(env, obj);
// 获取age字段的ID
jfieldID ageFieldID = (*env)->GetFieldID(env, personClass, "age", "I");
// 使用SetIntField设置age字段的值为30
(*env)->SetIntField(env, obj, ageFieldID, 30);
// 使用GetIntField获取age字段的值
jint age = (*env)->GetIntField(env, obj, ageFieldID);
}
java
// Java主程序
public class Main {
static {
System.loadLibrary("person"); // 加载本地库
}
public static void main(String[] args) {
Person person = new Person();
person.updateAge(); // 调用本地方法
System.out.println("New age: " + person.getAge()); // 输出新的年龄值
}
}
3.4 jni 访问java静态域
方法名 | 作用 |
---|---|
GetStaticFieldId | 根据变量名获取target中静态变量的ID |
GetStatic<Type>Field |
根据变量ID获取int静态变量的值,对应的还有byte,boolean,long等 |
SetStatic<Type>Field |
根据变量ID获取int静态变量的值,对应的还有byte,boolean,long等 |
上面的 <Type>
,可以是Object和8种基本数据类型
;比如GetStaticObjectField ,GetStaticIntField ,GetStaticDoubleField ...,和3.3类似。
3.5 jni 访问域和方法
方法名 | 作用 |
---|---|
GetMethodID | 获取类的实例方法的标识符 |
Call<Type>Method |
用于调用一个非静态的 Java 方法 ,并且该方法接受固定数量(0个或更多)的参数 。这个方法会根据方法的返回值类型 返回结果 |
Call<Type>MethodA |
用于调用一个非静态的 Java 方法 ,并且该方法接受可变数量的参数 。这个方法通过传递一个包含所有参数的数组 来调用 Java 方法 |
Call<Type>MethodV |
用于调用一个非静态的 Java 方法 ,并且该方法接受可变数量的参数,类似 Call<type>MethodA 。但是,这个方法是使用变长参数列表 (va_list)来传递参数的 |
上面的 <Type>
可以是Void、Object和8种基本数据类型
;比如CallVoidMethod ,CallObjectMethod ,CallBooleanMethod ... ,和3.3类似,多了一个Void,下面是演示案例:
java
public class Calculator {
public int add(int a, int b) {
return a + b;
}
}
c++
#include <jni.h>
JNIEXPORT jint JNICALL Java_com_example_Calculator_add(JNIEnv *env, jobject obj, jint a, jint b) {
// 获取 Calculator 类
jclass clazz = (*env)->GetObjectClass(env, obj);
// 获取 add 方法的 ID
jmethodID methodID = (*env)->GetMethodID(env, clazz, "add", "(II)I");
// 调用 add 方法
jint result;
// 使用 Call<type>Method 调用 Java 方法
result = (*env)->CallIntMethod(env, obj, methodID, a, b);
// 使用 Call<type>MethodA 调用 Java 方法
jvalue args[2];
args[0].i = a;
args[1].i = b;
result = (*env)->CallIntMethodA(env, obj, methodID, args);
// 使用 Call<type>MethodV 调用 Java 方法
va_list varargs;
va_start(varargs, b);
result = (*env)->CallIntMethodV(env, obj, methodID, varargs);
va_end(varargs);
return result;
}
java
public class Main {
static {
System.loadLibrary("calculator"); // 加载本地库
}
public static void main(String[] args) {
Calculator calculator = new Calculator();
int result = addFromJNI(calculator, 3, 4); // 通过 JNI 调用 add 方法并输出结果
System.out.println("Result: " + result);
}
private static native int addFromJNI(Calculator obj, int a, int b); // JNI 方法声明
}
3.6 jni 调用静态方法
方法名 | 作用 |
---|---|
GetStaticMethodID | 获取指定方法在 Java 类中静态方法的ID |
CallStatic<Type>Method |
用于调用一个静态的 Java 方法 ,并且该方法接受固定数量(0个或更多)的参数 。这个方法会根据方法的返回值类型 返回结果 |
CallStatic<Type>MethodA |
用于调用一个静态的 Java 方法 ,并且该方法接受可变数量的参数 。这个方法通过传递一个包含所有参数的数组 来调用 Java 方法 |
CallStatic<Type>MethodV |
用于调用一个静态的 Java 方法 ,并且该方法接受可变数量的参数,类似 CallStatic<type>MethodA 。但是,这个方法是使用变长参数列表 (va_list)来传递参数的 |
java
public class TestClass {
public static int add(int... numbers) {
int sum = 0;
for (int num : numbers) {
sum += num;
}
return sum;
}
}
c++
#include <jni.h>
JNIEXPORT void JNICALL Java_com_example_TestNativeMethods_callStaticMethod(JNIEnv *env, jobject obj) {
jclass cls = (*env)->FindClass(env, "com/example/TestClass"); // 获取类引用
jmethodID methodId = (*env)->GetStaticMethodID(env, cls, "add", "([I)I"); // 获取静态方法的 ID,注意参数类型是数组
if (methodId == NULL) {
// 处理获取方法 ID 失败的情况
return;
}
// 使用 CallStatic<Type>Method 调用静态方法
jint result1 = (*env)->CallStaticIntMethod(env, cls, methodId, 10, 20); // 直接传入固定数量的参数
// 使用 CallStatic<Type>MethodA 调用静态方法
jintArray args = (*env)->NewIntArray(env, 3);
jint argList[] = {30, 40, 50};
(*env)->SetIntArrayRegion(env, args, 0, 3, argList);
jint result2 = (*env)->CallStaticIntMethodA(env, cls, methodId, args); // 传入可变数量的参数的数组
// 使用 CallStatic<Type>MethodV 调用静态方法
va_list varargs;
va_start(varargs, obj);
jint result3 = (*env)->CallStaticIntMethodV(env, cls, methodId, varargs); // 使用变长参数列表来传递参数
va_end(varargs);
// 分别输出结果
}
java
public class MainActivity extends Activity {
static {
System.loadLibrary("native-lib"); // 加载名为 "native-lib" 的动态链接库
}
// 在onCreate方法中调用callStaticMethod
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
callStaticMethod(); // 在onCreate方法中调用callStaticMethod
}
// 声明一个 native 方法,用于调用动态链接库中的函数
public native void callStaticMethod();
}
3.7 jni 全局及局部引用
方法名 | 作用 | 注释 |
---|---|---|
NewGlobalRef |
创建全局引用 | 全局引用必须调用DeleteGlobalRef 显式释放 |
DeleteGlobalRef |
释放全局对象 | 必须主动释放,否则会导致内存泄漏 |
NewWeakGlobalRef | 创建弱全局引用 | 允许内存不足时对底层 Java 对象进行垃圾回收 |
DeleteWeakGlobalRef | 释放弱全局引用 | 大部分情况下不需要手动调用 ,除非你担心本地分配过多导致内存不足。** |
NewLocalRef | 创建局部引用 | 尽管在本地方法返回到 Java 后会自动释放本地引用,但过多分配本地引用可能会导致 VM 在执行本机方法期间内存不足 |
DeleteLocalRef | 释放局部对象 | 大部分情况下不需要手动调用 ,除非你担心本地分配过多导致内存不足。 |
c++
#include <jni.h>
JNIEXPORT void JNICALL Java_com_example_MyClass_createGlobalRef(JNIEnv *env, jobject obj, jstring str) {
// 创建全局引用
jstring globalRef = (*env)->NewGlobalRef(env, str);
// 使用全局引用
// ...
// 释放全局引用,必须调用
(*env)->DeleteGlobalRef(env, globalRef);
}
JNIEXPORT void JNICALL Java_com_example_MyClass_createWeakGlobalRef(JNIEnv *env, jobject obj, jobject globalObj) {
// 创建弱全局引用
jobject weakGlobalRef = (*env)->NewWeakGlobalRef(env, globalObj);
// 使用弱全局引用
// ...
// 释放弱全局引用,可不调用
(*env)->DeleteWeakGlobalRef(env, weakGlobalRef);
}
JNIEXPORT void JNICALL Java_com_example_MyClass_createLocalRef(JNIEnv *env, jobject obj) {
jclass clazz = (*env)->FindClass(env, "java/lang/String");
// 创建局部引用
jobject localRef = (*env)->NewLocalRef(env, clazz);
// 使用局部引用
// ...
// 释放局部引用,可不调用
(*env)->DeleteLocalRef(env, localRef);
}
3.8 jni还提供了异常处理机制
处理方式跟java一样有两种,要么往上(java层)抛,要么自己捕获处理
方法名 | 返回值 | 作用 |
---|---|---|
Throw | 成功时返回 0,失败时返回负数。 | 往上(java层)抛出异常 |
ThrowNew | 成功时返回 0,失败时返回负数。 | 往上(java层)抛出自定义异常 |
ExceptionOccurred | 判断是否有异常发生 | |
ExceptionClear | 清除异常 |
在下面示例中,Java层的NativeLib
类包含了一个native方法nativeMethod
,该方法声明了可能会抛出CustomException
异常。在JNI层的nativeMethod函数中,我们模拟了一个条件不符合的情况,并使用了ThrowNew
来抛出一个Java异常,之后又使用了ExceptionOccurred
来检查是否有异常发生,并使用了Throw
来手动抛出一个自定义异常。在Java层,我们通过try-catch
块捕获并处理了这些异常
java
public class NativeLib {
static {
System.loadLibrary("native-lib");
}
private native void nativeMethod() throws CustomException;
public static void main(String[] args) {
NativeLib lib = new NativeLib();
try {
lib.nativeMethod();
} catch (CustomException e) {
System.out.println("Caught CustomException: " + e.getMessage());
}
}
}
java
public class CustomException extends Exception {
public CustomException() {
super();
}
public CustomException(String message) {
super(message);
}
}
c++
#include <jni.h>
JNIEXPORT void JNICALL Java_NativeLib_nativeMethod(JNIEnv *env, jobject obj) {
jclass customExceptionClass;
jmethodID constructor;
jthrowable exc;
// 模拟条件不符合的情况,通过Throw抛出一个Java异常
if (/* 某种条件 */) {
(*env)->ThrowNew(env, (*env)->FindClass(env, "CustomException"), "Native method threw a CustomException");
return;
}
// 模拟检查是否有异常发生,并进行相应的处理
exc = (*env)->ExceptionOccurred(env);
if (exc) {
// 处理异常的逻辑
(*env)->ExceptionClear(env); // 清除异常
}
// 模拟手动抛出一个Java异常
customExceptionClass = (*env)->FindClass(env, "CustomException");
if (customExceptionClass == NULL) {
// 处理异常:找不到自定义异常类
return;
}
constructor = (*env)->GetMethodID(env, customExceptionClass, "<init>", "(Ljava/lang/String;)V");
if (constructor == NULL) {
// 处理异常:找不到构造函数
return;
}
jstring message = (*env)->NewStringUTF(env, "Custom exception message");
jobject exceptionObj = (*env)->NewObject(env, customExceptionClass, constructor, message);
if (exceptionObj == NULL) {
// 处理异常:无法创建自定义异常对象
return;
}
(*env)->Throw(env, (jthrowable)exceptionObj); // 抛出自定义异常
}
3.9 字符串和数组
---常用函数
3.9.1 字符串相关函数
方法名 | 返回值 | 备注 |
---|---|---|
NewString | Java 字符串对象或 NULL。 | 利用 Unicode字符数组 构造新的 java.lang.String 对象。 |
GetStringLength | 返回 Java 字符串的长度(Unicode 字符数) | 如果是处理 Java 层的 Unicode 字符串 ,则可以使用 GetStringLength |
GetStringChars | 指向 Unicode 字符串的指针,如果操作失败,则返回 NULL。 | 获取字符串字符指针 |
ReleaseStringChars | 通知虚拟机平台相关代码无需再访问 chars。参数 chars 是一个指针,可通过 GetStringChars() 从 string 获得 | 释放指针 |
NewStringUTF | Java 字符串对象或 NULL。 | 利用 UTF-8 字符数组 构造新 java.lang.String 对象。 |
GetStringUTFLength | 以字节为单位返回字符串的 UTF-8 长度 | 如果是将 Java 字符串转换为 C 字符串并使用 UTF-8 编码 ,则可以使用 GetStringUTFLength 来获得所需的缓冲区大小 |
GetStringUTFChars | 返回指向字符串的 UTF-8 字符数组的指针。 | 该数组在被 ReleaseStringUTFChars() 释放前将一直有效。 |
ReleaseStringUTFChars | 通知虚拟机平台相关代码无需再访问 utf。utf 参数是一个指针,可利用 GetStringUTFChars() 从 string 获得。 | 释放指针 |
NewString,GetStringLength,GetStringChars,ReleaseStringChars,NewStringUTF,GetStringUTFLength,GetStringUTFChars,ReleaseStringUTFChars |
3.9.2 数组相关函数
方法名 | 返回值 | 备注 |
---|---|---|
NewObjectArray | Java 数组对象或者NULL | 构造新的数组,它将保存类 elementClass 中的对象。所有元素初始值均设为 initialElement。 |
GetArrayLength |
数组的长度 | 经常是和GetObjectArrayElement一起用 |
GetObjectArrayElement |
Java 对象 | 返回 Object 数组的元素,越界会抛ArrayIndexOutOfBoundsException |
SetObjectArrayElement | Void | 设置 Object 数组的元素,越界会抛ArrayIndexOutOfBoundsException |
New<PrimitiveType>Array |
Java 数组或 NULL。 | <PrimitiveType> 可以替换为Java的八种基本数据类型 ,例如NewDoubleArray,NewBooleanArray |
NativeType Get<PrimitiveType>ArrayElements (JNIEnv *env, ArrayType array, jboolean *isCopy); |
返回指向数组元素的指针或 NULL。 | <PrimitiveType> 可以替换为Java的八种基本数据类型 ,例如NewDoubleArray,NewBooleanArray,此外如果isCopy不是NULL,在复制完成后被复制为TRUE,未复制为false |
void Release<PrimitiveType>ArrayElements (JNIEnv *env, ArrayType array, NativeType *elems, jint mode); |
释放指针 ,通过mode 可把对 elems 的修改复制回基本类型数组 。 |
mode三种值: 0 复制回内容并释放 elems 缓冲区; JNI_COMMIT 复制回内容但不释放 elems 缓冲区; JNI_ABORT 释放缓冲区但不复制回变化。一般用0 |
四. 真实案例代码展示
// 1.JniUtils.kt
kotlin
package com.android.solib
import androidx.annotation.IntRange
/**
* 调用jni的入口
*/
object JniUtils {
init {
// projectforblog是在CMakeLists.txt中定义的
System.loadLibrary("projectforblog")
}
/**
* 声明native静态注册
*/
external fun stringFromJNI(): String
/**
* 动态注册
* 打开文件
* @return
*/
external fun open_file(): Boolean
/**
* 动态注册
* 关闭文件
* @return
*/
external fun close_file(): Boolean
/**
* 动态注册
* 单独提供个Delay数组所属类型使用
* @param name
* @param paramIntBuf default: 0, valide range:0 ~ max , should handle by app
* @return
*/
external fun writeStringAndIntData(
name: String?,
@IntRange(from = 0) paramIntBuf: Int
): Boolean
/**
* 动态注册
* @param name
* @param int[]
* @param size
* @return boolean
*/
external fun writeIntArrayAudioData(
name: String?,
paramIntBuf: IntArray?,
size: Int): Boolean
/**
* 动态注册
* @description 参数有二维数组
* @param name
* @param paramIntBuf double[][]类型
* @return
*/
external fun writeTwoDimensionalArrayData(
name: String?,
paramDoubleBuf: Array<DoubleArray>?
): Boolean
}
// 入口文件 2.native-lib.cpp
c++
#include <jni.h>
#include <string>
#include <android/log.h>
#include "api.h"
#include <vector>
// 静态注册
extern "C" JNIEXPORT jstring
Java_com_android_solib_JniUtils_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
// 数组内类型用 ak7709_Mixer_In_param 结构体成员是1个数组,数组成员是结构体,结构体成员是2个double
std::vector<std::string> mixerArray = {
"IN1","IN2","IN3",
};
// 数组内类型用 ak7709_Delay1_param 结构体 ,三个int成员
std::vector<std::string> delayArray = {
"Delay1","Delay2","Delay3","Delay4"
};
// 数组内类型用 ak7709_GEQ_param 结构体,成员是6个double
std::vector<std::string> geqArray = {
"GEQ1","GEQ2","GEQ3","GEQ4"
};
// 数组内类型用 ak7709_Fader_param 结构体,成员是4个double
std::vector<std::string> faderArray = {
"fader1","fader2","fader3","fader4"
};
extern "C" jboolean
native_open_file(JNIEnv *env, jobject thiz) {
// 打开文件,这个方法的具体执行是在drv.c
open_ak7709();
return true;
}
extern "C" jboolean
native_close_file(JNIEnv *env, jobject thiz) {
// 打开文件,这个方法的具体执行是在drv.c
close_ak7709();
return true;
}
// 第三版,写入Delay数组所属类型的 只需要1个int数据,2个是默认值
extern "C" jboolean
writeStringAndIntData(JNIEnv *env, jobject thiz,jstring name,jint tap) {
// 读取的文件,返回文件地址指针,可以用来写入读取数据。
const char *readName = env->GetStringUTFChars(name, NULL);
// 这个结构体是c中需要的数据源
struct ak7709_Delay1_param bean;
bean.ofs = 0x00;// default value,without input
bean.max = 200;// default: 200, valid range: 1-2000,
bean.tap1 = tap;// default: 0, valide range:0 ~ max , should handle by app
// 这是drv.h声明,drv.c实现的方法
jboolean result = write_ak7709_param(readName, &bean, sizeof(bean));
__android_log_print(ANDROID_LOG_INFO,"native", "Int类型数组结果,%d",result);
// 释放指针
env->ReleaseStringUTFChars(name, readName);
return result;
}
extern "C" jboolean writeIntArrayAudioData(JNIEnv *env, jobject thiz,jstring name, jintArray paramIntBuf,jint size) {
//读取的文件名字
const char *readName = env->GetStringUTFChars(name, NULL);
jboolean isIntType = false;
// 创建一个名为 name 的常量引用,其类型将由初始化列表中的元素类型推断出来,并且不能被修改
for(const auto & name : delayArray) {
if(name == readName) {
isIntType = true;
break;
}
}
// 必须是int数组,而且数组的长度必须是1,传到底层是有3个参数,但是有2个参数不开放
if(isIntType && size == 1) {
// 获取int[]数组的指针,myParamIntArray[0 ... n]就和java数组一样用了
jint *myParamIntArray = env->GetIntArrayElements(paramIntBuf, NULL);
struct ak7709_Delay1_param bean;
bean.ofs = 0x00;// default value,without input
bean.max = 200;// default: 200, valid range: 1-2000,
bean.tap1 = myParamIntArray[0];// default: 0, valide range:0 ~ max , should handle by app
jboolean result = write_ak7709_param(readName, &bean, size * 4);
__android_log_print(ANDROID_LOG_INFO,"native", "Int类型数组结果,%d",result);
// 释放字符指针
env->ReleaseStringUTFChars(name, readName);
// 释放int数组指针
env->ReleaseIntArrayElements(paramIntBuf, myParamIntArray, 0);
return result;
} else {
__android_log_print(ANDROID_LOG_INFO,"native", "调用函数类型错误或数组长度不对");
// 释放字符指针
env->ReleaseStringUTFChars(name, readName);
return false;
}
}
/**
* 自定义的内部方法,给writeTwoDimensionalArrayData调用。
* @param env
* @param clazz
* @param array
* @return
*/
ak7709_Mixer_In_param getMixerItemData(JNIEnv *env, jobject clazz,jdoubleArray array) {
// 获取double[]数组指针
jdouble* jArray = env->GetDoubleArrayElements(array, NULL);
ak7709_Mixer_In_param bean;
bean.gain = jArray[0];
bean.invert = jArray[1];
env->ReleaseDoubleArrayElements(array, jArray, 0);
return bean;
}
/**
* jobjectArray是二维数组
*/
extern "C" jboolean writeTwoDimensionalArrayData(JNIEnv *env, jobject clazz,jstring name,jobjectArray paramDoubleBuf) {
//读取的文件名字
const char *readName = env->GetStringUTFChars(name, NULL);
// 获取二维数组长度 jsize就是jint的重定义类型,一般用于长度,大小等地方,比如容易理解
jsize len = (*env).GetArrayLength( paramDoubleBuf);
// 写入底层是否成功
jboolean result = false;
if(len == 2) {
ak7709_Mixer_In2_param mixerBean2;
jdoubleArray subJDoubleArrayFirst = (jdoubleArray) (*env).GetObjectArrayElement( paramDoubleBuf, 0);
jdouble *subArrayFirst = env->GetDoubleArrayElements(subJDoubleArrayFirst, NULL);
mixerBean2.in1.gain = subArrayFirst[0];
mixerBean2.in1.invert = subArrayFirst[1];
jdoubleArray subJDoubleArraySecond = (jdoubleArray) (*env).GetObjectArrayElement( paramDoubleBuf, 1);
jdouble *subArraySecond = env->GetDoubleArrayElements(subJDoubleArraySecond, NULL);
mixerBean2.in2.gain = subArraySecond[0];
mixerBean2.in2.invert = subArraySecond[1];
result = write_ak7709_param(readName, &mixerBean2, 4 * 8);
// 释放内存
env->ReleaseDoubleArrayElements(subJDoubleArrayFirst,subArrayFirst,0);
env->ReleaseDoubleArrayElements(subJDoubleArraySecond,subArraySecond,0);
} else if(len == 3) {
ak7709_Mixer_In3_param mixerBean3 = {
getMixerItemData(env, clazz,(jdoubleArray) (*env).GetObjectArrayElement( paramDoubleBuf, 0)),
getMixerItemData(env, clazz,(jdoubleArray) (*env).GetObjectArrayElement( paramDoubleBuf, 1)),
getMixerItemData(env, clazz,(jdoubleArray) (*env).GetObjectArrayElement( paramDoubleBuf, 2))
};
// 释放指针
result = write_ak7709_param(readName, &mixerBean3, 6 * 8);
} else if(len == 4) {
ak7709_Mixer_In4_param mixerBean4 = {
getMixerItemData(env, clazz,(jdoubleArray) (*env).GetObjectArrayElement( paramDoubleBuf, 0)),
getMixerItemData(env, clazz,(jdoubleArray) (*env).GetObjectArrayElement( paramDoubleBuf, 1)),
getMixerItemData(env, clazz,(jdoubleArray) (*env).GetObjectArrayElement( paramDoubleBuf, 2)),
getMixerItemData(env, clazz,(jdoubleArray) (*env).GetObjectArrayElement( paramDoubleBuf, 3))
};
result = write_ak7709_param(readName, &mixerBean4, 8 * 8);
}
// 释放指针
env->ReleaseStringUTFChars(name, readName);
env->DeleteLocalRef( paramDoubleBuf); //释放jobjectArray数组
return result;
}
// 注册方法要用的声明 {方法名,(罗列所有参数类型)返回类型 , 固定写法(void *)方法名 }
static const JNINativeMethod gMethods[] = {
{"open_file", "()Z", (void *) native_open_file},
{"close_file", "()Z", (void *) native_close_file},
{"writeStringAndIntData", "(Ljava/lang/String;I)Z", (void *) writeStringAndIntData},
{"writeIntArrayAudioData", "(Ljava/lang/String;[II)Z", (void *) writeIntArrayAudioData},
{"writeTwoDimensionalArrayData", "(Ljava/lang/String;[[D)Z", (void *) writeTwoDimensionalArrayData}
};
JNIEXPORT jint
JNI_OnLoad(JavaVM *vm,void *reserved) {
__android_log_print(ANDROID_LOG_INFO,"native", "Jni_OnLoad");
JNIEnv *env = NULL;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) //从JavaVM获取JNIEnv,一般使用1.6的版本
return -1;
// 这个路径必须对应真实路径
jclass clazz = env->FindClass("com/android/solib/JniUtils");
if (!clazz) {
__android_log_print(ANDROID_LOG_INFO,
"native",
"cannot get class: com/android/solib/JniUtils");
return -1;
}
if (env->
RegisterNatives(clazz, gMethods,sizeof(gMethods) / sizeof(gMethods[0]))) {
__android_log_print(ANDROID_LOG_INFO,
"native", "register native method failed!\n");
return -1;
}
return JNI_VERSION_1_6;
}
// 3.CMakeList.txt
JavaScript
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.22.1)
# Declares and names the project.
project(projectforblog VERSION 1.0)
#配置include的路径:CMAKE_CURRENT_SOURCE_DIR是默认的src/main/cpp路径
include_directories("${CMAKE_CURRENT_SOURCE_DIR}/include")
add_library(
# so库名字
projectforblog
# 这行代码指定了库的类型为共享库(在Unix系统如Linux中,这会产生一个.so文件;在Windows中,这会产生一个.dll文件)。
SHARED
# 组成projectforblog库文件的路径,多个文件用空格或者,分开都可以
native-lib.cpp drv.c)
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log)
target_link_libraries( # Specifies the target library.
projectforblog
# Links the target library to the log library
# included in the NDK.
${log-lib})
drv.c
和include目录
的头文件是底层的文件读写操作,属于业务和驱动代码,就不发出来了。
5.MainActivity.kt
kotlin
package com.android.projectforblog
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.android.projectforblog.databinding.ActivityMainBinding
import com.android.solib.JniUtils
import com.android.solib.JniUtils.stringFromJNI
class MainActivity : AppCompatActivity() {
private var binding: ActivityMainBinding? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding!!.root)
val tv = binding!!.sampleText
// native方法调用
tv.text = stringFromJNI()
// 打开文件
val openAudioFlag = JniUtils.open_file()
val openResult = if(openAudioFlag) "成功" else "失败"
Log.d("haiphon","打开$openResult")
val writeResult = JniUtils.writeStringAndIntData("Delay1",1)
Log.d("haiphon","写入$writeResult")
// 关闭文件
JniUtils.close_file()
}
}
五.总结
1.本文总耗时3天,差点写不下去了,幸好最后还是写到了这;
2.篇幅较长,由于把JNI的语法和函数方面的写的相对详细,方便大家理解,说明函数的同时也写了使用案例,如果大伙觉得太罗嗦,可以收藏起来以后直接当api用也行。
3.最后,如果满意的话请大家帮忙点个赞哈。