JNI开发之方法签名与Java通信(二)

阿里P7移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680
本篇文章将通过以下两部分内容来介绍JNI开发:

  • Android NDK开发 JNI类型签名和方法签名
  • JNI实现java与c/c++相互通讯

一、Android NDK开发 JNI类型签名和方法签名

在Java存在两种数据类型: 基本类型 和 引用类型 ,大家都懂的 。

在JNI的世界里也存在类似的数据类型,与Java比较起来,其范围更具严格性,如下:

1、primitive types ----基本数据类型,如:int、 float 、char等基本类型

2、reference types----引用类型,如:类、实例、数组。
特别需要注意:数组 ------ 不管是对象数组还是基本类型数组,都作为reference types存在。
1、primitive types (基本数据类型)映射参见下表:

19956127-8f8f6519cc5bdcd3.png

这些基本数据类型都是可以在Native层直接使用的 。

2、reference types (引用数据类型)映射参见下表

19956127-ba5444e0a1f21db7.png

注意:
1、引用数据类型则不能直接使用,需要根据JNI函数进行相应的转换后,才能使用

2、多维数组(包括二维数组)都是引用类型,需要使用 jobjectArray 类型存取其值 ;
例如:二维整型数组就是指向一位数组的数组,其声明使用方式如下:

    //获得一维数组 的类引用,即jintArray类型
    jclass intArrayClass = env->FindClass("[I"); 
    //构造一个指向jintArray类一维数组的对象数组,该对象数组初始大小为dimion
    jobjectArray obejctIntArray  =  env->NewObjectArray(dimion ,intArrayClass , NULL);
    ...//具体操作
19956127-15514d9df2d7b8a7.png
类描述符

类描述符是类的完整名称(包名+类名),将原来的 . 分隔符换成 / 分隔符。

例如:在java代码中的java.lang.String类的类描述符就是java/lang/String
其实,在实践中,我发现可以直接用该类型的域描述符取代,也是可以成功的。
例如:

jclass intArrCls = env->FindClass("java/lang/String")

等同于

jclass intArrCls = env->FindClass("Ljava/lang/String;")

