【问题标题】:JNI - Passing large amounts of data between Java and Native codeJNI - 在 Java 和本机代码之间传递大量数据
【发布时间】:2013-07-16 13:29:39
【问题描述】:

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

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 上传它们时保存一个带有字节缓冲区(与字节数组)的额外副本.

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

任何建议都会很棒。

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

【问题讨论】:

  • 你能用更简短的形式表达你的问题吗?
  • 为什么不尝试使用看起来足够快的最简单方法呢?如果速度不够快,或者你很好奇,你可以尝试更复杂的东西并进行比较。从您想到的所有方法和考虑的权衡来看,you know more about this specific problem and will be better able to answer it than anyone else
  • @Alex 我想提供尽可能多的细节,因为问题都是关于细节的。
  • @andrewdotn 我试图验证我的假设的正确性,除了找出什么会很快。这在最初的问题中可能并不明显,因为我一直在谈论性能权衡。

标签: java java-native-interface graphicsmagick


【解决方案1】:

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

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

正如您所说,(i) 不起作用,因为您不知道该方法将返回多少数据。 (ii) 由于该自定义打包协议,过于复杂。我会选择 (iii) 的修改版本:你不需要那个对象,你可以只返回一个 ByteBuffers 的数组,其中第一个元素是散列,其他元素是缩略图。你可以扔掉所有的memcpys!这就是直接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 不会尝试删除本机内存。您必须手动处理,因为您还手动创建了缓冲区。

【讨论】:

  • 感谢您的回答。我只有一个字节数组可供我输入。将字节数组包装在字节缓冲区中是否仍会产生相同的税收?我也是这样的数组语法:developer.android.com/training/articles/…。虽然副本仍然是可能的。如果我创建包装本机数据的字节缓冲区,谁负责清理它?我做 memcpy 只是因为我认为 Java 不知道什么时候释放它。此内存可能位于堆栈上、堆上或来自某些自定义分配器,这似乎会导致错误。
  • 感谢您的跟进。这似乎很复杂,因为我不知道何时确切地释放内存。我真的不知道字节缓冲区何时被 GC 处理。我读到了在 GC 上调用的 finalize 方法,但字节缓冲区不是我的类。有没有办法让我确定何时释放该内存?
  • 有一种方法:当你用完它时释放内存。只要您不对其进行任何操作,ByteBuffer 对象指向无效位置就没有问题。但是,您可以使用引用队列来检测对象何时被 GC 处理,然后才释放内存。但这超出了这个问题的范围,我也不推荐它,因为在不再需要时释放缓冲区更容易和更清洁。
【解决方案2】:
  1. 字节数组

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

【讨论】:

  • 一个字节数组的数组可以正常工作,除了所有的复制。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-09-26
  • 1970-01-01
  • 2017-12-08
  • 2011-05-03
  • 1970-01-01
  • 2017-01-05
相关资源
最近更新 更多