我们在上一篇文章《Android中JNI入门(一) 之 初识NDK和JNI》中已经通过Demo演示了如何在Java代码中去调用C++代码,今天继续来看看在JNI中是如何反调用Java方法的。先大概提一下,JNI中要调用Java方法的流程是先通过类名找到类,然后再根据方法名找到方法的id,最后就可以调用这个方法了。如果是调用Java中的非静态方法,那么就需要先构造出类的对象后才能调用它。其实实现跟我们平时在Java中使用反射是非常相似的,下面通过扩展上一篇文章中的Demo来看看JNI中反调用Java方法是实现。
1 扩展Hello world
修改JNIUtils.java,使其增加一个静态方法供JNI中去反调用:
public class JNIUtils {
static {
System.loadLibrary("jni-demo");
}
public static native String getInfo();
public static String getJavaInfo(String info, int index) {
return info + index;
}
}
修改JNIUtils.cpp:
#include "com_zyx_jnidemo_JNIUtils.h"
#include <stdio.h>
#include <android/log.h>
JNIEXPORT jstring JNICALL Java_com_zyx_jnidemo_JNIUtils_getInfo(JNIEnv * env, jclass thiz) {
__android_log_print(ANDROID_LOG_DEBUG, "zyx", "Hello world from JNI !");
//return env->NewStringUTF("Hello world from JNI !");
jclass clazz = env->FindClass("com/zyx/jnidemo/JNIUtils");
if (clazz == NULL) {
return env->NewStringUTF("find class error");
}
jmethodID methodId = env->GetStaticMethodID(clazz, "getJavaInfo", "(Ljava/lang/String;I)Ljava/lang/String;");
if(methodId == NULL) {
return env->NewStringUTF("find method error");
}
jstring info = env->NewStringUTF("Hello world from JNI !");
jint index = 2;
return (jstring)env->CallStaticObjectMethod(clazz, methodId, info, index);
}
非常简单的修改,然后再重新编译程序,运行可见:
2 Native代码反调用Java层代码
从上面修改后的代码中可见,Java_com_zyx_jnidemo_JNIUtils_getInfo函数中首先通过FindClass函数传入类名:com/zyx/jnidemo/JNIUtils来找到了类,然后再使用GetStaticMethodID函数传入类的变量、方法名 和 方法签名来找到了方法,接着再通过JNIEnv对象的CallStaticObjectMethod函数传入类、方法和两个对应的参数来完成最终的调用过程。这样一来就完成了一次从Java调用JNI然后再从JNI中调用Java方法的过程。其中,FindClass、GetStaticMethodID和CallStaticObjectMethod这些函数都是在jni.h中定义的,下面我们来看看这些函数的使用详细:
2.1 获取Class对象
jclass FindClass(const char* clsName)
通过类的名称(包名,但是要用"/"替换'".")来获取jclass。比如上面代码:jclass clazz = env->FindClass("com/zyx/jnidemo/JNIUtils");
jclass GetObjectClass(jobject obj)
通过对象实例来获取jclass,相当于Java中的getClass()函数
jclass getSuperClass(jclass obj)
通过jclass可以获取其父类的jclass对象
2.2 获取属性方法
jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
获取某个属性
jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
获取某个方法
jfieldID GetStaticFieldID(jclass clazz, const char* name, const char* sig)
获取某个静态属性
jmethodID GetStaticMethodID(jclass clazz, const char* name, const char* sig)
获取某个静态方法。比如上面代码:jmethodID methodId = env->GetStaticMethodID(clazz, "getJavaInfo", "(Ljava/lang/String;I)Ljava/lang/String;");
2.3 构造一个对象
jobject NewObject(jclass clazz, jmethodID methodID, ...)
jobject NewObjectA(jclass clazz, jmethodID methodID, const jvalue* args)
jobject NewObjectV(jclass clazz, jmethodIDmethodID,va_list args)
构造出一个对象,比如:
jmethodID methodId = env->GetMethodID(clazz, "方法名", "方法签名");
jobject obj = env->NewObject(clazz, methodId);
2.4 调用方法
jobject (*CallStaticXXXMethod)(JNIEnv*, jclass, jmethodID, ...);
调用某个静态方法,并返回XXX,比如上面代码:(jstring)env->CallStaticObjectMethod(clazz, methodId, info, index);
jobject (*CallXXXMethod)(JNIEnv*, jobject, jmethodID, ...);
调用某个对象静态方法,并返回XXX
3 方法签名
上面扩展Demo中提到使用GetStaticMethodID函数传入的第三个参数是方法签名:(Ljava/lang/String;I)Ljava/lang/String; ,那么这个方法签名到底是什么?又是如何得到的呢?下面我们就来讲讲方法签名是怎么一回事。
因为Java支持方法重载,即同样的方法名,可以有不一样的参数或返回类型。那么JNI如果仅仅是根据方法名是没有办法找到重载的方法的,所以要解决这个问题,JNI就衍生了一个签名的概念,也就是将参数类型和返回值类型进行组合,如果拥有一个该函数的签名信息和这个函数的函数名,我们就可以找到对应的Java层中的函数了。
2.1 使用命令查看方法签名
在命令行中通过命令:javap -s xxx.class便能查看到整个类里方法的签名,如图:
红框中便是上面Demo中要传入的值了。
2.2 推算方法签名
JNI的数据类型包含两种:基本类型和引用类型。而类型签名是标识一个特定的Java类型。
基本类型主要有jboolean、jchar、jint等,它们大多和Java中的数据类型相对应,只是前面多一个j,而签名大多是首字母大写,特殊除外,如下表:
JNI类型 |
Java类型 |
类型签名 |
jboolean |
boolean |
Z (因为B被byte使用) |
jbyte |
byte |
B |
jchar |
char |
C |
jshort |
short |
S |
jint |
int |
I |
jlong |
long |
J (因为L表示类的签名) |
jfloat |
float |
F |
jdouble |
double |
D |
void |
void |
V |
引用类型主要有类、对象和数组,类的签名比较简单,它采用”L+包名+类名+;”(特别要注意分号不要漏)的形式,只需要将其中的 . 替换为 / 即可,而数据类型数组就是“[ +数据类型签名”,如果是多维数组,它的签名为n个[ + 类型签名,对应关系如下表:
JNI类型 |
Java类型 |
类型签名 |
jobject |
Object |
Ljava/lang/Object; |
jclass |
Class |
Ljava/lang/Class; |
jstring |
String |
Ljava/lang/String; |
jobjectArray |
Object[] |
[Ljava/lang/Object; |
jbooleanArray |
boolean[] |
[Z |
jbyteArray |
byte[] |
[B |
jcharArray |
char[] |
[C |
jshortArray |
short[] |
[S |
jintArray |
int[] |
[I |
jlongArray |
long[] |
[J |
jfloatArray |
float[] |
[F |
jdoubleArray |
double[] |
[D |
jthrowable |
Throwable |
Ljava/lang/Throwable |
一个方法的签名为“ (参数类型签名) + 返回值类型签名”,像上面方法:String getJavaInfo(String info, int index)的情况,所以它的签名就应该是:(Ljava/lang/String;I)Ljava/lang/String; ,要注意不要漏掉分号。