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

31

我想要实现以下操作:

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复制到每个字节缓冲区的直接地址中。我假设还有一些方法可以设置写入字节缓冲区的字节数,以便Java代码知道字节缓冲区的大小。在设置了字节缓冲区之后,我可以填充我的Java对象并返回它。与(i)和(ii)相比,这里创建了更多的字节缓冲区和Java对象,但避免了自定义协议的复杂性。在 (i)、(ii) 和 (iii) 之间进行选择的原因——考虑到我对这些缩略图所做的唯一操作是上传它们,我希望在通过 NIO 上传它们时用字节缓冲区(而不是字节数组)来保存额外副本。

(iv) 定义一个Java类,其中包含缩略图的字节数组数组(而不是字节缓冲区数组)和一个字节数组的感知哈希。我在本机代码中创建这些Java数组,并使用SetByteArrayRegion从我的GraphicsMagick blob复制字节。与先前的方法相比,缺点是现在在从堆复制此字节数组到某个直接缓冲区时,Java领域中会有另一个副本。不确定与 (iii) 相比,我是否会在复杂性方面节省任何东西。

任何建议都将非常棒。

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


2
你能用更简短的形式表达你的问题吗? - Alexander Kulyakhtin
为什么不尝试使用看起来足够快的最简单的方法呢?如果它不够快,或者你好奇的话,可以尝试一些更复杂的方法并进行比较。根据你考虑过的所有方法和权衡,你对这个特定问题的了解比任何人都更深,因此能够给出更好的答案。 - andrewdotn
1
@Alex 我想提供尽可能多的细节,因为问题都在于细节。 - Rajiv
1
@andrewdotn 我正在尝试验证我的假设的正确性,同时找出什么是快速的。由于我一直在谈论性能权衡,这在最初的问题中可能并不明显。 - Rajiv
2个回答

33
以下是翻译内容:
什么是将Java字节传递到本地代码的最有效方法?我可以将其作为字节数组访问。我没有看到将其作为字节缓冲区(包装此字节数组)与字节数组一起传递的任何特定优势。
直接ByteBuffer的最大优点是,您可以在本地端调用GetDirectByteBufferAddress,并立即获得指向缓冲区内容的指针,而不需要任何开销。如果您传递字节数组,则必须使用GetByteArrayElementsReleaseByteArrayElements(它们可能会复制数组)或关键版本(它们暂停GC)。因此,使用直接ByteBuffer可以对代码性能产生积极影响。
作为你所说的,(i)不起作用,因为你不知道该方法将返回多少数据。(ii)由于自定义打包协议过于复杂。我会选择(iii)的修改版本: 你不需要那个对象,你可以返回一个ByteBuffer数组,其中第一个元素是哈希值,其他元素是缩略图。你可以放弃所有的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]));
}

编辑:

我只有一个字节数组可用于输入。将字节数组包装在字节缓冲区中是否仍会产生相同的开销?我还看到了这种数组语法:http://developer.android.com/training/articles/perf-jni.html#region_calls。虽然复制仍然是可能的。

GetByteArrayRegion 总是写入缓冲区,因此每次都会创建副本,因此我建议改用 GetByteArrayElements。在Java端将数组复制到直接的 ByteBuffer 也不是最好的选择,因为您仍然需要那个副本,如果 GetByteArrayElements 固定了数组,则最终可以避免该问题。

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

如果数据在堆栈上,则必须将其复制到Java数组、在Java代码中创建的直接ByteBuffer或堆上的某个位置(并指向该位置的直接ByteBuffer)中。如果数据在堆上,则可以安全地使用使用NewDirectByteBuffer创建的直接ByteBuffer,只要您能确保没有人释放内存。当堆内存被释放时,您不能再使用ByteBuffer对象。当使用NewDirectByteBuffer创建的直接ByteBuffer被GC'd时,Java不会尝试删除本机内存。因为您也手动创建了缓冲区,所以必须手动处理这个问题。

谢谢回答。我只有一个字节数组可用于输入。将字节数组包装在字节缓冲区中是否仍会产生相同的开销?我还看到了这种数组的语法:http://developer.android.com/training/articles/perf-jni.html#region_calls。虽然仍然可能进行复制。如果我创建包装本地数据的字节缓冲区,谁负责清理它?我执行memcpy只是因为我认为Java不知道何时释放它。这个内存可以在堆栈上、堆上或来自某些自定义分配器,这似乎会导致错误。 - Rajiv
感谢您的跟进。这似乎很复杂,因为我不知道何时确切地释放内存。我真的不知道字节缓冲区何时被垃圾回收。我了解到有一个 finalize 方法在 GC 上调用,但字节缓冲区不是我的类。有没有办法让我确定何时释放该内存? - Rajiv
2
有一种方法:在使用完内存后释放它。只要您不再使用它,ByteBuffer对象指向无效位置就没有问题。但是,您可以使用引用队列来检测对象何时被GC并仅在此时释放内存。但这超出了本问题的范围,我也不建议这样做,因为在不再需要缓冲区时释放缓冲区更容易、更清洁。 - main--

1
  1. 字节数组

  2. 我曾经需要做类似的事情,我返回了一个字节数组的容器(Vector或其他)。另一位程序员将其实现为回调函数(我认为这样更简单但有点傻),例如JNI代码会为每个响应调用Java方法,然后原始调用(进入JNI代码)将返回。虽然这样做可以正常工作。


一个字节数组的数组可以正常工作,除了所有的复制。 - Rajiv

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接