JNI - 在Java和Native代码之间传递大量数据

Raj*_*jiv 27 java java-native-interface graphicsmagick

我想要实现以下目标:

1)我在java端有一个表示图像的字节数组.

2)我需要让我的本机代码访问它.

3)本机代码使用GraphicsMagick解码此图像,并通过调用resize创建一堆缩略图.它还计算图像的感知散列,该散列是矢量或unint8_t数组.

4)一旦我将这些数据返回给Java端,不同的线程就会读取它.缩略图将通过HTTP上传到某些外部存储服务.

我的问题是:

1)将字节从Java传递到我的本机代码的最有效方法是什么?我可以将其作为字节数组访问.我认为将其作为字节缓冲区(包装此字节数组)与字节数组传递在此处没有任何特别的优势.

2)将这些缩略图和感知哈希返回给java代码的最佳方法是什么?我想到了几个选择:

(i)我可以在Java中分配一个字节缓冲区,然后将其传递给我的本机方法.然后,本机方法可以写入并在完成后设置一个限制并返回写入的字节数或一些表示成功的布尔值.然后,我可以对字节缓冲区进行切片和切块,以提取不同的缩略图和感知哈希,并将其传递给将上传缩略图的不同线程.这种方法的问题是我不知道要分配的大小.所需的大小将取决于我提前生成的缩略图的大小和缩略图的数量(我事先知道).

(ii)一旦我知道所需的大小,我也可以在本机代码中分配字节缓冲区.我可以根据自定义打包协议将我的blob记忆到正确的区域并返回此字节缓冲区.(i)和(ii)都很复杂,因为自定义打包协议必须指示每个缩略图的长度和感知散列.

(iii)定义一个具有缩略图字段的Java类:字节缓冲区数组和感知散列字节:字节数组.当我知道所需的确切大小时,我可以在本机代码中分配字节缓冲区.然后,我可以将GraphicsMagick blob中的字节memcpy到每个字节缓冲区的直接地址.我假设还有一些方法来设置写在字节缓冲区上的字节数,以便java代码知道字节缓冲区有多大.设置字节缓冲区后,我可以填写我的Java对象并返回它.与(i)和(ii)相比,我在这里创建了更多的字节缓冲区以及Java对象,但我避免了自定义协议的复杂性.(i),(ii)和(iii)背后的基本原理 - 鉴于我对这些缩略图的唯一做法是上传它们,我希望通过NIO上传它们时保存带字节缓冲区(vs字节数组)的额外副本.

(iv)定义一个Java类,它具有缩略图的字节数组(而不是字节缓冲区)和感知散列的字节数组.我在我的本机代码中创建这些Java数组,并使用SetByteArrayRegion从我的GraphicsMagick blob复制字节.与以前的方法相比,缺点是,当上传它时,将这个字节数组从堆复制到某个直接缓冲区时,现在Java中还会有另一个副本.不确定我是否会在复杂性方面保存任何东西,而不是(iii).

任何建议都会很棒.

编辑:@main提出了一个有趣的解决方案.我正在编辑我的问题以跟进该选项.如果我想像@main建议的那样在DirectBuffer中包装本机内存,我怎么知道何时可以安全地释放本机内存?

mai*_*n-- 26

将字节从Java传递到我的本机代码的最有效方法是什么?我可以将其作为字节数组访问.我认为将其作为字节缓冲区(包装此字节数组)与字节数组传递在此处没有任何特别的优势.

直接的一大优点ByteBuffer是你可以GetDirectByteBufferAddress在本机端调用,你立即有一个指向缓冲区内容的指针,没有任何开销.如果传递字节数组,则必须使用GetByteArrayElementsReleaseByteArrayElements(它们可能复制数组)或关键版本(它们暂停GC).因此,使用direct ByteBuffer可以对代码的性能产生积极影响.

正如你所说,(i)将无法工作,因为你不知道该方法将返回多少数据.(ii)由于该定制包装协议而过于复杂.我会去修改版本的(iii):你不需要那个对象,你可以只返回一个ByteBuffers 数组,其中第一个元素是哈希,其他元素是缩略图.你可以扔掉所有memcpy的东西!这就是直接的全部要点ByteBuffer:避免复制.

码:

void Java_MyClass_createThumbnails(JNIEnv* env, jobject, jobject input, jobjectArray output)
{
    jsize nThumbnails = env->GetArrayLength(output) - 1;
    void* inputPtr = env->GetDirectBufferAddress(input);
    jlong inputLength = env->GetDirectBufferCapacity(input);

    // ...

    void* hash = ...; // a pointer to the hash data
    int hashDataLength = ...;
    void** thumbnails = ...; // an array of pointers, each one points to thumbnail data
    int* thumbnailDataLengths = ...; // an array of ints, each one is the length of the thumbnail data with the same index

    jobject hashBuffer = env->NewDirectByteBuffer(hash, hashDataLength);
    env->SetObjectArrayElement(output, 0, hashBuffer);

    for (int i = 0; i < nThumbnails; i++)
        env->SetObjectArrayElement(output, i + 1, env->NewDirectByteBuffer(thumbnails[i], thumbnailDataLengths[i]));
}
Run Code Online (Sandbox Code Playgroud)

编辑:

我只有一个字节数组可用于输入.不将字节数组包装在字节缓冲区中仍然会产生相同的税吗?我也是这样的数组语法:http://developer.android.com/training/articles/perf-jni.html#region_calls.虽然副本仍然可以.

GetByteArrayRegion总是写入缓冲区,因此每次都创建一个副本,所以我建议GetByteArrayElements改为.将数组复制到ByteBufferJava端的直接也不是最好的主意,因为您仍然拥有最终可以避免的数据副本GetByteArrayElements.

如果我创建包装本机数据的字节缓冲区,谁负责清理它?我做memcpy只是因为我认为Java不知道何时释放它.这个内存可能在堆栈上,堆上或某些自定义分配器上,这似乎会导致错误.

如果数据在堆栈上,那么您必须将其复制到Java数组中,ByteBuffer这是一个在Java代码中创建的直接或堆上的某个地方(以及ByteBuffer指向该位置的直接数据).如果它在堆上,那么您可以安全地使用ByteBuffer您创建的直接使用NewDirectByteBuffer,只要您可以确保没有人释放内存.当堆内存空闲时,您必须不再使用该ByteBuffer对象.当ByteBuffer使用NewDirectByteBufferGC'd 创建的direct时,Java不会尝试删除本机内存.您必须手动处理,因为您还手动创建了缓冲区.

  • 有一种方法:当你完成它后释放内存.只要你没有对它做任何事情,`ByteBuffer`对象指向无效位置就没问题.但是,您可以使用引用队列来检测对象何时是GC,然后才释放内存.但这超出了这个问题的范围,我也不推荐它,因为在不再需要时释放缓冲区更容易和更清洁. (2认同)