Android JNI(三)——JNI数据结构之JNINativeMethod

时间:2025-01-29 19:08:04

上俩篇,我们讲了都是jni基础的原理知识以及如何使用cmake实现简单的jni。接下来本想讲解Java与Native相互调用的,但是发现Java与Native相互调用中设计了好多基础知识,其中用的比较多的就是数据结构JNINativeMethod,所以,我想单独拿出来讲一下。

什么是JNINativeMethod

Andoird 中使用了一种不同传统Java JNI的方式来定义其native的函数。其中很重要区别是Andorid使用了一种Java 和 C 函数的映射表数组,并在其中描述了函数的参数和返回值。这个数组的类型是JNINativeMethod。

JNINativeMethod 结构体的官方定义

JNINativeMethod存在这个文件中,结构体如下:

typedef struct {
    const char* name;
    const char* signature;
    void*       fnPtr;
} JNINativeMethod;

下面就纤细介绍结构体参数的意思

  1. name
    变量name是Java中函数的名字
  2. signature
    变量signature,用字符串是描述了Java中函数的参数和返回值,这个参数最为复杂,接下来我会详细意义介绍。
  3. fnPtr
    变量fnPtr是函数指针,指向native函数。前面都要接 (void *)

注意:

第一个变量name与第三个变量fnPtr是对应的,一个是java层方法名,对应着第三个参数的native方法名字

下面就详细介绍第二个变量signature,也是最重要的。

变量signature细解

在讲变量signature变量前,我们先讲一下jni常用的数据类型,我们知道java有自己的基本数据类型,但是java的数据类型是不能直接和c/c++交互的,为了统一这个问题,jni也 给出了一套数据类型的于Java一一对应。

  1. 常用类型
字符 c/c++类型 Java类型
V void void
Z jboolean boolean
I jint int
J jlong long
D jdouble double
F jfloat float
B jbyte byte
C jchar char
S jshort short
  1. 复杂类型(数组)
    数组则以"["开始,用两个字符表示
