JNI函数向Android应用返回数据:ByteBuffer读取与写入问题
嘿,我看你遇到的核心问题是JNI解压后的数据明明已经写入目标ByteBuffer了,但Java层就是读不到——本质是ByteBuffer的position和limit状态没和实际写入的字节数同步,Java层不知道哪些是有效数据。结合你的代码和编辑内容,我给你梳理下解决思路和修正方案:
一、先搞定JNI到Java层的ByteBuffer状态同步
你的编辑2已经找对了方向,但还有几个细节要调整:
1. 别再乱转类型了!
lzfse_decode_buffer返回的size_t是实际写入的字节数,你的测试值是319488,这个数完全在jint(32位有符号整数,最大值2^31-1)的范围内,直接转jint就行。之前转jlong出现负数是因为size_t是无符号类型,强制转有符号jlong时高位被符号扩展了,完全没必要。
2. 用反射同步ByteBuffer的有效区间
在JNI里,我们需要调用Java ByteBuffer的limit()和position()方法,把limit设为实际解压的字节数,position设为0——这样Java层一拿到ByteBuffer,就知道从哪读到哪。
修正后的JNI代码:
JNIEXPORT jlong JNICALL Java_com_company_appName_MainActivity_decompress(JNIEnv* env, jclass cls, jobject src, jobject dst) { uint8_t* src_buffer = (*env)->GetDirectBufferAddress(env, src); const size_t src_size = (*env)->GetDirectBufferCapacity(env, src); uint8_t* dst_buffer = (*env)->GetDirectBufferAddress(env, dst); size_t dst_size = (*env)->GetDirectBufferCapacity(env, dst); // 执行解压,拿到实际写入的字节数 size_t decompressed_size = lzfse_decode_buffer(dst_buffer, dst_size, src_buffer, src_size, NULL); ALOG("实际解压出的字节数: %zu", decompressed_size); // 通过反射获取ByteBuffer的类和方法ID jclass byteBufferCls = (*env)->GetObjectClass(env, dst); if (byteBufferCls == NULL) { return -1; // 类获取失败,返回错误码 } // 获取limit(int)方法:参数是jint,返回Buffer对象 jmethodID limitMethod = (*env)->GetMethodID(env, byteBufferCls, "limit", "(I)Ljava/nio/Buffer;"); // 获取position(int)方法:参数是jint,返回Buffer对象 jmethodID positionMethod = (*env)->GetMethodID(env, byteBufferCls, "position", "(I)Ljava/nio/Buffer;"); if (limitMethod != NULL && positionMethod != NULL) { // 设置limit为实际解压大小,position归0,让Java层能直接读取有效数据 (*env)->CallObjectMethod(env, dst, limitMethod, (jint)decompressed_size); (*env)->CallObjectMethod(env, dst, positionMethod, 0); } // 返回解压大小给Java层,方便做校验 return (jlong)decompressed_size; }
二、Java层正确读取并写入文件
现在JNI已经帮我们把ByteBuffer的状态调好了,Java层的代码也可以优化下,尤其是IO效率和源数据的正确性:
修正后的Java调用代码:
try { File inFile = new File(Environment.getExternalStorageDirectory().getPath() + "/1"); ByteBuffer buf = ByteBuffer.allocateDirect((int) inFile.length()); // 预估目标缓冲区大小,可根据实际压缩比调整 ByteBuffer buf_out = ByteBuffer.allocateDirect((int) inFile.length() * 20); // 批量读取源文件到ByteBuffer(比单个字节读取快N倍) InputStream is = new FileInputStream(inFile); byte[] tempBuf = new byte[4096]; int bytesRead; while ((bytesRead = is.read(tempBuf)) != -1) { buf.put(tempBuf, 0, bytesRead); } buf.flip(); // 重点!写入后position在末尾,flip后把position设为0,limit设为写入长度,JNI才能读到完整的源数据 is.close(); Log.d("nims", "源缓冲区有效数据长度: " + buf.limit()); // 调用JNI解压 Long decompressedSize = decompress(buf, buf_out); Log.d("nims", "实际解压字节数: " + decompressedSize); Log.d("nims", "目标缓冲区有效数据长度: " + buf_out.limit()); // 写入文件:JNI已经把position设为0,limit设为有效长度,直接写就行 File outFile = new File(Environment.getExternalStorageDirectory().getPath() + "/2"); FileChannel channel = new FileOutputStream(outFile, false).getChannel(); int writtenBytes = channel.write(buf_out); Log.d("nims", "实际写入文件的字节数: " + writtenBytes); channel.close(); } catch (IOException e) { Log.e("nims", "I/O错误: " + e.getMessage()); }
额外优化建议:修正源数据长度的传递
原来的JNI代码用GetDirectBufferCapacity获取源数据长度是错的——这个方法返回的是缓冲区的总容量,不是实际写入的源数据长度!比如你分配了100字节缓冲区,但只写入了50字节,GetDirectBufferCapacity还是返回100,会导致解压时读错数据。
建议修改Native方法,直接传入源数据的实际长度:
// 修改Native方法定义 public static native Long decompress(ByteBuffer src, int srcLength, ByteBuffer dst);
然后JNI代码里直接用传入的长度:
JNIEXPORT jlong JNICALL Java_com_company_appName_MainActivity_decompress(JNIEnv* env, jclass cls, jobject src, jint srcLength, jobject dst) { uint8_t* src_buffer = (*env)->GetDirectBufferAddress(env, src); const size_t src_size = (size_t)srcLength; // 直接用传入的实际长度 // ... 后面的代码不变 }
Java层调用时传buf.limit()就行:
buf.flip(); Long decompressedSize = decompress(buf, buf.limit(), buf_out);
总结一下
- JNI层:解压后必须同步ByteBuffer的
limit和position,告诉Java层哪些是有效数据; - Java层:读取完源文件后一定要
flip()源ByteBuffer,确保JNI拿到正确的源数据长度; - 类型转换:根据实际数值大小选择合适的Java整数类型,避免无符号转有符号的坑。
内容的提问来源于stack exchange,提问作者Nimesh Chandramaniya

