JNI引用类型数组操作
如果在阅读本文的时候,你发现有不懂的操作,而我又没有详细解释的,可以从下面几篇文章中寻找答案
准备工作
在讲解之前呢,需要做一些准备工作。
首先需要一个Java引用类型的类Person.java
,这个类将被用做创建数组
package com.uni.ndkdemo;
public class Person {
private String mName;
public Person(String name) {
mName = name;
}
public void sayHello() {
System.out.println("Hello, " + mName + "!");
}
}
然后得准备一个native
入口的类
package com.uni.ndkdemo;
public class ArrayTest {
static {
System.loadLibrary("array_jni");
}
public native void sayHello(Person[] persons);
public static void main(String[] args) {
ArrayTest arrayTest = new ArrayTest();
Person persons[] = new Person[2];
persons[0] = new Person("Jay Chow");
persons[1] = new Person("Stephen Chow");
arrayTest.sayHello(persons);
}
}
ArrayTest.java
的main()
方法中创建一个了Person
数组,并给每个元素赋值,然后调用了sayHello()
这个native
方法把这Person
数组传入JNI层。
最后,通过函数动态注册,需要在JNI层实现对应的函数,假设这个函数如下
static void com_uni_ndkdemo_ArrayTest_sayHello(JNIEnv *env, jobject thiz, jobjectArray objectArray)
{
}
我将在这个函数中来讲解JNI如何操作引用类型数组。
GetObjectArrayElement
jobject GetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index);
GetObjectArrayElement
函数获取数组array
在索引index
下的元素。
IsInstanceOf
jboolean IsInstanceOf(JNIEnv *env, jobject obj, jclass clazz);
IsInstanceOf
函数用于判断对象obj
是否是clazz
类的实例对象。
可以利用这个函数来判断数组元素的类型,这样就可以避免调用了错的方法。
实战
static void com_uni_ndkdemo_ArrayTest_sayHello(JNIEnv *env, jobject thiz, jobjectArray objectArray)
{
// 1. 获取数组的长度
jsize length = env->GetArrayLength(objectArray);
// 2. 获取Person类的Class对象
jclass clazz_Person = env->FindClass("com/uni/ndkdemo/Person");
if (clazz_Person == NULL)
{
return;
}
// 3. 获取Person的sayHello方法字段
jmethodID methodID_sayHello = env->GetMethodID(clazz_Person, "sayHello", "()V");
if (methodID_sayHello == NULL)
{
return;
}
// 4. 循环调用每个Person对象的sayHello()方法
for (int i = 0; i < length; i++)
{
// 获取引用类型数组的对象
jobject element = env->GetObjectArrayElement(objectArray, i);
// 判断数组元素是否是Person类对象
if (env->IsInstanceOf(element, clazz_Person))
{
// 调用Person对象的sayHello()方法
env->CallVoidMethod(element, methodID_sayHello);
}
}
}
首先通过第二步和第三步来获取Person
类的sayHello()
方法的jmethodID
,然后调用GetObjectArrayElement
来获取数组的元素,最后通过IsInstanceOf
判断数组元素是Person
类对象后,就调用了它的sayHello
方法。
NewObjectArray
前面所讲的都是处理从Java层传入JNI层的引用类型数组,当然,也可以在JNI层创建引用类型数组,并返回给Java层,这就要用到NewObjectArray
函数
jobjectArray NewObjectArray(JNIEnv *env, jsize length,
jclass elementClass, jobject initialElement);
NewObjectArray
会根据参数elementClass
的类型,创建一个长度为length
的数组。
如果你指定了第四个参数initialElement
,那么将会用第四个参数初始化数组的所有元素。如果指定initialElement
为NULL
,那么数组所有元素为NULL
。
SetObjectArrayElement
void SetObjectArrayElement(JNIEnv *env, jobjectArray array,
jsize index, jobject value);
SetObjectArrayElement
函数是为数组array
,设置索引index
下的元素的值value
。
NewObjectArray
可以在创建数组的时候,用参数jobject initialElement
给数组每个元素赋初值。SetObjectArrayElement
函数可以用参数jobject value
给数组元素设置值。那么问题来了,这个用于赋值的对象如何创建呢?可以使用 NewObject
/NewObjectA
/NewObjectV
,或者 AllocObject
函数。
NewObject & NewObjectA & NewObjectV
jobject NewObject(JNIEnv *env, jclass clazz,
jmethodID methodID, ...);
jobject NewObjectA(JNIEnv *env, jclass clazz,
jmethodID methodID, const jvalue *args);
jobject NewObjectV(JNIEnv *env, jclass clazz,
jmethodID methodID, va_list args);
这三个函数的区别在于传入参数的形式不一样而已。
参数clazz
代表Java类的Class
对象,可以通过FindClass
函数来获取。
参数methodID
代表Java类的构造函数,需要通过GetMethodID
来获取,不过调用GetMethodID
函数时,传入的函数名参数一定要为<init>
,而且传入函数签名参数的返回值一定要为V
。这个可能说的有点抽象,不过可以从后面的例子中看出如何使用。
AllocObject
jobject AllocObject(JNIEnv *env, jclass clazz);
AllocObject
函数非常有意思,它会为clazz
类的对象分配内存,而不用调用clazz
所指定类的任何构造函数,并返回一个指向这个对象的引用。
AllocObject
函数只是为对象分配内存,但是并没有给对象的变量赋值,因此需要我们手动对给对象的变量进行赋值。而前面讲的NewObject
函数需要指定调用哪个构造函数,并且如果构造函数带有参数,还得在调用的时候传入相应的参数。
例子
现在需要先准备一个Java的native
方法,返回一个Person
数组
public class ArrayTest {
static {
System.loadLibrary("array_jni");
}
public native Person[] getNativePersons(int length);
public static void main(String[] args) {
for (int i = 0; i < nativePersons.length; i++) {
nativePersons[i].sayHello();
}
}
}
然后到JNI层去实现
static const int NAME_SIZE = 2;
static const char *names[NAME_SIZE] = {"周星驰", "周杰伦"};
static jobjectArray getPersons(JNIEnv *env, jobject thiz, jint length)
{
// 1. 找到Person类的Class对象
jclass clazz_Person = env->FindClass("com/umx/ndkdemo/Person");
if (clazz_Person == NULL) {
return NULL;
}
// 2. 找到Person类的构造函数的jmethodID
// 注意,这里找的是带有String参数的构造函数
jmethodID methodId_Person_constructor = env->GetMethodID(clazz_Person, "<init>",
"(Ljava/lang/String;)V");
if (methodId_Person_constructor == NULL) {
return NULL;
}
// 3. 创建一个Person数组,每个元素为NULL
jobjectArray array = env->NewObjectArray(length, clazz_Person, NULL);
// 4. 循环为数组中的每个元素赋值
for (int i = 0; i < length; i++) {
const char *name = names[i % NAME_SIZE];
jobject person = NULL;
if (i == length - 1) {
// 为Person对象分配内存
person = env->AllocObject(clazz_Person);
jmethodID methodID_setName = env->GetMethodID(clazz_Person, "setName",
"(Ljava/lang/String;)V");
// 调用setName()方法给创建的Person对象的mName变量赋值
env->CallVoidMethod(person, methodID_setName, env->NewStringUTF(name));
} else {
// 调用Person(String name)构造函数创建Person对象
person = env->NewObject(clazz_Person, methodId_Person_constructor,
env->NewStringUTF(name));
}
// 给数组元素设置值
env->SetObjectArrayElement(array, i, person);
// 释放局部变量
env->DeleteLocalRef(person);
}
return array;
}
第三步调用NewObjectArray
来创建Person
数组,但是由于最后一个参数为NULL
,因此数组元素的值都为NULL
。那么就需要第四步来为数组每个元素赋值。
在第四步的实现中,如果是数组中最后一个元素,那么就调用AllocObject
函数来为Person
对象分配内存,但是这个对象还没有数据,必须通过调用setName
方法来设置mName
的值。而数组中其它元素都是调用Person(String name)
这个构造函数来创建对象的,所以在调用NewObject
的时候得传入一个参数。
对象创建好了之后就需要为调用SetObjectArrayElement
来为每个元素赋值。
如果这个实现中还有不懂的地方,请参考文章开头前指定的文章。
结束
JNI处理引用类型的数组要比处理基本类型数组要复杂得多,因为要涉及到很多的方方面面,但是这些方方面面都在前面的文章中讲过。所有,如果你看这篇文章有困惑,不妨去前面的文章中寻找答案。
Java的String
类也是一个引用类型,但是这个引用类型是用数组实现的,因此比较特殊。在下一篇文章中,我将会来讲解JNI如何来操作字符串的。
转载自:https://juejin.cn/post/6844903891633455118