昨天加班的时候遇到了 ​​GetPrimitiveArrayCritical​​ 导致的崩溃,正好搜到这篇文章,提到这个问题,大家可以借鉴一下,避免像我这样出错。

解决后续,使用 GetByteArrayElements 替代了 GetPrimitiveArrayCritical 方法。

以下是原文:

常规类型的传递

这部分算是 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
jobject obj = env->CallObjectMethod(...);

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

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

全局对象

上面提到了, 临时对象会在不确定时机被 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 与 JNI 互传数据的那些事_临时对象Java 与 JNI 互传数据的那些事_临时对象Java 与 JNI 互传数据的那些事_临时对象

顾名思义, 这货是直接访问 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()

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

  • 条件二

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

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

不管怎样, 最终我们可以用 ByteBuffer 来开心的玩耍了, 然而这货本身作为一个比较底层的 buffer, 提供的功能挺少,

于是又得自己管理内存增长之类的逻辑了, 怎样, 有没有一种回到 C 语言的美好感觉了?


Java 与 JNI 互传数据的那些事_Java_04