在本文我们跳过JNI的底层机制,读者最好先把它想象为本地代码和java代码的粘合剂
通俗地说,JNI是一种技术,通过这种技术可以做到以下两点:
· Java程序中的函数可以调用Native语言写的函数,Native一般指的是C/C++编写的函数。
· Native程序中的函数可以调用Java层的函数,也就是在C/C++程序中可以调用Java的函数。
Android 中调用C/C++库的步骤:
- 第一步:通过System.loadLibrary引入C代码库名。
- 第二步:在cpp目录下的natice-lib.cpp中编写C/C++代码。
- 第三步:调用C/C++文件中对应的实现方法即可。
JNI/NDK的API
在C/C++本地代码中访问Java端的代码,一个常见的应用就是获取类的属性和调用类的方法,为了在C/C++中表示属性和方法,JNI在jni.h头文件中定义了jfieldID,jmethodID类型来分别代表Java端的属性和方法。在访问或者设置Java属性的时候,首先就要先在本地代码取得代表该Java属性的jfeldID,然后才能在本地代码中进行Java属性操作,同样,需要调用Java端的方法时,也是需要取得代表该方法的jmethodID才能进行Java方法调用。接下来尝试下如何在native中调用Java中的方法。
JNIEnv 类型和jobject类型
JNIEnv *env和jobjet instance,简单介绍下这两个类型的作用。
JNIEnv 类型
JNIEnv类型实际上代表了Java环境,通过JNIEnv*指针就可以对Java端的代码进行操作。比如我们可以使用JNIEnv来创建Java类中的对象,调用Java对象的方法,获取Java对象中的属性等。
JNIEnv类中有很多函数可以用,如下所示:
- NewObject: 创建Java类中的对象。
- NewString: 创建Java类中的String对象。
- NewArray: 创建类型为Type的数组对象。
- GetField: 获取类型为Type的字段。
- SetField: 设置类型为Type的字段的值。
- GetStaticField: 获取类型为Type的static的字段。
- SetStaticField: 设置类型为Type的static的字段的值。
- CallMethod: 调用返回类型为Type的方法。
- CallStaticMethod: 调用返回值类型为Type的static 方法。
当然,除了这些常用的函数方法外,还有更多可以使用的函数,可以在jni.h文件中进行查看,或者参考https://docs.oracle.com/javase/6/docs/technotes/guides/jni/spec/jniTOC.html链接去查询相关方法,上面都说得特别清楚。
好了,说完JNIEnv,接下来我们讲第二个 jobject。
jobject 类型
jobject可以看做是java中的类实例的引用。当然,情况不同,意义也不一样。如果native方法不是static, obj 就代表native方法的类实例。
如果native方法是static, obj就代表native方法的类的class 对象实例(static 方法不需要类实例的,所以就代表这个类的class对象)。
C语言调用java中方法的语法类似于java中的反射,java中的对象映射在C语言中都用jobject表示。
举一个简单的例子:我们在TestJNIBean中创建一个静态方法testStaticCallMethod和非静态方法testCallMethod,我们看在cpp文件中该如何编写?
TestJNIBean的代码:
public class TestJNIBean{
public static final String LOGO = "learn android with aserbao";
static {
System.loadLibrary("native-lib");
}
public native String testCallMethod(); //非静态
public static native String testStaticCallMethod();//静态
public String describe(){
return LOGO + "非静态方法";
}
public static String staticDescribe(){
return LOGO + "静态方法";
}
}
cpp文件中实现:
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_androidndk_TestJNIBean_testCallMethod(JNIEnv *env, jobject instance) {
jclass a_class = env->GetObjectClass(instance); //因为是非静态的,所以要通过GetObjectClass获取对象
jmethodID a_method = env->GetMethodID(a_class,"describe","()Ljava/lang/String;");// 通过GetMethod方法获取方法的methodId.
jobject jobj = env->AllocObject(a_class); // 对jclass进行实例,相当于java中的new
jstring pring= (jstring)(env)->CallObjectMethod(jobj,a_method); // 类调用类中的方法
char *print=(char*)(env)->GetStringUTFChars(pring,0); // 转换格式输出。
return env->NewStringUTF(print);
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_androidndk_TestJNIBean_testStaticCallMethod(JNIEnv *env, jclass type) {
jmethodID a_method = env->GetMethodID(type,"describe","()Ljava/lang/String;"); // 通过GetMethod方法获取方法的methodId.
jobject jobj = env->AllocObject(type); // 对jclass进行实例,相当于java中的new
jstring pring= (jstring)(env)->CallObjectMethod(jobj,a_method); // 类调用类中的方法
char *print=(char*)(env)->GetStringUTFChars(pring,0); // 转换格式输出。
return env->NewStringUTF(print);
}
上面的两个方法最大的区别就是静态方法会直接传入jclass,从而我们可以省去获取jclass这一步,而非静态方法传入的是当前类
GetFieldID是得到java类中的参数ID,GetMethodID得到java类中方法的ID,它们只能调用类中声明为 public的参数或方法。使用如下:
jfieldID topicFieldId = env->GetFieldID(objectClass,"name", "Ljava/lang/String;");
jmethodID getcName=env->GetMethodID(objectClass,"getcatName","()Ljava/lang/String;");
第一参数是Java 类对象。第二个参数是参数(或方法名),第三个参数是该参数(或方法)的签名。第三个参数由以下方法得到。
在C/C++中调用Java代码
如何获取Java中的类并生成对象
JNIEnv类中有如下几个方法可以获取java中的类:
- jclass FindClass(const char* name) 根据类名来查找一个类,完整类名
需要我们注意的是,FindClass方法参数name是某个类的完整路径。比如我们要调用Java中的Date类的getTime方法,那么我们就可以这么做:
extern "C"
JNIEXPORT jlong JNICALL
Java_com_example_androidndk_TestJNIBean_testNewJavaDate(JNIEnv *env, jobject instance) {
jclass class_date = env->FindClass("java/util/Date");//注意这里路径要换成/,不然会报illegal class name
jmethodID a_method = env->GetMethodID(class_date,"<init>","()V");
jobject a_date_obj = env->NewObject(class_date,a_method);
jmethodID date_get_time = env->GetMethodID(class_date,"getTime","()J");
jlong get_time = env->CallLongMethod(a_date_obj,date_get_time);
return get_time;
}
- jclass GetObjectClass(jobject obj) 根据一个对象,获取该对象的类
如何在C/C++中调用Java方法?
在JNIEnv环境下,我们有如下两种方法可以获取方法和属性:
- GetMethodID: 获取非静态方法的ID;
- GetStaticMethodID: 获取静态方法的ID;
来取得相应的jmethodID。
GetMethodID方法如下:
1 jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
方法的参数说明:
- clazz: 这个方法依赖的类对象的class对象。
- name: 这个字段的名称。
- sign: 这个字段的签名(每个变量,每个方法都有对应的签名)。
在C/C++中修改Java变量
修改Java中对应的变量思路其实也很简单。
- 找到对应的类对象。
- 找到类中的需要修改的属性
- 重新给类中属性赋值
代码如下:
public class TestJNIBean{
static {
System.loadLibrary("native-lib");
}
public int modelNumber = 1;
/**
* 修改modelNumber属性
*/
public native void testChangeField();
}
/*
* 修改属性
*/
extern "C"
JNIEXPORT void JNICALL
Java_com_example_androidndk_TestJNIBean_testChangeField(JNIEnv *env, jobject instance) {
jclass a_class = env->GetObjectClass(instance); // 获取当前对象的类
jfieldID a_field = env->GetFieldID(a_class,"modelNumber","I"); // 提取类中的属性
env->SetIntField(instance,a_field,100); // 重新给属性赋值
}
调用testChangeField()方法后,TestJNIBean中的modelNumber将会修改为100。
在C/C++中操作Java数组
jType* GetArrayElements((Array array, jboolean* isCopy)):这类方法可以把Java的基本类型数组转换成C/C++中的数组。isCopy为true的时候表示数据会拷贝一份,返回的数据的指针是副本的指针。如果false则不会拷贝,直接使用Java数据的指针。不适用isCopy可以传NULL或者0。
newObject 和 AllocObject 区别
(1)AllocObject这是给对象分配了内存空间,并没有变量的初始化、构造方法的调用,
newObject有对变量进行初始化,并调用指定的构造方法
(2)方法签名的获取 使用命令 Javap
例如:javap -classpath F:\JNI\app\build\intermediates\javac\debug\compileDebugJavaWithJavac\classes -s com.example.jni.CTransferJava
(3)GetMethodID 获取构造函数 使用<init>代替
知识补充链接:https://www.freesion.com/article/3964761069/