数组类型的描述符则为,则为: [ + 其类型的域描述符 (后文说明)
例如:

int [ ]     其描述符为[I

float [ ]   其描述符为[F

String [ ]  其描述符为[Ljava/lang/String;
域描述符

1、基本类型的描述符已经被定义好了,如下表所示:

19956127-cd8ec8d8bd4e031b.png

这里容易搞混淆的是Boolean和long为什么不取首字母。因为byte已经是B,所以B被占了,Boolean选择用Z。Long为什么不用L,因为L表示对象,被占用,选择用J

** 2、引用类型的描述符**

一般引用类型则为 L + 该类型类描述符 + ; (注意,这儿的分号“;”只得是JNI的一部分,而不是我们汉语中的分段,下同)
例如:String类型的域描述符为 Ljava/lang/String;
对于数组,其为 : [ + 其类型的域描述符 + ;

int[ ]     其描述符为[I
float[ ]   其描述符为[F
String[ ]  其描述符为[Ljava/lang/String;
Object[ ]类型的域描述符为[Ljava/lang/Object;

多维数组则是 n个[ +该类型的域描述符 , N代表的是几维数组。例如:

int  [ ][ ] 其描述符为[[I
float[ ][ ] 其描述符为[[F
方法描述符

将参数类型的域描述符按照申明顺序放入一对括号中后跟返回值类型的域描述符,规则如下: (参数的域描述符的叠加)返回类型描述符。对于,没有返回值的,用V(表示void型)表示。举例如下:

             Java层方法                                               JNI函数签名

            String test ( )                                         Ljava/lang/String;

            int f (int i, Object object)                            (ILjava/lang/Object;)I

            void set (byte[ ] bytes)                                ([B)V

二、JNI实现java与c/c++相互通讯

2.1、签名映射表

JNI获取Java类的方法和字段,都需要一个很重要的参数,就是Java类的方法和字段的签名。所以最好能够记住它们。


19956127-5878582835ea2ffa.png

"Lfully-qualified-class;"->L类全名; 例如Java String类对应的签名是Ljava/lang/String;

"[type"->java数组的签名,例如int[]的签名[I,java Stringg[]的签名是[Ljava/lang/String;

"(arg-types)ret-type"->(函数参数)返回值,()只是所有参数,ret-type是返回类型签名例如

void test(String msg)对应的签名是(Ljava/lang/String;)V

long f(int n,String s,int[] arr)对应的签名(Ijava/lang/String;[I)J

void f()对应用的签名()V

2.2、jni是如何访问java中的方法和字段

jni的native接口中,第一个参数为JNI接口指针(JNIEnv),第二个参数根据native方法是静态还是非静态而不同。非静态native方法的第二个参数是对该对象的引用,静态方法的第二参数是对其java类的引用。其与参数对应Java方法参数。

从上述描述中,如果是非静态的,我们可以拿到对象的引用。通过对象的引用,我们可以访问该对象的字段和方法。如果是静态的,我们可以访问该对象的静态方法和静态字段。那么jni中具体是如何访问的呢?我们将在下面的章节通过实例来介绍

2.3、jni访问java中的方法

java代码

public void show(String s){
    Log.i("MainActivity","show:"+s);
}
public native void showString(String s);

jni代码

//访问Java中的show方法
JNIEXPORT void JNICALL Java_com_zzy_ndkdemo_MainActivity_showString(JNIEnv *env, jobject instance, jstring s) {

    //获取instance的类名称
    jclass cls = (*env)->GetObjectClass(env,instance);
    if(cls==NULL)
    {
        LOGD("Class %s not found");
    }

    //获取方法ID,第二个参数为类名称,第三个参数为方法名称,第三个参数为方法签名,详细参见签名对照表
    jmethodID id =(*env)->GetMethodID(env,cls,"show","(Ljava/lang/String;)V");
    if(id !=NULL)
    {
        //访问方法,第二个为类实例,第三个参数为方法ID,第四和第四以后为方法参数,
        // 根据返回类型不同,调用不同的CallXXXMethod方法,xxx返回类型
        (*env)->CallVoidMethod(env,instance,id,s);
    }
}
2.4、jni访问java中的静态方法

java代码

public static void showStatic(String s){
    Log.i("MainActivity","show static:"+s);
}
public native void showStaticString(String s);

jni代码

//访问Java中的showStatic静态方法
JNIEXPORT void JNICALL Java_com_zzy_ndkdemo_MainActivity_showStaticString(JNIEnv *env, jobject instance, jstring s) {

    //获取instance的类名称
    jclass cls = (*env)->GetObjectClass(env,instance);
    if(cls==NULL)
    {
        LOGD("Class %s not found");
    }

    //获取静态方法ID,第二个参数为类名称,第三个参数为方法名称,第三个参数为方法签名,详细参见签名对照表
    jmethodID id =(*env)->GetStaticMethodID(env,cls,"showStatic","(Ljava/lang/String;)V");
    if(id !=NULL)
    {
        //访问方法,第二个为类名称,第三个参数为方法ID,第四和第四以后为方法参数,
        // 根据返回类型不同,调用不同的CallStaticXXXMethod方法,xxx返回类型
        (*env)->CallStaticVoidMethod(env,cls,id,s);
    }
}

1、从中我们可以看出静态方法比非静态方法的访问多加了一个static,访问函数由类实例,变成类引用。

2、如果静态方法不存在instance类中,我们可以通过FindClass进行访问,其中第二个参数为类的全路径

jclass cls = (*env)->FindClass(env, "com/zzy/ndkdemo/JniDemo");
if (cls == NULL) {
    LOGD("Class %s not found");
}
2.5、jni访问java中的字段

java代码

User user = new User();
user.name = "zhang san";
user.age = 30;
User.token = "2018-2011—3223";

String name = showUserName(user);
Log.i("MainActivity","show name:"+name);
public native String showUserName(User user);

jni代码

//访问User中的字段name
JNIEXPORT jstring JNICALL Java_com_zzy_ndkdemo_MainActivity_showUserName(JNIEnv *env, jobject instance, jobject user) {

    //获取User的类名称
    jclass cls=(*env)->GetObjectClass(env,user);
    if (cls == NULL) {
        LOGD("Class %s not found");
        return NULL;
    }

    //获取字段ID,第二个参数类名称,第三个参数字段名,第三个参数字段签名,详细参见签名对照表
    jfieldID id = (*env)->GetFieldID(env,cls,"name","Ljava/lang/String;");
    if(id==NULL)
    {
        LOGD("Field token not found");
        return NULL;
    }
    //获取字段内容,调用GetXXXField,xxx为字段类型。除基本类型外,其他的都使用GetObjectField
    jstring name=(jstring) (*env)->GetObjectField(env,user,id);
    return name;
}
2.6、jni访问Java中的静态字段

java代码

User user = new User();
user.name = "zhang san";
user.age = 30;
User.token = "2018-2011—3223";
String token =showUserStaticToken(user);
Log.i("MainActivity","show static token:"+token);
public native String showUserStaticToken(User user);

jni代码

//访问User中的静态字段Token
JNIEXPORT jstring JNICALL Java_com_zzy_ndkdemo_MainActivity_showUserStaticToken(JNIEnv *env, jobject instance, jobject user) {

    //获取User的类名称
    jclass cls=(*env)->GetObjectClass(env,user);
    if (cls == NULL) {
        LOGD("Class %s not found");
        return NULL;
    }

    //获取静态字段ID,第二个参数类名称,第三个参数字段名,第三个参数字段签名,详细参见签名对照表
    jfieldID id = (*env)->GetStaticFieldID(env,cls,"token","Ljava/lang/String;");
    if(id==NULL)
    {
        LOGD("Field token not found");
        return NULL;
    }

    //获取字段内容,调用GetStaticXXXField,xxx为字段类型。除基本类型外,其他的都使用GetStaticObjectField
    jstring token=(*env)->GetStaticObjectField(env,cls,id);
    return token;
}

1、从中我们可以看出静态字段比非静态字段的访问多加了一个static,访问函数由类实例,变成类引用。

2、如果静态字段中不存在instance类中,我们可以通过FindClass进行访问,其中第二个参数为类的全路径

jclass cls = (*env)->FindClass(env, "com/zzy/ndkdemo/User");
if (cls == NULL) {
    LOGD("Class %s not found");
}
2.7、jni中更新java的字段内容

java代码

User user = updateUser(user);
Log.i("MainActivity","updateUser name:"+user.name+" age:"+user.age+" token:"+User.token);
public native User updateUser(User user);

jni代码

//更新user内容
JNIEXPORT jobject JNICALL Java_com_zzy_ndkdemo_MainActivity_updateUser(JNIEnv *env, jobject instance,jobject user)
{
    //获取User的类名称
    jclass cls=(*env)->GetObjectClass(env,user);
    if (cls == NULL) {
        LOGD("Class %s not found");
        return NULL;
    }

    //获取每一个字段ID,第二个参数类名称,第三个参数字段名,第三个参数字段签名,详细参见签名对照表
    jfieldID idName = (*env)->GetFieldID(env,cls,"name","Ljava/lang/String;");
    jfieldID idAge = (*env)->GetFieldID(env,cls,"age","I");
    jfieldID idToken = (*env)->GetStaticFieldID(env,cls,"token","Ljava/lang/String;");

    //将cha字符串转成jstring
    jstring name = (*env)->NewStringUTF(env, "李四");
    jstring token = (*env)->NewStringUTF(env, "new token");

    //更新字段内容,调用setxxxField,xxx为字段类型,如果是静态字段还需要加上static
    //第二个参数为需要修改的类实例或则类名,第三个参数为字段Id,第四个参数为需要修改的内容
    (*env)->SetObjectField(env,user,idName,name);
    (*env)->SetIntField(env,user,idAge,20);
    (*env)->SetStaticObjectField(env,cls,idToken,token);

    return user;
}
2.8、jni中创建java类实例

java代码

User user = createUser();
Log.i("MainActivity","createUser name:"+user.name+" age:"+user.age+" token:"+User.token);
public native User createUser();

jni代码

//创建user实例
JNIEXPORT jobject JNICALL Java_com_zzy_ndkdemo_MainActivity_createUser(JNIEnv *env, jobject instance)
{
    //在指定路径查找到USer类名称
    jclass cls = (*env)->FindClass(env, "com/zzy/ndkdemo/User");
    if (cls == NULL) {
        LOGD("Class %s not found");
    }

    //获取User类的构造方法ID,第二个参数为类名称,第三个参数固定为"<init>"
    //第四个参数为构造函数签名,详细参见签名对照表
    jmethodID id= (*env)->GetMethodID(env, cls, "<init>", "()V");

    //实例化User类,第二个参数类名称,第三个参数构造方法ID
    jobject user = (*env)->NewObject(env, cls, id);
    if (user == NULL) {
        LOGD("Create User failed");
    }

    //获取每一个字段ID,第二个参数类名称,第三个参数字段名,第三个参数字段签名,详细参见签名对照表
    jfieldID idName = (*env)->GetFieldID(env,cls,"name","Ljava/lang/String;");
    jfieldID idAge = (*env)->GetFieldID(env,cls,"age","I");
    jfieldID idToken = (*env)->GetStaticFieldID(env,cls,"token","Ljava/lang/String;");

    //将cha字符串转成jstring
    jstring name = (*env)->NewStringUTF(env, "王五");
    jstring token = (*env)->NewStringUTF(env, "second token");

    //赋予字段内容,调用setxxxField,xxx为字段类型,如果是静态字段还需要加上static
    //第二个参数为需要修改的类实例或则类名,第三个参数为字段Id,第四个参数为需要修改的内容
    (*env)->SetObjectField(env,user,idName,name);
    (*env)->SetIntField(env,user,idAge,10);
    (*env)->SetStaticObjectField(env,cls,idToken,token);

    return user;
}
2.9、jni中的异常

JNI中也有异常,不过它和C++、Java的异常不太一样。当调用JNIEnv的某些函数出错后,会产生一个异常,但这个异常不会中断本地函数的执行,直到从JNI层返回到Java层后,虚拟机才会抛出这个异常。虽然在JNI层中产生的异常不会中断本地函数的运行,但一旦产生异常后,就只能做一些资源清理工作了(例如释放全局引用,或者ReleaseStringChars)。如果这时调用除上面所说函数之外的其他JNIEnv函数,则会导致程序死掉。JNIEnv提供了三个函数进行帮助:

1、ExceptionOccured函数,用来判断是否发生异常。

2、ExceptionClear函数,用来清理当前JNI层中发生的异常。

3、ThrowNew函数,用来向Java层抛出异常。

int jniCheckException(JNIEnv *env) {
    jthrowable ex = (*env)->ExceptionOccurred(env);
    if (ex) {
        (*env)->ExceptionDescribe(env);
        (*env)->ExceptionClear(env);
        (*env)->DeleteLocalRef(env, ex);
        return 1;
    }
    return 0;
}
2.10、常用函数封装

在JNI中访问方法和字段,都需要走好几个步骤,如果其中一个步骤失败了,就会导致整个访问过程的失败、甚至死机。所以我们在每个函数的调用过程中,尽量对其做异常检测。以下为常用函数的封装。

jobject jniGlobalRef(JNIEnv *env, jobject cls) {
    jobject gcls = (*env)->NewGlobalRef(env, cls);
    if (gcls == NULL)
        LOGE("Global ref failed (out of memory?)");
    return gcls;
}

jclass jniFindClass(JNIEnv *env, const char *name) {
    jclass cls = (*env)->FindClass(env, name);
    if (cls == NULL)
        LOGE("Class %s not found", name);
    else
        jniCheckException(env);
    return cls;
}

jmethodID jniGetMethodID(JNIEnv *env, jclass cls, const char *name, const char *signature) {
    jmethodID method = (*env)->GetMethodID(env, cls, name, signature);
    if (method == NULL) {
        LOGE("Method %s %s not found", name, signature);
        jniCheckException(env);
    }
    return method;
}

jfieldID jniGetFieldID(JNIEnv *env, jclass cls, const char *name, const char *type) {
    jfieldID field = (*env)->GetFieldID(env, cls, name, type);
    if (field == NULL)
        LOGE("Field %s type %s not found", name, type);
    return field;
}

jobject jniNewObject(JNIEnv *env, jclass cls, jmethodID constructor, const char *name) {
    jobject object = (*env)->NewObject(env, cls, constructor);
    if (object == NULL)
        LOGE("Create object %s failed", name);
    else
        jniCheckException(env);
    return object;
}
2.11、总结

JNI 程序开发者要遵循 native 语言本身的内存管理机制,避免造成内存泄漏。以 C 语言为例,当用 malloc() 在进程堆中动态分配内存时,JNI 程序在使用完后,应当调用 free() 将内存释放。总之,所有在 native 语言编程中应当注意的内存泄漏规则,在 JNI 编程中依然适应。Native 语言本身引入的内存泄漏会造成 native memory 的内存,严重情况下会造成 native memory 的 out of memory。如上述中使用到的NewStringUTF和NewObject在使用完成后,都需要进行释放。
Demo:https://github.com/zhao007z4/NDKDemo

参考 https://blog.csdn.net/zhao007z5/article/details/80066366
https://www.cnblogs.com/mingfeng002/p/6595047.html
阿里P7移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值