JNI动态注册native方法及JNI数据使用详解手机开发

前言

或许你知道了jni的简单调用,其实不算什么百度谷歌一大把,虽然这些jni绝大多数情况下都不会让我们安卓工程师来弄,毕竟还是有点难,但是我们还是得打破砂锅知道为什么这样干吧,至少也让我们知道调用流程和数据类型以及处理方法,或许你会有不一样的发现。

今天也给出一个JNI动态注册native方法的例子,如图: 
JNI动态注册native方法及JNI数据使用详解手机开发

JNI实现步骤

JNI 开发流程主要分为以下步骤:

  • 编写声明了 native 方法的 Java 类
  • 将 Java 源代码编译成 class 字节码文件
  • 用 javah -jni 命令生成.h头文件(javah 是 jdk 自带的一个命令,-jni 参数表示将 class 中用native 声明的函数生成 JNI 规则的函数)
  • 用本地代码(c/c++)实现.h头文件中的函数
  • 将(c/c++)文件编译成动态库(Windows:*.dll,linux/unix:*.so,mac os x:*.jnilib)
  • 拷贝动态库至本地库目录下,并运行 Java 程序(System.loadLibrary(“xxx”))

我们安卓开发工程师显然只需要编写native的java类,然后clean下编译器知道把我们的java编译成了class文件,但是我们必须知道是调用了javac命令,javah jni命令我们还是得执行,其他的工作就差不多了,不管是什么编译器,反正jni步骤就这样。

JVM 查找 native 方法

JVM 查找 native 方法有两种方式:

  • 按照 JNI 规范的命名规则
  • 调用 JNI 提供的 RegisterNatives 函数,将本地函数注册到 JVM 中。

是不是感到特别的意外,jni还能够利用RegisterNatives 函数查找native方法,其实我也才刚刚知道有这方法,因为要根据包名类名方法名的规范来写是很傻逼的,哈哈,有的人或许觉得这样很直观。

严格按照命名规则实现native方法的调用

我们还是按步骤来说吧,先来解读JNI规范的命名规则:

* 我们先来看下.h文件 *

 
#include  
 
#ifndef _Included_com_losileeya_jnimaster_JNIUtils 
#define _Included_com_losileeya_jnimaster_JNIUtils 
#ifdef __cplusplus 
extern  { 
#endif 
 
JNIEXPORT jstring JNICALL  Java_com_losileeya_jnimaster_JNIUtils_say 
        (JNIEnv *, jclass,jstring); 
#ifdef __cplusplus 
} 
#endif 
#endif

我们再来看下Linux 下jni_md.h头文件内容:

#ifndef _JAVASOFT_JNI_MD_H_   
#define _JAVASOFT_JNI_MD_H_   
#define JNIEXPORT   
#define JNIIMPORT   
#define JNICALL   
typedef int jint;   
#ifdef _LP64 /* 64-bit Solaris */   
typedef long jlong;   
#else   
typedef long long jlong;   
#endif   
typedef signed char jbyte;   
#endif  

从上面我们可以看出文件以#ifndef开始然后#endif 结尾,不会C的话是不是看起来有点蛋疼,#号呢代表宏,这里来普及一下宏的使用和定义。

#define 标识符 字符串 
其中,#表示这是一条预处理命令;#define为宏定义命令;“标识符”为宏定义的宏名;“字符串”可以上常数、表达式、格式串等。

举例如下:

#define PI 3.14  
void main() 
{ 
printf(, PI);  
}

条件编译的命令

#ifndef def 
语句1 
# else 
语句2 
# endif 
表示如果def在前面进行了宏定义那么就编译语句1(语句2不编译),否则编译语句2(语句1不编译)

再看我们.h文件并没有else,所以我们就编译宏定义的本地方法类(com_losileeya_jnimaster_JNIUtils),你突然就会发现我们的宏是我们的native类,然后把包名点类名的点改成了下划线,然后你会发现多了_Included不要多想,就是included关键字加个下划线,这样我们就给本地类进行了宏定义。然后

