最近在研究ffmpeg这个视频库,需要使用到jni的接口,官网看了一遍jni函数注册的方法,感觉特别繁琐,每当新增一个native方法时都要手动编写一个对应的cpp函数,并且cpp函数还要有命名约束,实在不方便。
并且静态注册还有以下弊端:

  • 后期类名、文件名改动,头文件所有函数将失效,需要手动改,超级麻烦易出错
  • 代码编写不方便,由于 JNI 层函数的名字必须遵循特定的格式,且名字特别长;
  • 会导致程序员的工作量很大,因为必须为所有声明了 native 函数的 java 类编写 JNI 头文件;
  • 程序运行效率低,因为初次调用 native 函数时需要根据根据函数名在 JNI 层中搜索对应的本地函数,然后建立对应关系,这个过程比较耗时。

一、JNI注册函数的流程

我们知道,在编写JNI函数的时候,都需要一个JNIEnv对象,这个对象相当于JVM 代言人,持有java 环境变量指针,是一个包含了 JVM 接口的结构,它包含了与 JVM 进行交互以及与 Java 对象协同工作所必需的函数。
在JNI中,android还是使用JNIEnv这个对象中的RegisterNativesUnregisterNatives这两个方法来注册或取消注册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; //Java jni函数的名称
const char* signature; //描述了函数的参数和返回值
void* fnPtr; //函数指针,jni函数的对应的c函数
} 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函数的对应关系。

/**
* 指定需要动态注册的类,需要注意的是,一个.so中只能有一个JNI_OnLoad
*/
static const char *jniClassName = "com/example/ndkstudy/Util";
/**
* jni函数和c函数的对应关系
*/
static JNINativeMethod methods[] = {
// 三个参数分别为:jni函数的函数名,jni函数返回属性(return xx), 该jni函数对应的c函数
{"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的实现。

/**
* 注册方法,注册com/example/ndkstudy/Util类中所有的jni函数
*/
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;
}
/**
* System.loadLibrary 加载完 JNI 动态库之后,调用 JNI_OnLoad 函数,开始动态注册
*/
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;
//注册方法,如过你有多个不同多含有JNI函数多java类,那么你需要逐个注册
registerUtil(env);
//registerUtil1(env);
result = JNI_VERSION_1_6;
return result;
}

2.3 编写系统的JNI_OnUnload函数

/**
* 当 VM 释放该组件时会调用 JNI_OnUnload 方法
* @param vm
* @param reserved
*/
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

然后我们就可以在动态注册函数的类中指定方法了

// Created by aria on 2019/4/12.

#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";
/**
* jni函数和c函数的对应关系
*/
static JNINativeMethod methods[] = {
{"stringFromJNI", "()Ljava/lang/String;", (void *) stringFromJNI},
};
static JNINativeMethod methods1[] = {
{"test", "()Ljava/lang/String;", (void *) jniTest},
};
/**
* 注册方法,注册com/example/ndkstudy/Util类中所有的jni函数
*/
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;
}
/**
* System.loadLibrary 加载完 JNI 动态库之后,调用 JNI_OnLoad 函数,开始动态注册,需要注意的是,一个.so中只能有一个JNI_OnLoad
*/
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);

// return JNI_ERR;
result = JNI_VERSION_1_6;
return result;
}
/**
* 当 VM 释放该组件时会调用 JNI_OnUnload 方法
* @param vm
* @param reserved
*/
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/

其他

项目结构

效果

demo