Java 与 JNI 互传数据的那些事

By | 2016-04-20

常规类型的传递

这部分算是 JNI 的基本内容, 理所当然的有一大坨接口来干这些事情,
比如 NewString, GetStringChars, GetArrayLength, NewByteArray

到 Java 层自然就是原生的数据类型了, 比如 String, int, byte[]

需要注意的只是, 有的类型不需要释放, 有的类型则需要, 例如对象可能需要 DeleteLocalRefDeleteGlobalRef,
访问数组内容一般需要 ReleaseByteArrayElements

Java 对象的内存管理

Java 使用类似引用计数的方式管理内存, 并且不定期 GC, 所以 JNI 访问 Java 对象还需要特别注意

临时对象

大多数情况来说, 访问方式一般都是 JNI 调用 Java 接口, 返回一个 Java 的临时对象, 例如:

// Java
public Object func() {...}

// JNI
jobject obj = env->CallObjectMethod(...);

这种情况一般不同担心, JNI 端代码结束后, 一般会在合适时机 GC

当然, 特殊情况是, 假如 JNI 端需要持续执行较长时间, 并且可能访问了较多的 Java 端对象,
就需要手动调用 DeleteLocalRef 释放这些临时对象, 避免性能问题, 此外, JNI 端 local 对象个数也是有限制的

同理的还有 NewString 等 JNI 接口, 创建的都是临时对象

全局对象

上面提到了, 临时对象会在不确定时机被 GC, 所以如果你要长期使用这个对象, 简单的保存 jobject 是不行的

正确的做法是, 使用 NewGlobalRef 来创建一个全局引用, 这个引用会一直存在, 不会被 GC, 直到你调用 DeleteGlobalRef

这边需要注意的是, NewGlobalRef 虽然是操作同一个对象, 但是 jobject 本身是不一样的, 典型的使用方法是:

jobject localRef = xxx;
jobject globalRef = env->NewGlobalRef(localRef);
env->DeleteLocalRef(localRef);

特殊类型的传递

对象和基础类型的传递一般都没什么问题, 麻烦事在于一大段内存块的传递

指针传递

这是比较常见也比较容易实现的方式, 典型使用场景是, 内存块的申请/使用/释放 (也可以是 C++ 对象), 都在 JNI 端处理, Java 端只负责调用和这个指针的传递

很常规的做法就是把指针转换为 jlong 进行传递即可

PS:

  • 有点担心哪天升级 128 位 CPU 了咋办, 不过目前大多 JNI 都是这种处理方式, 应该未来会有变通方式解决吧
  • 更保险起见的方式是将指针转换为 byte[] 进行传递, 不过麻烦而且性能会受影响

内存块的传递

典型使用场景:

Java 读取图片或音频什么的, 把内容作为内存块交给 JNI 处理, JNI 处理完后, 把新的内存块内容返回给 Java 端再重新解析为图片或音频什么的

(这里姑且不论使用临时文件做中转的方式, 何况写文件也挺慢的呢)

这个可谓麻烦至极, 首先, 通常都会想到用 byte[] 传递,
然而这货无论是 Java 传到 JNI 还是 JNI 传到 Java, 都 (可能但不一定) 需要进行深拷贝,
对于分分钟上兆的媒体文件来说, 是个巨大的 CPU 和内存开销

找遍文档, 避免深拷贝的方法大概有

GetPrimitiveArrayCritical

看上去好像那么一回事, 写个简单的测试代码发现可以用, 皆大欢喜了?

图样图森破, 当你尝试在 GetPrimitiveArrayCritical 和 ReleasePrimitiveArrayCritical 之间
再调用 Java 代码时, 就会发现挂了

顾名思义, 这货是直接访问 Java 的底层数据内容, 对于 Java 这种不知何时会 GC 的运行时来说,
Java 显然不会让你瞎搞, 试想以下流程:

  1. GetPrimitiveArrayCritical 得到底层数据指针
  2. JNI 端调用 Java 代码, 很可能产生 GC
  3. 底层数据指针所属的 Java 对象被 GC 了, 这个指针自然也就无效了

所以, 这个只适合用于只读而且逻辑简单的场景, 就像多线程编程通常不推荐在被锁的代码块里面做太多事情一样
(否则可能一不小心就死锁了)

ByteBuffer

仔细查找文档可以发现 NewDirectByteBuffer 这么个东东, 对应的是 Java 端的 java.nio.ByteBuffer

有了 ByteBuffer, JNI 端就可以通过 GetDirectBufferAddress 获得内存地址, 完美了… 吗?

图样图森破, 这货依然有着麻烦的使用条件:

  • 虽然都叫 ByteBuffer, 但是这个只能使用 NewDirectByteBuffer 或者 ByteBuffer.allocateDirect() 进行创建,
    否则 GetDirectBufferAddress 返回的总是 NULL

    不支持的几个: ByteBuffer.allocate(), ByteBuffer.wrap()

    更具体的原因, 各位有兴趣可以去搜搜 DirectByteBufferHeapByteBuffer

  • JNI 这魂淡没有提供足够的方法去操作 ByteBuffer 的方法

    比如 position(), remaining(), flip() 都没有, 而只有 GetDirectBufferCapacity 来获取最大容量,
    所以你还得自行添加一大坨内容, 来在 JNI 调用这些方法

不管怎样, 最终我们可以用 ByteBuffer 来开心的玩耍了, 然而这货本身作为一个比较底层的 buffer, 提供的功能挺少,
于是又得自己管理内存增长之类的逻辑了, 怎样, 有没有一种回到 C 语言的美好感觉了?

over

转载请注明来自: http://zsaber.com/blog/p/107

既然都来了, 有啥想法顺便留个言呗? (无奈小广告太多, 需审核, 见谅)

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注