JNI——操作 Java 层对象

110 阅读8分钟

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