名称 c/c++类型 Java类型
[I jintArray int[]
[F jfloatArray float[]
[B jbyteArray byte[]
[C jcharArray char[]
[S jshortArray short[]
[D jdoubleArray double[]
[J jlongArray long[]
[Z jbooleanArray boolean[]

上面的都是基本类型。如果Java函数的参数是class,则以"L"开头,以";" 结尾中间是用"/" 隔开的包及类名。而其对应的C函数名的参数则为jobject.

举一个例子:Java是String类,其对应的类为jstring:

Ljava/lang/String; String jstring
Ljava/net/Socket; Socket jobject

如果JAVA函数位于一个嵌入类,则用$作为类名间的分隔符。

 例如 "(Ljava/lang/String;Landroid/os/Utils$UtilsStatus;)Z"

好了,到此,jni的数据类型也讲完了,下面回过头来说signature变量,这样就更加的易懂。顾名思义,signature就是签名的意思。因为Java是支持函数重载的,也就是说,可以定义相同方法名,但是不同参数的方法,然后Java根据其不同的参数,找到其对应的实现的方法。这样是很好,所以说JNI肯定要支持的,那JNI要怎么支持那,如果仅仅是根据函数名,没有办法找到重载的函数的,所以为了解决这个问题,JNI就衍生了一个概念——“签名”,即将参数类型和返回值类型的组合。如果拥有一个该函数的签名信息和这个函数的函数名,我们就可以顺序的找到对应的Java层中的函数了。在接下来的一篇,我将会详细讲解签名,这里不过多讲解签名,只讲参数的用法,还是举个栗子说明:

/*  
 * 由于gMethods[]是一个<名称,函数指针>对照表,在程序执行时,  
 * 可多次调用registerNativeMethods()函数来更换本地函数的指针,  
 * 从而达到弹性调用本地函数的目的。   
 */    
static JNINativeMethod gMethods[] = {    
    {"setDataSource",       "(Ljava/lang/String;)V",            (void *)com_media_ffmpeg_FFMpegPlayer_setDataSource},    
    {"_setVideoSurface",    "(Landroid/view/Surface;)V",        (void *)com_media_ffmpeg_FFMpegPlayer_setVideoSurface},    
    {"prepare",             "()V",                              (void *)com_media_ffmpeg_FFMpegPlayer_prepare},    
    {"_start",              "()V",                              (void *)com_media_ffmpeg_FFMpegPlayer_start},    
    {"_stop",               "()V",                              (void *)com_media_ffmpeg_FFMpegPlayer_stop},    
    {"getVideoWidth",       "()I",                              (void *)com_media_ffmpeg_FFMpegPlayer_getVideoWidth},    
    {"getVideoHeight",      "()I",                              (void *)com_media_ffmpeg_FFMpegPlayer_getVideoHeight},    
    {"seekTo",              "(I)V",                             (void *)com_media_ffmpeg_FFMpegPlayer_seekTo},    
    {"_pause",              "()V",                              (void *)com_media_ffmpeg_FFMpegPlayer_pause},    
    {"isPlaying",           "()Z",                              (void *)com_media_ffmpeg_FFMpegPlayer_isPlaying},    
    {"getCurrentPosition",  "()I",                              (void *)com_media_ffmpeg_FFMpegPlayer_getCurrentPosition},    
    {"getDuration",         "()I",                              (void *)com_media_ffmpeg_FFMpegPlayer_getDuration},    
    {"_release",            "()V",                              (void *)com_media_ffmpeg_FFMpegPlayer_release},    
    {"_reset",              "()V",                              (void *)com_media_ffmpeg_FFMpegPlayer_reset},    
    {"setAudioStreamType",  "(I)V",                             (void *)com_media_ffmpeg_FFMpegPlayer_setAudioStreamType},    
    {"native_init",         "()V",                              (void *)com_media_ffmpeg_FFMpegPlayer_native_init},    
    {"native_setup",        "(Ljava/lang/Object;)V",            (void *)com_media_ffmpeg_FFMpegPlayer_native_setup},    
    {"native_finalize",     "()V",                              (void *)com_media_ffmpeg_FFMpegPlayer_native_finalize},    
    {"native_suspend_resume", "(Z)I",                           (void *)com_media_ffmpeg_FFMpegPlayer_native_suspend_resume},    
};      

比如上面列子中的这些参数:

"()V"

"(II)V"

"(Ljava/lang/String;Ljava/lang/String;)V"

括号里面表示参数的类型,括号后面表示返回值。

  1. “()”
    “()” 中的字符表示参数,后面的则代表返回值。例如"()V" 就表示void Fun();
  2. “(II)V”
    表示 void Func(int, int);参数是俩个整型。
  3. “(Ljava/lang/String;Ljava/lang/String;)V”
    表示 void Func(string, string);参数是俩个字符类型。

总结一下:

  • 对象类型
    以"L"开头,以";“结尾,中间是用”/" 隔开
  • 数组类型
    以"[“开始。如上表第2个(n维数组的话,则是前面多少个”[“而已,如”[[[D"表示“double[][][]”)
  • 对象数组类型
    上述两者结合

JNINativeMethod 简单实战

下面是我简单的JNINativeMethod实战,代码如下:

#include "log_util.h"
#include <>
#include ""
#include <>
#include <>

using namespace std;

#ifdef __cplusplus
extern "C" {
#endif

static const char *className = "com/bnd/multimedialearning/jni/Sample";

//native对应得函数
static void printHello(JNIEnv *env, jobject, jlong handle) {
    LOGI("JNI", "native function is print to hello");
}

//获取所有的native函数数组
static JNINativeMethod getMethods[] = {
        {"printHello", "(J)V", (void*)printHello},
};



//注册NativeMethods
static int jniRegisterNativeMethods(JNIEnv* env, const char* className,
                                    const JNINativeMethod* gMethods, int numMethods)
{
    jclass clazz;

    LOGI("JNI","Registering %s natives\n", className);
    clazz = (env)->FindClass( className);
    if (clazz == NULL) {
        LOGE("JNI","Native registration unable to find class '%s'\n", className);
        return -1;
    }

    int result = 0;
    if ((env)->RegisterNatives(clazz, getMethods, numMethods) < 0) {
        LOGE("JNI","RegisterNatives failed for '%s'\n", className);
        result = -1;
    }

    (env)->DeleteLocalRef(clazz);
    return result;
}

//JNI_OnLoad动态注册:动态注册通过RegisterNatives方法把C/C++中的方法映射到Java中的native方法
//注意:
//当我们使用()方法加载so库的时候,Java虚拟机就会找到这个JNI_OnLoad函数兵调用该函数,这个函数的作用是告诉Dalvik虚拟机此C库使用的是哪一个JNI版本,如果你的库里面没有写明JNI_OnLoad()函数,VM会默认该库使用最老的JNI 1.1版本。
// 由于最新版本的JNI做了很多扩充,也优化了一些内容,如果需要使用JNI新版本的功能,就必须在JNI_OnLoad()函数声明JNI的版本。同时也可以在该函数中做一些初始化的动作,其实这个函数有点类似于Android中的Activity中的onCreate()方法。该函数前面也有三个关键字分别是JNIEXPORT,JNICALL,jint。
// 其中JNIEXPORT和JNICALL是两个宏定义,用于指定该函数时JNI函数。jint是JNI定义的数据类型,因为Java层和C/C++的数据类型或者对象不能直接相互的引用或者使用,JNI层定义了自己的数据类型,jint就是用于衔接Java层和JNI层。

jint JNI_OnLoad(JavaVM* vm, void* reserved){
    LOGI("JNI", "enter jni_onload");

    JNIEnv* env = NULL;
    jint result = -1;

    if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
        return result;
    }

    //获取所有的函数的个数
    jint size = sizeof(getMethods) / sizeof(JNINativeMethod);
    LOGI("JNI", "jint size is '%s'\n", size);
    jniRegisterNativeMethods(env, className, getMethods, size);

    return JNI_VERSION_1_4;
}

#ifdef __cplusplus
}
#endif

com_bnd_multimedialearning_jni_Sample_printHello的函数原型为:

static jobject com_bnd_multimedialearning_jni_Sample_printHello(JNIEnv *env, jobject thiz, jstring path, jint baudrate);

注意:如果是C++,使用的是env, 如果是C,使用的是(*env),最好参考相应系统中的代码来写。

好了,关于结构体JNINativeMethod的讲述就告一段路了,接下来我将会讲解Java如何和c/c++相互调用,如果你对jni还没有入门,建议先看一下我的前倆篇博客。Android JNI(一)——NDK与JNI基础Android JNI(二)——实战JNI之Hello World