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

时间:2013-07-17 20:04:57

标签: 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中包装本机内存,我怎么知道何时可以安全地释放本机内存?

2 个答案:

答案 0 :(得分:25)

  

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

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

如你所说,(i)将无法工作,因为您不知道该方法将返回多少数据。 (ii)由于该定制包装协议而过于复杂。我会去修改版本的(iii):你不需要那个对象,你可以只返回一个ByteBuffer的数组,其中第一个元素是哈希,其他元素是缩略图。你可以扔掉所有memcpy s !这是直接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]));
}

修改

  

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

GetByteArrayRegion总是写入缓冲区,因此每次都会创建一个副本,所以我建议改为GetByteArrayElements。将数组复制到Java端的直接ByteBuffer也不是最好的主意,因为如果GetByteArrayElements固定数组,您仍然可以最终避免使用该副本。

  

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

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

答案 1 :(得分:1)

  1. 字节数组

  2. 我不得不做类似的事情,我返回了一个Byte数组的容器(Vector或其他东西)。其中一个程序员实现了这个(我认为这更容易,但有点傻)回调。例如JNI代码将为每个响应调用Java方法,然后返回原始调用(进入JNI代码)。这确实可行。