最近在研究ffmpeg这个视频库,需要使用到jni的接口,官网看了一遍jni函数注册的方法,感觉特别繁琐,每当新增一个native
方法时都要手动编写一个对应的cpp函数,并且cpp函数还要有命名约束,实在不方便。
并且静态注册还有以下弊端:
- 后期类名、文件名改动,头文件所有函数将失效,需要手动改,超级麻烦易出错
- 代码编写不方便,由于 JNI 层函数的名字必须遵循特定的格式,且名字特别长;
- 会导致程序员的工作量很大,因为必须为所有声明了 native 函数的 java 类编写 JNI 头文件;
- 程序运行效率低,因为初次调用 native 函数时需要根据根据函数名在 JNI 层中搜索对应的本地函数,然后建立对应关系,这个过程比较耗时。
一、JNI注册函数的流程
我们知道,在编写JNI函数的时候,都需要一个JNIEnv
对象,这个对象相当于JVM 代言人,持有java 环境变量指针,是一个包含了 JVM 接口的结构,它包含了与 JVM 进行交互以及与 Java 对象协同工作所必需的函数。
在JNI中,android还是使用JNIEnv
这个对象中的RegisterNatives
和UnregisterNatives
这两个方法来注册或取消注册JNI的函数。
jint RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods) { return functions->RegisterNatives(this, clazz, methods, nMethods); } jint UnregisterNatives(jclass clazz) { return functions->UnregisterNatives(this, clazz); }
|
从代码可以看出,如果你希望手动注册JNI方法,那么你需要jni方法所在类的java类路径,以及JNINativeMethod
而JNINativeMethod是一个描述java jni函数和c函数的对应关系的构造体。
typedef struct { const char* name; const char* signature; void* fnPtr; } JNINativeMethod;
|
再来看java加载so库的代码
System.loadLibrary("XXX");
|
在上面这句代码中,在加载so库的时候,系统首先会寻找JNI_OnLoad(JavaVM *vm, void *reserved)
方法,用于注册JNI函数,因此我们便可以重写该方法来覆盖android默认的JNI_OnLoad
来达到我们想要的效果。
有加载就会有释放,在jvm释放so库的时候,系统会调用JNI_OnUnload
这个方法来。
二、JNI手动注册实现
总结一下:所谓的动态注册,无非就是我们自己编写JNI_OnLoad
覆盖系统默认的注册函数,然后手动调用JNIEnv->RegisterNatives()
来注册JNI函数。
2.1、配置需要动态注册的类和JNI方法
在上面分析jvm加载so库的流程中,已经知道,注册jni函数,我们需要jni函数所在的java类的类路径,以及一个JNINativeMethod构造体。
因此第一步我们需要创建一个string字符串用于保存jni函数所在的java类路径,其次需要创建一个JNINativeMethod数组用于描述jni函数和c函数的对应关系。
static const char *jniClassName = "com/example/ndkstudy/Util";
static JNINativeMethod methods[] = { {"stringFromJNI", "()Ljava/lang/String;", (void *) stringFromJNI}, {"test", "()Ljava/lang/String;", (void *) jniTest}, };
|
将来如果需要在com/example/ndkstudy/Util
中新增jni函数,那么我们只需要在methods
新增一行映射关系。
2.2 覆盖系统的JNI_OnLoad
方法
这是核心步骤,利用c的特效,我们可以执行编写JNI_OnLoad
的实现来达到替换系统默认JNI_OnLoad
的实现。
static int registerUtil(JNIEnv *env) { jclass clazz = env->FindClass(jniClassName); if (clazz == nullptr) return JNI_FALSE; jint methodSize = sizeof(methods) / sizeof(methods[0]); if (env->RegisterNatives(clazz, methods, methodSize) < 0) return JNI_FALSE; return JNI_TRUE; }
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { JNIEnv *env = nullptr; jint result = -1; if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) return JNI_ERR; registerUtil(env); result = JNI_VERSION_1_6; return result; }
|
2.3 编写系统的JNI_OnUnload
函数
JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *reserved) { JNIEnv *env = nullptr; jint ret = vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6); if (ret != JNI_OK) { return; } }
|
三、完整代码
出于设计的考虑,一般来说,业务代码应该单独分离到其他文件中,在c的项目中也应该遵守这个规则。
3.1、业务代码,native-lib.cpp
#include <jni.h> #include <string> extern jstring stringFromJNI(JNIEnv *env, jobject obj) { std::string hello = "Hello from C++"; return env->NewStringUTF(hello.c_str()); } extern jstring jniTest(JNIEnv *env, jobject obj){ std::string test = "test"; return env->NewStringUTF(test.c_str()); }
|
需要注意的是,为了能让其他c文件使用业务函数,我们需要使用extern
关键字来声明业务函数
3.2、动态注册代码,jni_dynamic.cpp
然后我们就可以在动态注册函数的类中指定方法了
#include <jni.h> #include "native-lib.cpp" #include <string>
static const char *jniClassName = "com/example/ndkstudy/Util"; static const char *jniClassName1 = "com/example/ndkstudy/Util1";
static JNINativeMethod methods[] = { {"stringFromJNI", "()Ljava/lang/String;", (void *) stringFromJNI}, }; static JNINativeMethod methods1[] = { {"test", "()Ljava/lang/String;", (void *) jniTest}, };
static int registerUtil(JNIEnv *env) { jclass clazz = env->FindClass(jniClassName); if (clazz == nullptr) return JNI_FALSE; jint methodSize = sizeof(methods) / sizeof(methods[0]); if (env->RegisterNatives(clazz, methods, methodSize) < 0) return JNI_FALSE; return JNI_TRUE; } static int registerUtil1(JNIEnv *env) { jclass clazz = env->FindClass(jniClassName1); if (clazz == nullptr) return JNI_FALSE; jint methodSize = sizeof(methods1) / sizeof(methods1[0]); if (env->RegisterNatives(clazz, methods1, methodSize) < 0) return JNI_FALSE; return JNI_TRUE; }
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { JNIEnv *env = nullptr; jint result = -1; if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) return JNI_ERR; registerUtil(env); registerUtil1(env);
result = JNI_VERSION_1_6; return result; }
JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *reserved) { JNIEnv *env = nullptr; jint ret = vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6); if (ret != JNI_OK) { return; } }
|
3.3 编写ndk编译文件
由于一个.so中只能有一个JNI_OnLoad函数,因此我们需要修改ndk的编译文件。
在默认静态注册jni的方式中,mk文件中,我们只需要配置业务函数所在的cpp文件native-lib.cpp
就可以了。
而在使用动态注册jni的方式中,我们需要将native-lib.cpp
修改为动态注册函数所在的cpp文件jni_dynamic.cpp
。
我这使用的是cmake
的配置方式,cmake方式详情见我上一篇文章:https://www.laoyuyu.me/2019/04/12/android/cmake/
其他
项目结构
效果