30

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

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) 背后的基本原理 - 鉴于我对这些缩略图所做的唯一事情就是上传它们,

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

任何建议都会很棒。

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

4

2 回答 2

32

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

直接的最大优点ByteBuffer是您可以GetDirectByteBufferAddress在本机端调用,并且您可以立即获得指向缓冲区内容的指针,而无需任何开销。如果你传递一个字节数组,你必须使用GetByteArrayElementsand ReleaseByteArrayElements(他们可能会复制数组)或关键版本(他们暂停 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改为。将数组直接复制到ByteBufferJava 端也不是最好的主意,因为如果固定GetByteArrayElements数组,您仍然拥有最终可以避免的副本。

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

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

于 2013-07-17T21:00:20.663 回答
1
  1. 字节数组

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

于 2013-07-17T21:01:33.870 回答