C++中如何实现一个完整的Jni模块
Updated:
在开发中容易忽略的事情文章中有提到,对于客户端开发,复用程度最高的还是C语言,但C语言实现的功能在Java语言环境中要通过Jni来调用,Jni基本实现比较简单,但是要做稳定还要考虑很多因素,比如实例管理、资源的释放、多线程处理,本文主要讨论设计一套有实例管理的完整JNI模块
基本流程
JNI_OnLoad
C层加载动态库时调用的第一个函数就是JNI_OnLoad,JNI_OnLoad可以获得JavaVM是全局变量,通过JavaVM可以拿到JNIEnv来进行Jni常见操作(比如FindClass),不同的Jni模块共享同一个JavaVM。
可以有一个工具模块,来管理JavaVM,同时在不同线程使用JNIEnv时封装调用AttachCurrentThread和DetachCurrentThread的逻辑,其他Jni模块依赖这个工具模块来使用JNIEnv。
一般在在JNI_OnLoad时进行RegisterNatives操作,具体代码如下,NativeTest里面的initNativeMethods方法会调用RegisterNatives
extern “C” JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) |
RegisterNatives
强烈建议大家不要用静态方法注册,方法名和包名要保持一致,如果你只提供一个简单的c层jni封装,别人集成的话只能按照你指定的包名集成
通过RegisterNatives方法可以把Java层对应类的方法和C层对应的函数绑定起来,java_class代表Java层的对应类的class信息,所以这个绑定是函数元信息跟函数元信息的绑定,不涉及到具体实例
jint RegisterNatives(jclass java_class, JNINativeMethod method, jint methodLen) |
具体代码实例如下:
void NativeTest::initNativeMethods() |
JNINativeMethod类型主要包括三个元素,依次为:Java层对应的方法名、Java层方法名的签名,C层对应的方法,比如 “createNativeTest”、”(Lcom/test/TestCallback;)J”、(void*) &NativeTest::createNativeTest
com/test/ 对应java的包名com.test
创建C层实例
JNI功能是Java层的一个函数到C层的一个函数的映射,不是一个Java层的类实例和C层类实例的一一对应
,所以Java类和C层的类都应该是static的,当需要使用C层的一个实例时,需要在Java层保持C层的一个实例地址,所有操作都是基于这个实例,具体代码如下,通过createNativeTest来创建一个实例,同时传入了一个回调类TestCb,用来在Test方法完成某项功能后进行回调。
jlong NativeTest::createNativeTest(JNIEnv* env, jclass cls,jobject jobject_test_callback) |
把实例地址返回给Java层,Java层在调用其他方法时,比如start方法,需要把这个作为参数传入,通过reinterpret_cast强转为对应的类型,再进行相应的调用,如下:
jint NativeTest::start(JNIEnv* env, jclass cls,jlong test_address) |
C层回调
Java层调用了一个C的接口,C里面给Java一个回调是很常见的业务,具体代码如下:
TestCb::TestCb(JNIEnv* env, jobject jobject_callback) |
TestCb类里面保存了从Java层传入的对象指针jobject_callback,在构造函数中将其转化成全局的指针jobject_callback_global_和对应的类信息对象jclass_callback_global_,在Test类中进行回调时,通过jobject_callback_global_获取jmethodID,通过jclass_callback_global_找到对应的对象完成回调,如下:
void TestCb::onTest(long value) |
Java层实现
Java层的类主要是对C层实例的封装,让使用者感觉不到是C的实现,代码如下,在构造函数的时候调用createNativeTest来保存C层的指针,调用start方法时把nativeAddress传到C层,通过terminate方法来释放对应的C层指针
package com.test; |
回调类TestCallback是给C层调用的,在NativeTest初始化的时候把回调地址传给C层,TestCallback可以作为一个接口,这样更改回调实现时就不用更改C层代码
public class TestCallback { |
调用Jni模块
实例化NativeTest,通过调用start和destroy来控制c层的内存初始化和释放,进行实例管理,防止内存泄露
NativeTest nativeTest = NativeTest.createNativeTest(new TestCallback(){ |
资源释放
Jni在本地局部或全局引用没有释放,超过内存句柄限制时会导致“local reference table overflow”,jobject、jstring、jclass是常见的需要释放的类型,通过命名方式可以提醒程序员来释放相应资源
- jobject类型本地局部命名以“jobject_”开头,例如:jobject_arraylist
- jstring类型本地局部命名以“jstring_”开头,例如:jstring_id
- jclass类型本地局部命名以“jclass_”开头,例如:jclass_test