1. 操作 int 数组
Java 层有一个 int 数组,使用 JNI 怎么打印该数组呢?MainActivity 代码如下:
public class MainActivity extends AppCompatActivity {
private static final String TAG = MainActivity.class.getSimpleName();
static {
System.loadLibrary("jnitest");
}
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// int 数组
int[] intArr = new int[]{2, 0, 0, 2, 8, 6};
testIntArray(intArr);
for(int i = 0; i < intArr.length; i++){
Log.d(TAG, "Java 层 int 数组第 " + i + " 个元素的值是:" + intArr[i]);
}
}
private native void testIntArray(int[] intArr);
}
自动生成的 JNI 函数的参数是 int_arr(jintArray 类型),对应 Java 层 testIntArray() 中的 int[] 类型的参数 intArr。在 JNI 函数中不能直接通过 int_arr 拿到数组中的元素,需要结合 env 的 GetIntArrayElements() 函数,该函数返回的是 jint* 类型,这样我们就可以通过指针位移来拿到数组中的元素了,JNI 代码如下:
#include <jni.h>
#include <string>
// 日志输出
#include <android/log.h>
#define TAG "jni"
// __VA_ARGS__ 代表可变参数
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__);
extern "C"
JNIEXPORT void JNICALL
Java_com_example_jnitest_MainActivity_testIntArray(JNIEnv *env, jobject thiz, jintArray int_arr) {
// 获取 int_arr 的长度
jsize arrLen = env->GetArrayLength(int_arr);
// 将数组转换为 jint* 指针
jint* intP = env->GetIntArrayElements(int_arr, nullptr);
for(int i = 0; i < arrLen; i++){
LOGD("Native 层 int 数组第 %d 个元素的值是:%d", i, *(intP + i))
}
env->ReleaseIntArrayElements(int_arr, intP, JNI_OK);
}
其中 GetIntArrayElements() 函数的第二个参数是 jboolean* 类型的,一般直接传 nullptr 即可。
除非你需要检查该数组是否被复制。如果数组被复制,JNI 会在操作后将该指针的值设置为JNI_TRUE (1);如果没有复制,设置为JNI_FALSE (0),用法如下:
jboolean isCopy; jint* intP = env->GetIntArrayElements(int_arr, &isCopy); if (isCopy == JNI_TRUE) { LOGD("JNI 对数组进行了复制操作。"); } else { LOGD("JNI 直接返回了数组指针。"); }
最后需要使用 ReleaseIntArrayElements() 释放内存,ReleaseIntArrayElements() 的第 3 个参数决定了释放操作的行为,通常有以下 3 种选项:
- JNI_COMMIT(1) :将修改提交回 Java 层,但不释放本地数组指针。适用于你需要多次操作 Native 数组,想暂时提交修改但仍需保留数组访问权限时。
- JNI_ABORT(2) :放弃本地数组的更改,释放本地数组指针,但不会将更改提交回 Java 层。适用于你只想读取数组内容或操作过程中决定丢弃修改时。
- JNI_OK(0) :将对 Native 数组的修改提交回 Java 层,同时释放 Native 数组指针。适用于你修改了数组内容并希望这些更改同步到 Java 层时,该选项最常用。
从这里可以看出,传 JNI_OK 可以将 Native 数组的修改提交回 Java 层。如果我们把数组中的每个元素加 20,代码如下:
for(int i = 0; i < arrLen; i++){
*(intP + i) = *(intP + i) + 20;
LOGD("Native 层 int 数组第 %d 个元素的值是:%d", i, *(intP + i))
}
重新运行程序,打印如下:
jni com.example.jnitest D Native 层 int 数组第 0 个元素的值是:22
jni com.example.jnitest D Native 层 int 数组第 1 个元素的值是:20
jni com.example.jnitest D Native 层 int 数组第 2 个元素的值是:20
jni com.example.jnitest D Native 层 int 数组第 3 个元素的值是:22
jni com.example.jnitest D Native 层 int 数组第 4 个元素的值是:28
jni com.example.jnitest D Native 层 int 数组第 5 个元素的值是:26
MainActivity com.example.jnitest D Java 层 int 数组第 0 个元素的值是:22
MainActivity com.example.jnitest D Java 层 int 数组第 1 个元素的值是:20
MainActivity com.example.jnitest D Java 层 int 数组第 2 个元素的值是:20
MainActivity com.example.jnitest D Java 层 int 数组第 3 个元素的值是:22
MainActivity com.example.jnitest D Java 层 int 数组第 4 个元素的值是:28
MainActivity com.example.jnitest D Java 层 int 数组第 5 个元素的值是:26
可以看到 Native 层的修改同步到了 Java 层,如果使用 JNI_ABORT 则不会同步到 Java 层。
在函数内部创建的局部变量在函数执行结束时会自动释放内存,但是执行释放操作可以避免大量局部引用堆积,避免本地引用表溢出,更符合 JNI 的编码规范。
2. 操作 String 数组
把 int 数组换成 String 数组,又该如何调用呢? MainActivity 代码如下:
public class MainActivity extends AppCompatActivity {
private static final String TAG = MainActivity.class.getSimpleName();
static {
System.loadLibrary("jnitest");
}
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
String[] strs = {"Jim", "Brian", "Lily", "Peter"};
testStringArray(strs);
for(int i = 0; i < strs.length; i++){
Log.d(TAG, "Java 层 String 数组第 " + i + " 个元素的值是:" + strs[i]);
}
}
private native void testStringArray(String[] strs);
}
自动生成的 JNI 函数的参数是 jobjectArray 类型,对应于 testStringArray() 函数中的 String[] 类型。这时候我想模仿上面的方式,发现 env 中并没有 GetStringArrayElements() ,但是有 GetObjectArrayElement() 函数,该函数还需要传一个 int 类型的数组下标,其返回值为 jobject 类型,jstring 是 jobject 的子类,可以通过强转的方式转换为 jstring。拿到 jstring 之后还不能直接打印,需要把 jstring 转换为 char * 类型,最后记得调用对应的函数释放内存,JNI 代码如下:
extern "C"
JNIEXPORT void JNICALL
Java_com_example_jnitest_MainActivity_testStringArray(JNIEnv *env, jobject thiz,
jobjectArray strs) {
// 获取 strs 的长度
jsize arrLen = env->GetArrayLength(strs);
for (jsize i = 0; i < arrLen; i++) {
// 遍历字符串数组
jstring jstr = (jstring) env->GetObjectArrayElement(strs, i);
// 将 jstring 类型转换成 char* 类型
const char* str = env->GetStringUTFChars(jstr, nullptr);
if (str != nullptr) {
LOGD("Native 层 String 数组第 %d 个元素的值是:%s", i, str);
env->ReleaseStringUTFChars(jstr, str);
}
// 释放局部引用
env->DeleteLocalRef(jstr);
}
}
运行后打印如下:
jni com.example.jnitest D Native 层 String 数组第 0 个元素的值是:Jim
jni com.example.jnitest D Native 层 String 数组第 1 个元素的值是:Brian
jni com.example.jnitest D Native 层 String 数组第 2 个元素的值是:Lily
jni com.example.jnitest D Native 层 String 数组第 3 个元素的值是:Peter
MainActivity com.example.jnitest D Java 层 String 数组第 0 个元素的值是:Jim
MainActivity com.example.jnitest D Java 层 String 数组第 1 个元素的值是:Brian
MainActivity com.example.jnitest D Java 层 String 数组第 2 个元素的值是:Lily
MainActivity com.example.jnitest D Java 层 String 数组第 3 个元素的值是:Peter
如果想通过 JNI 把字符串数组中的元素都修改成 Kevin,代码如下:
extern "C"
JNIEXPORT void JNICALL
Java_com_example_jnitest_MainActivity_testStringArray(JNIEnv *env, jobject thiz,
jobjectArray strs) {
// 获取 strs 的长度
jsize arrLen = env->GetArrayLength(strs);
jstring newStr = env->NewStringUTF("Kevin");
for (jsize i = 0; i < arrLen; i++) {
// 将数组中第 i 个位置的元素替换为 Kevin
env->SetObjectArrayElement(strs, i, newStr);
}
for (jsize i = 0; i < arrLen; i++) {
jstring jstr = (jstring) env->GetObjectArrayElement(strs, i);
// 将 jstring 类型转换成 char* 类型
const char* str = env->GetStringUTFChars(jstr, nullptr);
if (str != nullptr) {
LOGD("Native 层 String 数组第 %d 个元素的值是:%s", i, str);
env->ReleaseStringUTFChars(jstr, str);
}
// 释放局部引用
env->DeleteLocalRef(jstr);
}
}
运行后打印如下:
jni com.example.jnitest D Native 层 String 数组第 0 个元素的值是:Kevin
jni com.example.jnitest D Native 层 String 数组第 1 个元素的值是:Kevin
jni com.example.jnitest D Native 层 String 数组第 2 个元素的值是:Kevin
jni com.example.jnitest D Native 层 String 数组第 3 个元素的值是:Kevin
MainActivity com.example.jnitest D Java 层 String 数组第 0 个元素的值是:Kevin
MainActivity com.example.jnitest D Java 层 String 数组第 1 个元素的值是:Kevin
MainActivity com.example.jnitest D Java 层 String 数组第 2 个元素的值是:Kevin
MainActivity com.example.jnitest D Java 层 String 数组第 3 个元素的值是:Kevin
可以看到 Java 层的字符串也都变成了 Kevin。
3.操作自定义类
使用 JNI 如何操作自定义类呢?假设有一条线段,线段有起点和终点,代码如下:
public class Path {
private PointF startPoint; // 起点
private PointF endPoint; // 终点
public Path(PointF startPoint, PointF endPoint){
this.startPoint = startPoint;
this.endPoint = endPoint;
}
}
MainActivity 代码如下:
public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary("jnitest");
}
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
PointF startPoint = new PointF(100, 100);
PointF endPoint = new PointF(200, 200);
Path path = new Path(startPoint, endPoint);
testPath(path);
}
private native void testPath(Path path);
}
使用 JNI 如何打印这两个点的 x 和 y 坐标?
要获取 startPoint 的 x 坐标,我们来进行反推。startPoint 是 PointF 的实例,即最后要拿到 PointF 实例的 x 坐标,可以通过 GetFloatField(jobject obj, jfieldID fieldID) 函数来获取实例中的 float 类型的字段的值,该函数需要 obj 和 fieldID 参数,obj 即 startPoint 的实例,可以通过 GetObjectField(jobject obj, jfieldID fieldID) 函数拿到。fieldID 即 x 在 PointF 中的字段 id ,可以通过 GetFieldID(jclass clazz, const char* name, const char* sig) 拿到,代码如下:
jobject startPointJObject = env->GetObjectField(path, startPointFieldId); // 拿到 PointF 实例
jclass pointFJClass = env->FindClass("android/graphics/PointF");
jfieldID xFieldId = env->GetFieldID(pointFJClass, "x", "F"); // 拿到 x 的 FieldId
jfloat startX = env->GetFloatField(startPointJObject, xFieldId); // 通过 x 的 FieldId 拿到 x 的值
GetObjectField() 函数又需要传入两个参数,其中 startPointFieldId 是 startPoint 在 Path 实例中的字段 id,获取 x 坐标的完整代码如下:
jclass jclazz = env->GetObjectClass(path);
jfieldID startPointFieldId = env->GetFieldID(jclazz, "startPoint", "Landroid/graphics/PointF;"); // 拿到 startPoint 的 FieldId
jobject startPointJObject = env->GetObjectField(path, startPointFieldId); // 通过 FieldId 拿到 PointF 实例
jclass pointFJClass = env->FindClass("android/graphics/PointF");
jfieldID xFieldId = env->GetFieldID(pointFJClass, "x", "F"); // 拿到 x 的 FieldId
jfloat startX = env->GetFloatField(startPointJObject, xFieldId); // 通过 x 的 FieldId 拿到 x 的值
所有代码补全后代码如下:
extern "C"
JNIEXPORT void JNICALL
Java_com_example_jnitest_MainActivity_testPath(JNIEnv *env, jobject thiz, jobject path) {
jclass jclazz = env->GetObjectClass(path);
jfieldID startPointFieldId = env->GetFieldID(jclazz, "startPoint", "Landroid/graphics/PointF;");
jobject startPointJObject = env->GetObjectField(path, startPointFieldId);
jclass pointFJClass = env->FindClass("android/graphics/PointF");
jfieldID xFieldId = env->GetFieldID(pointFJClass, "x", "F");
jfieldID yFieldId = env->GetFieldID(pointFJClass, "y", "F");
jfloat startX = env->GetFloatField(startPointJObject, xFieldId);
jfloat startY = env->GetFloatField(startPointJObject, yFieldId);
LOGD("startPoint x: %f, y: %f", startX, startY);
jfieldID endPointFieldId = env->GetFieldID(jclazz, "endPoint", "Landroid/graphics/PointF;");
jobject endPointJObject = env->GetObjectField(path, endPointFieldId);
jfloat endX = env->GetFloatField(endPointJObject, xFieldId);
jfloat endY = env->GetFloatField(endPointJObject, yFieldId);
LOGD("endPoint x: %f, y: %f", endX, endY);
env->DeleteLocalRef(startPointJObject);
env->DeleteLocalRef(endPointJObject);
env->DeleteLocalRef(pointFJClass);
env->DeleteLocalRef(jclazz);
}
运行后打印如下:
jni com.example.jnitest D startPoint x: 100.000000, y: 100.000000
jni com.example.jnitest D endPoint x: 200.000000, y: 200.000000