#ifdef __cplusplus 
extern “C” {#endif

这是说明如果宏定义了c++,并且里面有c我们还是支持c的,并且c代码写extern “C” {}里面。可以看出#endif对应上面的#ifdef-cplusplus,#ifdef-cplusplus对应最后的#endif, #ifdef与#endif总是一一对应的,表明条件编译开始和结束。

JNIEXPORT 和 JNICALL 的作用 
因为安卓是跑在 Linux 下的,所以从 Linux 下的jni_md.h头文件可以看出来,JNIEXPORT 和 JNICALL 是一个空定义,所以在 Linux 下 JNI 函数声明可以省略这两个宏。 
再来看我们的方法:

JNIEXPORT jstring JNICALL  Java_com_losileeya_jnimaster_JNIUtils_say 
        (JNIEnv *, jclass,jstring)

函数命名规则为:Java_类全路径_方法名。 
如:Java_com_losileeya_jnimaster_JNIUtils_say,其中Java_是函数的前缀,com_losileeya_jnimaster_JNIUtils是类名,say是方法名,它们之间用 _(下划线) 连接。

  • 第一个参数:JNIEnv* 是定义任意 native 函数的第一个参数(包括调用 JNI 的 RegisterNatives 函数注册的函数),指向 JVM 函数表的指针,函数表中的每一个入口指向一个 JNI 函数,每个函数用于访问 JVM 中特定的数据结构。
  • 第二个参数:调用 Java 中 native 方法的实例或 Class 对象,如果这个 native 方法是实例方法,则该参数是 jobject,如果是静态方法,则是 jclass。
  • 第三个参数:Java 对应 JNI 中的数据类型,Java 中 String 类型对应 JNI 的 jstring 类型。(后面会详细介绍 JAVA 与 JNI 数据类型的映射关系)。

函数返回值类型:夹在 JNIEXPORT 和 JNICALL 宏中间的 jstring,表示函数的返回值类型,对应 Java 的String 类型。

如果你需要装逼的话你就可以自己去写.h文件,然后就可以抛弃javah -jni 命令,只需要按照函数命名规则编写相应的函数原型和实现即可(逼就是这么装出来的)

RegisterNatives动态获取本地方法

是不是感觉一个方法的名字太长非常的蛋疼,然后我们呢直接使用,RegisterNatives来自己命名调用native方法,这样是不是感觉好多了。

要实现呢,我们必须重写JNI_OnLoad()方法这样就会当调用 System.loadLibrary(“XXXX”)方法的时候直接来调用JNI_OnLoad(),这样就达到了动态注册实现native方法的作用。

/* 
* System.loadLibrary()时调用 
* 如果成功返回JNI版本, 失败返回-1 
*/ 
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { 
    JNIEnv* env = NULL; 
    jint result = -1; 
    if ((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK) { 
        return -1; 
    } 
    assert(env != NULL); 
    if (!registerNatives(env)) {//注册 
        return -1; 
    } 
    //成功 
    result = JNI_VERSION_1_4; 
    return result; 
}

并且我们需要为类注册本地方法,那样就能方便我们去调用,不多说看方法:

 
static int registerNatives(JNIEnv* env) { 
    return registerNativeMethods(env, JNIREG_CLASS, gMethods,sizeof(gMethods) / sizeof(gMethods[0])); 
}

也可以为某一个类注册本地方法

 
static int registerNativeMethods(JNIEnv* env 
        , const char* className 
        , JNINativeMethod* gMethods, int numMethods) { 
    jclass clazz; 
    clazz = (*env)->FindClass(env, className); 
    if (clazz == NULL) { 
        return JNI_FALSE; 
    } 
    if ((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0) { 
        return JNI_FALSE; 
    } 
    return JNI_TRUE; 
}

JNINativeMethod 结构体的官方定义

typedef struct {   
  const char* name;   
  const char* signature;   
  void* fnPtr;   
} JNINativeMethod;  
  • 第一个变量name是Java中函数的名字。
  • 第二个变量signature,用字符串是描述了Java中函数的参数和返回值
  • 第三个变量fnPtr是函数指针,指向native函数。前面都要接 (void *)

    第一个变量与第三个变量是对应的,一个是java层方法名,对应着第三个参数的native方法名字(不明白请看后面代码就会清楚了)。

哈哈最后我们就把native方法绑定到JNINativeMethod上我们来看下事例:

static JNINativeMethod gMethods[] = {     
    {,       ,            (void *)com_media_ffmpeg_FFMpegPlayer_setDataSource},     
    {,    ,        (void *)com_media_ffmpeg_FFMpegPlayer_setVideoSurface},     
    {,             ,                              (void *)com_media_ffmpeg_FFMpegPlayer_prepare},     
    {,              ,                              (void *)com_media_ffmpeg_FFMpegPlayer_start},     
    {,               ,                              (void *)com_media_ffmpeg_FFMpegPlayer_stop},     
    {,       ,                              (void *)com_media_ffmpeg_FFMpegPlayer_getVideoWidth},     
    {,      ,                              (void *)com_media_ffmpeg_FFMpegPlayer_getVideoHeight},     
    {,              ,                             (void *)com_media_ffmpeg_FFMpegPlayer_seekTo},     
    {,              ,                              (void *)com_media_ffmpeg_FFMpegPlayer_pause},     
    {,           ,                              (void *)com_media_ffmpeg_FFMpegPlayer_isPlaying},     
    {,  ,                              (void *)com_media_ffmpeg_FFMpegPlayer_getCurrentPosition},     
    {,         ,                              (void *)com_media_ffmpeg_FFMpegPlayer_getDuration},     
    {,            ,                              (void *)com_media_ffmpeg_FFMpegPlayer_release},     
    {,              ,                              (void *)com_media_ffmpeg_FFMpegPlayer_reset},     
    {,  ,                             (void *)com_media_ffmpeg_FFMpegPlayer_setAudioStreamType},     
    {,         ,                              (void *)com_media_ffmpeg_FFMpegPlayer_native_init},     
    {,        ,            (void *)com_media_ffmpeg_FFMpegPlayer_native_setup},     
    {,     ,                              (void *)com_media_ffmpeg_FFMpegPlayer_native_finalize},     
    {, ,                           (void *)com_media_ffmpeg_FFMpegPlayer_native_suspend_resume},     
};    

第一个参数就是我们写的方法,第三个就是.h文件里面的方法,第二个参数显得有点难度,这里会主要去讲。 
主要是第二个参数比较复杂:

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

  • “()” 中的字符表示参数,后面的则代表返回值。例如”()V” 就表示void * Fun();
  • “(II)V” 表示 void Fun(int a, int b);
  • “(II)I” 表示 int sum(int a, int b);

这些字符与函数的参数类型的映射表如下: 
字符 Java类型 C类型 
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

数组则以”[“开始,用两个字符表示

[I jintArray int[] 
[F jfloatArray float[] 
[B jbyteArray byte[] 
[C jcharArray char[] 
[S jshortArray short[] 
[D jdoubleArray double[] 
[J jlongArray long[] 
[Z jbooleanArray boolean[] 
如图: 
这里写图片描述

  • 对象类型:以”L”开头,以”;”结尾,中间是用”/” 隔开。如上表第1个
  • 数组类型:以”[“开始。如上表第2个(n维数组的话,则是前面多少个”[“而已,如”[[[D”表示“double[][][]”)
  • 如果Java函数的参数是class,则以”L”开头,以”;”结尾中间是用”/” 隔开的包及类名。而其对应的C函数名的参数则为jobject. 一个例外是String类,其对应的类为jstring

    Ljava/lang/String; String jstring 
    Ljava/net/Socket; Socket jobject 
    这里写图片描述 
    如果JAVA函数位于一个嵌入类,则用作为类名间的分隔符。例如“(Ljava/lang/String;Landroid/os/FileUtilsFileStatus;)Z”

好了,所有 的介绍也完了,那么我们就来实现我们的代码:(果断把h文件删除,看效果) 
JNIUtil.c:

 
 
 
 
 
 
jstring call(JNIEnv* env, jobject thiz) 
{ 
    return (*env)->NewStringUTF(env, ); 
} 
jint sum(JNIEnv* env, jobject jobj,jint num1,jint num2){ 
    return num1+num2; 
} 
/** 
* 方法对应表 
*/ 
static JNINativeMethod gMethods[] = { 
        {, , (void*)call}, 
        {, , (void*)sum}, 
}; 
 
/* 
* 为某一个类注册本地方法 
*/ 
static int registerNativeMethods(JNIEnv* env 
        , const char* className 
        , JNINativeMethod* gMethods, int numMethods) { 
    jclass clazz; 
    clazz = (*env)->FindClass(env, className); 
    if (clazz == NULL) { 
        return JNI_FALSE; 
    } 
    if ((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0) { 
        return JNI_FALSE; 
    } 
 
    return JNI_TRUE; 
} 
 
 
/* 
* 为所有类注册本地方法 
*/ 
static int registerNatives(JNIEnv* env) { 
    return registerNativeMethods(env, JNIREG_CLASS, gMethods, 
                                 sizeof(gMethods) / sizeof(gMethods[0])); 
} 
 
/* 
* System.loadLibrary()时调用 
* 如果成功返回JNI版本, 失败返回-1 
*/ 
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { 
    JNIEnv* env = NULL; 
    jint result = -1; 
    if ((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK) { 
        return -1; 
    } 
    assert(env != NULL); 
    if (!registerNatives(env)) {//注册 
        return -1; 
    } 
    //成功 
    result = JNI_VERSION_1_4; 
    return result; 
}

代码写完了,主要也是利用findClass来获取方法,从而实现方法的调用。效果重现: 
JNI动态注册native方法及JNI数据使用详解手机开发

demo 传送梦:RegisterNatives.rar

JNI数据类型及常用方法(JNI安全手册)

基本类型和本地等效类型表:

这里写图片描述

引用类型: 
这里写图片描述

接口函数表:

const struct JNINativeInterface ... = { 
    NULL, 
    NULL, 
    NULL, 
    NULL, 
    GetVersion, 
 
    DefineClass, 
    FindClass, 
    NULL, 
    NULL, 
    NULL, 
    GetSuperclass, 
    IsAssignableFrom, 
    NULL, 
 
    Throw, 
    ThrowNew, 
    ExceptionOccurred, 
    ExceptionDescribe, 
    ExceptionClear, 
    FatalError, 
    NULL, 
    NULL, 
 
    NewGlobalRef, 
    DeleteGlobalRef, 
    DeleteLocalRef, 
    IsSameObject, 
    NULL, 
    NULL, 
    AllocObject, 
 
    NewObject, 
    NewObjectV, 
    NewObjectA, 
    GetObjectClass, 
 
    IsInstanceOf, 
 
    GetMethodID, 
 
    CallObjectMethod, 
    CallObjectMethodV, 
    CallObjectMethodA, 
    CallBooleanMethod, 
    CallBooleanMethodV, 
    CallBooleanMethodA, 
    CallByteMethod, 
    CallByteMethodV, 
    CallByteMethodA, 
    CallCharMethod, 
    CallCharMethodV, 
    CallCharMethodA, 
    CallShortMethod, 
    CallShortMethodV, 
    CallShortMethodA, 
    CallIntMethod, 
    CallIntMethodV, 
    CallIntMethodA, 
    CallLongMethod, 
    CallLongMethodV, 
    CallLongMethodA, 
    CallFloatMethod, 
    CallFloatMethodV, 
    CallFloatMethodA, 
    CallDoubleMethod, 
    CallDoubleMethodV, 
    CallDoubleMethodA, 
    CallVoidMethod, 
    CallVoidMethodV, 
    CallVoidMethodA, 
 
    CallNonvirtualObjectMethod, 
    CallNonvirtualObjectMethodV, 
    CallNonvirtualObjectMethodA, 
    CallNonvirtualBooleanMethod, 
    CallNonvirtualBooleanMethodV, 
    CallNonvirtualBooleanMethodA, 
    CallNonvirtualByteMethod, 
    CallNonvirtualByteMethodV, 
    CallNonvirtualByteMethodA, 
    CallNonvirtualCharMethod, 
    CallNonvirtualCharMethodV, 
    CallNonvirtualCharMethodA, 
    CallNonvirtualShortMethod, 
    CallNonvirtualShortMethodV, 
    CallNonvirtualShortMethodA, 
    CallNonvirtualIntMethod, 
    CallNonvirtualIntMethodV, 
    CallNonvirtualIntMethodA, 
    CallNonvirtualLongMethod, 
    CallNonvirtualLongMethodV, 
    CallNonvirtualLongMethodA, 
    CallNonvirtualFloatMethod, 
    CallNonvirtualFloatMethodV, 
    CallNonvirtualFloatMethodA, 
    CallNonvirtualDoubleMethod, 
    CallNonvirtualDoubleMethodV, 
    CallNonvirtualDoubleMethodA, 
    CallNonvirtualVoidMethod, 
    CallNonvirtualVoidMethodV, 
    CallNonvirtualVoidMethodA, 
 
    GetFieldID, 
 
    GetObjectField, 
    GetBooleanField, 
    GetByteField, 
    GetCharField, 
    GetShortField, 
    GetIntField, 
    GetLongField, 
    GetFloatField, 
    GetDoubleField, 
    SetObjectField, 
    SetBooleanField, 
    SetByteField, 
    SetCharField, 
    SetShortField, 
    SetIntField, 
    SetLongField, 
    SetFloatField, 
    SetDoubleField, 
    GetStaticMethodID, 
    CallStaticObjectMethod, 
    CallStaticObjectMethodV, 
    CallStaticObjectMethodA, 
    CallStaticBooleanMethod, 
    CallStaticBooleanMethodV, 
    CallStaticBooleanMethodA, 
    CallStaticByteMethod, 
    CallStaticByteMethodV, 
    CallStaticByteMethodA, 
    CallStaticCharMethod, 
    CallStaticCharMethodV, 
    CallStaticCharMethodA, 
    CallStaticShortMethod, 
    CallStaticShortMethodV, 
    CallStaticShortMethodA, 
    CallStaticIntMethod, 
    CallStaticIntMethodV, 
    CallStaticIntMethodA, 
    CallStaticLongMethod, 
    CallStaticLongMethodV, 
    CallStaticLongMethodA, 
    CallStaticFloatMethod, 
    CallStaticFloatMethodV, 
    CallStaticFloatMethodA, 
    CallStaticDoubleMethod, 
    CallStaticDoubleMethodV, 
    CallStaticDoubleMethodA, 
    CallStaticVoidMethod, 
    CallStaticVoidMethodV, 
    CallStaticVoidMethodA, 
    GetStaticFieldID, 
    GetStaticObjectField, 
    GetStaticBooleanField, 
    GetStaticByteField, 
    GetStaticCharField, 
    GetStaticShortField, 
    GetStaticIntField, 
    GetStaticLongField, 
    GetStaticFloatField, 
    GetStaticDoubleField, 
    SetStaticObjectField, 
    SetStaticBooleanField, 
    SetStaticByteField, 
    SetStaticCharField, 
    SetStaticShortField, 
    SetStaticIntField, 
    SetStaticLongField, 
    SetStaticFloatField, 
    SetStaticDoubleField, 
    NewString, 
    GetStringLength, 
    GetStringChars, 
    ReleaseStringChars, 
    NewStringUTF, 
    GetStringUTFLength, 
    GetStringUTFChars, 
    ReleaseStringUTFChars, 
    GetArrayLength, 
    NewObjectArray, 
    GetObjectArrayElement, 
    SetObjectArrayElement, 
    NewBooleanArray, 
    NewByteArray, 
    NewCharArray, 
    NewShortArray, 
    NewIntArray, 
    NewLongArray, 
    NewFloatArray, 
    NewDoubleArray, 
    GetBooleanArrayElements, 
    GetByteArrayElements, 
    GetCharArrayElements, 
    GetShortArrayElements, 
    GetIntArrayElements, 
    GetLongArrayElements, 
    GetFloatArrayElements, 
    GetDoubleArrayElements, 
    ReleaseBooleanArrayElements, 
    ReleaseByteArrayElements, 
    ReleaseCharArrayElements, 
    ReleaseShortArrayElements, 
    ReleaseIntArrayElements, 
    ReleaseLongArrayElements, 
    ReleaseFloatArrayElements, 
    ReleaseDoubleArrayElements, 
    GetBooleanArrayRegion, 
    GetByteArrayRegion, 
    GetCharArrayRegion, 
    GetShortArrayRegion, 
    GetIntArrayRegion, 
    GetLongArrayRegion, 
    GetFloatArrayRegion, 
    GetDoubleArrayRegion, 
    SetBooleanArrayRegion, 
    SetByteArrayRegion, 
    SetCharArrayRegion, 
    SetShortArrayRegion, 
    SetIntArrayRegion, 
    SetLongArrayRegion, 
    SetFloatArrayRegion, 
    SetDoubleArrayRegion, 
    RegisterNatives, 
    UnregisterNatives, 
    MonitorEnter, 
    MonitorExit, 
    GetJavaVM, 
};

基本上jni的数据和方法都差不多放这里了,你就可以随便开发了。这个你也可以去看 
jni完全手册

JNI与C/C++数据类型的转换(效率开发)

字符数组与jbyteArray

int byteSize = (int) env->GetArrayLength(jbyteArrayData);   
unsigned char* data = new unsigned char[byteSize + 1]; 
env->GetByteArrayRegion(jbyteArrayData, 0, byteSize, reinterpret_cast(data)); 
data[byteSize] = '/0';
jbyte *jb =  (jbyte*) data;   //data是字符数组类型 
jbyteArray jarray = env->NewByteArray(byteSize);   //byteSize是字符数组大小 
env->SetByteArrayRegion(jarray, 0, byteSize, jb);

字符数组与jstring

char* JstringToChar(JNIEnv* env, jstring jstr) { 
    if(jstr == NULL) { 
        return NULL; 
    } 
    char* rtn = NULL; 
    jclass clsstring = env->FindClass(); 
    jstring strencode = env->NewStringUTF(); 
    jmethodID mid = env->GetMethodID(clsstring, , 
            ); 
    jbyteArray barr = (jbyteArray) env->CallObjectMethod(jstr, mid, strencode); 
    jsize alen = env->GetArrayLength(barr); 
    jbyte* ba = env->GetByteArrayElements(barr, JNI_FALSE); 
    if (alen > 0) { 
        rtn = (char*) malloc(alen + 1); 
        memcpy(rtn, ba, alen); 
        rtn[alen] = 0; 
    } 
    env->ReleaseByteArrayElements(barr, ba, 0); 
    return rtn; 
}
jstring StrtoJstring(JNIEnv* env, const char* pat) 
{ 
    jclass strClass = env->FindClass(); 
    jmethodID ctorID = env->GetMethodID(strClass, , ); 
    jbyteArray bytes = env->NewByteArray(strlen(pat)); 
    env->SetByteArrayRegion(bytes, 0, strlen(pat), (jbyte*)pat); 
    jstring encoding = env->NewStringUTF(); 
    return (jstring)env->NewObject(strClass, ctorID, bytes, encoding); 
}

特么最简单的可以直接使用

jstring jstr = env->NewStringUTF(str); 

jint与int的互转都可以直接使用强转,如:

jint i = (jint) 1024;

上面的代码你看见了吗,都是env的一级指针来做的,所以是cpp的使用方法,如果你要转成c的那么就把env替换为(*env)好了,具体的方法可能有点小改动(请自行去参考jni手册),报错的地方请自行引入相关的.h文件,估计对你了解jni有更深的了解。

总结

本篇主要介绍了JNI动态注册native方法,并且顺便截了几个jni的图,以及使用的基本数据转换处理,至于实际应用中比如java 调用c,c调用java以及混合调用等我们都需要实践中去处理问题。

原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/5987.html

(0)
上一篇 2021年7月17日
下一篇 2021年7月17日

相关推荐

发表回复

登录后才能评论