Android Jni开发-生成so库和aar详解(三)

上篇文章讲述了如何生成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 包括引用数据表2jclass 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})
  1. drv.cinclude目录的头文件是底层的文件读写操作,属于业务和驱动代码,就不发出来了。

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.最后,如果满意的话请大家帮忙点个赞哈。

相关推荐
yngsqq4 分钟前
c#使用高版本8.0步骤
java·前端·c#
Myli_ing39 分钟前
考研倒计时-配色+1
前端·javascript·考研
余道各努力,千里自同风41 分钟前
前端 vue 如何区分开发环境
前端·javascript·vue.js
软件小伟1 小时前
Vue3+element-plus 实现中英文切换(Vue-i18n组件的使用)
前端·javascript·vue.js
醉の虾1 小时前
Vue3 使用v-for 渲染列表数据后更新
前端·javascript·vue.js
张小小大智慧1 小时前
TypeScript 的发展与基本语法
前端·javascript·typescript
hummhumm1 小时前
第 22 章 - Go语言 测试与基准测试
java·大数据·开发语言·前端·python·golang·log4j
asleep7012 小时前
第8章利用CSS制作导航菜单
前端·css
hummhumm2 小时前
第 28 章 - Go语言 Web 开发入门
java·开发语言·前端·python·sql·golang·前端框架
幼儿园的小霸王2 小时前
通过socket设置版本更新提示
前端·vue.js·webpack·typescript·前端框架·anti-design-vue