【zz】Java 与 JNI 互传数据

Java 与 JNI 互传数据的那些事

转载


1 常规类型的传递
2 Java 对象的内存管理
2.1 临时对象
2.2 全局对象
3 特殊类型的传递
3.1 指针传递
3.2 内存块的传递
3.2.1 GetPrimitiveArrayCritical
3.2.2 ByteBuffer

常规类型的传递

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

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

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

Java 对象的内存管理

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

临时对象

  • 大多数情况来说, 访问方式一般都是 JNI 调用 Java 接口, 返回一个 Java 的临时对象, 例如:
// Java
public Object func() {...}

// JNI  这种都是临时对象 todo!!!!!!
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 显然不会让你瞎搞, 试想以下流程:

GetPrimitiveArrayCritical 得到底层数据指针

JNI 端调用 Java 代码, 很可能产生 GC

  • 底层数据指针所属的 Java 对象被 GC 了, 这个指针自然也就无效了
    所以, 这个只适合用于只读而且逻辑简单的场景, 就像多线程编程通常不推荐在被锁的代码块里面做太多事情一样
    (否则可能一不小心就死锁了)

ByteBuffer

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

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

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

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

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

  • 更具体的原因, 各位有兴趣可以去搜搜 DirectByteBuffer 和 HeapByteBuffer

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

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

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

over

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

等风来不如迎风去

你的鼓励是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值