在开发中容易忽略的事情文章中有提到,对于客户端开发,复用程度最高的还是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) { if (!NativeTest::initNativeMethods()) { return JNI_ERR; } return JNI_VERSION_1_6; }
|
RegisterNatives
强烈建议大家不要用静态方法注册,方法名和包名要保持一致,如果你只提供一个简单的c层jni封装,别人集成的话只能按照你指定的包名集成
通过RegisterNatives方法可以把Java层对应类的方法和C层对应的函数绑定起来,java_class代表Java层的对应类的class信息,所以这个绑定是函数元信息跟函数元信息的绑定,不涉及到具体实例
jint RegisterNatives(jclass java_class, JNINativeMethod method, jint methodLen)
|
具体代码实例如下:
void NativeTest::initNativeMethods() { JNINativeMethod nativeFunctions[] = { { "createNativeTest", "(Lcom/test/TestCallback;)J", (void*) &NativeTest::createNativeTest }, { "destroyNativeTest", "(J)I", (void*) &NativeTest::destroyNativeTest }, { "start", "(J)I", (void*) &NativeTest::terminate }, }; env->RegisterNatives("com/test/NativeTest", nativeFunctions, sizeof(nativeFunctions)/sizeof(nativeFunctions[0])); }
|
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) { TestCb* test_cb = new TestCb(env,jobject_test_callback); Test* test = new Test(test_cb); if(test == NULL){ return -1; } return reinterpret_cast<intptr_t>(test); } jint NativeTest::destroyNativeTest(JNIEnv* env, jclass cls,jlong test_address) { Test* test = reinterpret_cast<Test*>(test_address); if (test == NULL) { return -1; } delete test; return 0; }
|
把实例地址返回给Java层,Java层在调用其他方法时,比如start方法,需要把这个作为参数传入,通过reinterpret_cast强转为对应的类型,再进行相应的调用,如下:
jint NativeTest::start(JNIEnv* env, jclass cls,jlong test_address) { Test* test = reinterpret_cast<Test*>(test_address); if (test == NULL) { return -1; } return test->start(); }
|
C层回调
Java层调用了一个C的接口,C里面给Java一个回调是很常见的业务,具体代码如下:
TestCb::TestCb(JNIEnv* env, jobject jobject_callback) { jobject_callback_global_ = NULL; if (NULL != jobject_callback) { jclass jclass_callback = env->GetObjectClass(jobject_callback); if (NULL != jclass_callback) { jobject_callback_global_ = env->NewGlobalRef(jobject_callback); jclass_callback_global_ = (jclass) env->NewGlobalRef(jclass_callback);; env->DeleteLocalRef(jclass_callback); } } }
TestCb::~TestCb() { if (jobject_callback_global_) { env->DeleteGlobalRef(jobject_callback_global_); jobject_callback_global_ = NULL; } if (jclass_callback_global_) { env->DeleteGlobalRef(jclass_callback_global_); jclass_callback_global_ = NULL; } }
|
TestCb类里面保存了从Java层传入的对象指针jobject_callback,在构造函数中将其转化成全局的指针jobject_callback_global_和对应的类信息对象jclass_callback_global_,在Test类中进行回调时,通过jobject_callback_global_获取jmethodID,通过jclass_callback_global_找到对应的对象完成回调,如下:
void TestCb::onTest(long value) { jlong j_value = (jlong) value; jmethodID jmethod =env->GetMethodID(jclass_callback_global_, "onTest", "(J)V"); env->CallVoidMethod(jobject_callback_global_, jmethod,j_value); }
|
Java层实现
Java层的类主要是对C层实例的封装,让使用者感觉不到是C的实现,代码如下,在构造函数的时候调用createNativeTest来保存C层的指针,调用start方法时把nativeAddress传到C层,通过terminate方法来释放对应的C层指针
package com.test;
public class NativeTest { private long nativeAddress = 0L; private TestCallback testCallback = new TestCallback(); private boolean create() { if(0L != this.nativeAddress) { this.destroy(); } this.nativeAddress = createNativeTest(this.testCallback); return 0L != this.nativeAddress; } public NativeTest() { create(); } public int start() { return start(this.nativeAddress); } public int terminate() { int ret = terminate(this.nativeAddress); return destroy(); } private int destroy() { long nativeAddressTemp = this.nativeAddress; this.nativeAddress = 0L; return destroyNativeTest(nativeAddressTemp); }
private static native long createNativeTest(TestCallback callback); private static native int destroyNativeTest(long nativeAddress); private static native int start(long nativeAddress); }
|
回调类TestCallback是给C层调用的,在NativeTest初始化的时候把回调地址传给C层,TestCallback可以作为一个接口,这样更改回调实现时就不用更改C层代码
public class TestCallback { public void onTest(long value){ } }
|
调用Jni模块
实例化NativeTest,通过调用start和destroy来控制c层的内存初始化和释放,进行实例管理,防止内存泄露
NativeTest nativeTest = NativeTest.createNativeTest(new TestCallback(){ public void onTest(long value){ } }) nativeTest.start();
nativeTest.destroy();
|
资源释放
Jni在本地局部或全局引用没有释放,超过内存句柄限制时会导致“local reference table overflow”,jobject、jstring、jclass是常见的需要释放的类型,通过命名方式可以提醒程序员来释放相应资源
- jobject类型本地局部命名以“jobject_”开头,例如:jobject_arraylist
- jstring类型本地局部命名以“jstring_”开头,例如:jstring_id
- jclass类型本地局部命名以“jclass_”开头,例如:jclass_test