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

我正在努力实现以下目标:

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 land中将有另一个副本。不确定我是否会在这里保存任何复杂性与(iii)的东西。

任何建议都会很棒。

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


答案 1

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

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

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

法典:

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总是写入缓冲区,因此每次都创建一个副本,所以我建议改用。将数组复制到 Java 端的直接数组也不是最好的主意,因为您仍然拥有该副本,如果固定该数组,则最终可以避免该副本。GetByteArrayElementsByteBufferGetByteArrayElements

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

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


答案 2
  1. 字节数组

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


推荐