深层次理解JNI中的GetArray<PrimitiveType>Elements()函数

2,519 阅读5分钟
原文链接: blog.csdn.net

最近在学习JNI,在做一个小demo练手时,出现了一点问题:通过调用native方法,将Java层生成的整型数组传到C层,在C层将数组中的每个元素加10,然后返回到Java层。最初的实现的代码如下:

JNIEXPORT jintArray JNICALL Java_com_example_javacallc2_JNI_increaseArrayEles
        (JNIEnv *env, jobject jobj, jintArray jArray) {

    //1. 拿到数组的长度
    jsize size = (*env)->GetArrayLength(env, jArray);
    //2. 获取C层数组的首地址
    jint *jArr = (*env)->GetIntArrayElements(env, jArray, JNI_FALSE);

    //3. 通过for循环将每个元素加10
    int i = 0;
    for (i = 0; i < size; ++i) {
        *(jArr + i) += 10;
    }

    return jArray;
}

顺便贴出MainActivity和native方法:

public class JNI {

    static {
        System.loadLibrary("Test");
    }

    public native int[] increaseArrayEles(int[] intArray);
}

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        TextView textView = new TextView(this);
        setContentView(textView);

        int[] array = {1, 2, 3, 4, 5};
        int[] updatedArray = new JNI().increaseArrayEles(array);
       textView.setText( "before : " + Arrays.toString(array));
        "\n updated : " + Arrays.toString(updatedArray));
    }
}

发现在调用完native方法后在有些模拟器中数组中元素的值没有改变


但是在有的模拟器上是可以得到正确结果的。这是为什么呢?

C层对应的方法中的逻辑很简单,第一步获取数组长度是不会有问题的,最后一步for遍历就是标准C语言基础。那么极有可能出现问题的就是第二步:获取C层数组的首地址。

对于GetIntArrayElements函数的三个参数:

  • 第一个参数JNIEnv*,就是一个指向JNI环境的指针
  • 第二个参数jintArray,表示传入一个数组首地址,也就是C层函数中传入的第三个参数,直接传入即可
  • 第三个参数jboolean*,三种取值NULL、JNIFALSE(表示直接返回指向原始数组的指针)、JNITRUE(表示返回一个C数组,它是对原始Java数组的拷贝)->这地方有坑,稍后再说

为了更深层次的理解这个GetIntArrayElements()方法,我打开了JNI官方文档的第四章(http://docs.Oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html),找到“GetArrayElements Routines”一节:

A family of functions that returns the body of the primitive array. The result is valid until the corresponding ReleaseArrayElements() function is called. Since the returned array may be a copy of the Java array, changes made to the returned array will not necessarily be reflected in the original array until ReleaseArrayElements() is called. If isCopy is not NULL, then *isCopy is set to JNI_TRUE if a copy is made; or it is set to JNI_FALSE if no copy is made.

翻译过来就是:

这是用来返回原生数据类型数组体的家族函数。函数返回的指针在调用对应的ReleaseArrayElements()函数之前都是有效的(就是指针指向的区域没有被释放,是可以使用的)。因为这个函数返回的数组可能是Java数组的一份拷贝,所以直到调用ReleaseArrayElements()方法,对返回的数组所做的修改才会反映到原始数组中。

还是有点迷糊,接着去阅读文档中关于ReleaseArrayElements()函数的描述(关键就是这个对此函数的最后一个参数的理解,直接看截图):


三种取值:

  • 0:表示会把缓冲区(即在GetArrayElements方法中返回的)的内容拷贝回原始Java数组中,然后释放缓冲区
  • JNI_COMMIT:表示也会把缓冲区的内容拷贝回原始Java数组中,但是不会释放缓冲区
  • JNI_ABORT:表示不会把缓冲区的内容拷贝回原始Java数组中,直接释放缓冲区。

我试着在JNI的C代码中添加了一行代码,即在最后调用这个Release方法,现在代码如下:

JNIEXPORT jintArray JNICALL Java_com_example_javacallc2_JNI_increaseArrayEles
        (JNIEnv *env, jobject jobj, jintArray jArray) {

    ......

    (*env)->ReleaseIntArrayElements(env, jArray, jArr, 0);

    return jArray;
}

一运行,发现Java数组的内容变化了:


虽然要的效果出来了,但是目前在自己的认知中存在矛盾:在调用GetIntArrayElements函数时我们的第三个参数传入了JNI_FALSE,即表示我们得到的数组就是指向原始Java数组呀,那么我们对它返回的数组所做的改变是直接反映到原始Java数组中的呀,因为它们是同一块内存区域。为什么还需要通过ReleaseIntArrayElements()函数进行拷贝?这不是自相矛盾了吗。

最终在文档第二章中得到答案:(http://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/design.html#accessing primitivearrays)

Second, programmers can use another set of functions to retrieve a pinned-down version of array elements. Keep in mind that these functions may require the java VM to perform storage allocation and copying. Whether these functions in fact copy the array depends on the VM implementation, as follows:

• If the garbage collector supports pinning, and the layout of the array is the same as expected by the native method, then no copying is needed.

• Otherwise, the array is copied to a nonmovable memory block (for example, in the C heap) and the necessary format conversion is performed. A pointer to the copy is returned.

也就是说如果GC支持pin操作,那么我们是可以直接拿到指向原始数组的指针,否则就会直接返回一份拷贝。这么说就明白了。现在我们就可以解释之前的我们认为矛盾的地方了:

原来,GetIntArrayElements()函数的返回值是和JVM相关的,如果JVM的GC支持pin操作,那么返回值就是指向原始数组的指针;否则返回的就是原始数组的一份拷贝的首地址。那么我们当前运行的模拟器的JVM是不支持pin操作的,这个方法无论你的第三个参数传入什么值,返回的都是一份拷贝而不是数组本身;当然如果JVM支持pin操作,那么就要视第三个参数而定。

大家不信的话可以自己试验一下,推荐两个可以对比试验的模拟器:6.0 x86处理器的模拟器是不支持pin操作的,4.4 armeabi处理的模拟器是支持pin操作的。

之前提到过在调用ReleaseIntArrayElements()函数的时候有坑,至于这个是什么呢?就是它的第三个参数,它是一个jboolean*类型的指针,那么我们之前的代码

jint *jArr = (*env)->GetIntArrayElements(env, jArray, JNI_FALSE);

是有问题的。关键这么传不会崩溃,但是如果传入的是JNI_TRUE程序是直接就崩了。正确的传参方式是:

jboolean isCopy = JNI_FALSE;

jint *jArr = (*env)->GetIntArrayElements(env, jArray, &isCopy);

像这样,自己定义一个变量,最终将变量的地址传过去,因为它这里要的是指针。