在 JNI 调用中,不仅仅 Java 可以调用本地方法,本地方法也可以调用 Java 中的方法和成员变量。
Java 中的类封装了属性和方法,想要访问 Java 中的属性和方法,首先要获得 Java 类或 Java 对象,然后再访问属性、调用方法。
在 Java 中类成员指静态属性和静态方法,它们属于类而不属于对象。而对象成员是指非静态属性和非静态方法,他们属于具体一个对象,不同的对象其成员是不同的,所以在本地代码中,对类成员的访问和对对象成员的访问是不同的。
1、获取 Java 类的两种方式
(1)通过传入JNI中的完整类名来获取类
// name:类全名,包含包名,包名间隔符用 “/”
jclass FindClass(const char *name); // JNI获得Android中的类并保存在jActivity中
jclass jcls = env->FindClass("com/aaron/link/LedNative");
(2)通过传入JNI中的一个java的对象来获取该对象的类
// obj: 引用类型
jclass GetObjectClass(jobject obj); // JNI获得引用obj所对应的类
jclass myCls = env->GetObjectClass(obj);
2、获取 Java 属性 ID 和方法 ID
在本地代码中要访问设置 Java 属性和方法,首先要在本地代码中取得代表该 Java 属性的 jfieldID 和代表该 Java 方法的 jmethodID,然后才能进行属性操作和方法调用。
// clazz:要取的成员对应的类
// name:要取的方法或者属性
// sig:要取的方法或属性的签名 // 根据属性签名返回 clazz 类中的该属性 ID
jfieldID GetFieldID(jclass clazz, const char *name, const char *sig);
// 根据属性签名返回 clazz 类中的静态属性 ID
jfieldID GetStaticFieldID(jclass clazz, const char *name, const char *sig);
// 根据方法签名返回 clazz 类中的该方法 ID
jmethodID GetMethodID(jclass clazz, const char *name, const char *sig);
// 根据方法签名返回 clazz 类中的静态方法 ID
jmethodID GetStaticMethodID(jclass clazz, const char *name, const char *sig);
举例:
Java代码
class MyClass {
private int mNumber;
private static String mName = "Aaron";
public MyClass() {
mNumber = 100;
} public void printNum() {
System.out.println("Number:" + mNumber);
} public static void printName() {
System.out.println("Name:" + mName);
}
} class NativeCallJava {
static {
System.loadLibrary("native_callback");
} private static native void callNative(MyClass cls); public static void main(String arg[]) {
callNative(new MyClass());
}
}
本地代码
void JNI_callNative(JNIEnv *env, jclass thiz, jobject obj)
{
// 获取对象对应的类
jclass myCls = env->GetObjectClass(obj);
// 获取属性
jfieldID mNumFieldID = env->GetFieldID(myCls, "mNumber", "I");
// 获取静态属性
jfieldID mNameFieldID = env->GetStaticFieldID(myCls, "mName", "java/lang/String");
// 获取方法
jmethodID printNumMethodID = env-GetMethodID(myCls, "printNum", "(V)V");
// 获取静态方法
jmethodID printNameMethodID = env-GetStaticMethodID(myCls, "printName", "(V)V");
}
3、JNI 类型签名
Java 语言是面向对象的语言,支持重载机制,即允许多个具有相同的方法名不同的方法签名的方法存在。
不能只通过方法名明确的让 JNI 找到 Java 对应的方法,还要指定方法的签名,即参数列表和返回值类型。
类型签名 | Z | B | C | S | I | J | F | D | L | V | [ | [I | [F | [B | [C | [S | [D | [J | [Z |
Java 类型 | boolean | byte | char | short | int | long | float | double | 类 | void | [] | int[] | float[] | byte[] | char[] | short[] | double[] | long[] | boolean[] |
基本类型
以特定的单个大写字母表示
Java类类型
Java 类类型以 L 开头,以 “/” 分隔包名,在类名后加上 “;” 分割符,例如:String 的签名为:Ljava/lang/String
在 Java 中数组是引用类型,数组以 “[” 开头,后面跟数组元素类型签名,例如:int[] 的签名是 [I,对于二维数组,int[][] 签名是 [[I,object 数组签名就是 [Ljava/lang/Object
对于方法签名,在 JNI 中有特定的表示方式:(参数1类型签名参数2类型签名参数3类型签名... ...)返回值类型签名
注意:
(1)方法名在方法签名中没有体现出来。
(2)括号内表示参数列表,参数列表紧密相连,中间没有逗号,没有空格。
(3)返回值出现在括号后面。
(4)没有返回值也要加上 V 类型。
Java 方法 | JNI 方法签名 |
boolean isLedOn(void); | (V)Z |
void setLedOn(int ledNo); | (I)V |
String substr(String str, int idx, int count); | (Ljava/lang/String;II)Ljava/lang/String |
char fun(int n, String s, int[] value); | (ILjava/lang/String;[I)C |
boolean showMsg(android.View v, String msg); | (Lanfroid/View;Ljava/lang/String;)Z |
4、JNI 操作 Java 属性和方法
(1)获取、设置属性值和静态属性值
取得了代表属性和静态属性的 jfieldID,就可以使用 JNIEnv 中提供的方法来获取、设置属性值和静态属性值。
// <type>表示 Java 中的基本类型
// 获取属性值的 JNI 方法
j<type> Get<type>Field(jobject obj, jfieldID fieldID);
j<type> GetStatic<type>Field(jobject obj, jfieldID fieldID);
// 设置属性值的 JNI 方法
void Set<type>Field(jobject obj, jfieldID fieldID, j<type> val);
void SetStatic<type>Field(jobject obj, jfieldID fieldID, j<type> val);
(2)通过 JNI 调用 Java 中的方法
取得了代表方法的 jmethodID,就可以使用 JNIEnv 中提供的方法来调用 Java 中的方法。
// type 是这个方法的返回值类型,首字母大写
// 第一个参数代表调用的这个方法所属于的对象,或者这个静态方法所属的类。
// 第二个参数代表 jmethodID,后面的表示调用方法的参数列表,...表示变长参数。
// 调用 Java 成员方法
Call<type>Method(jobject obj, jmethodID method, ...);
// 调用 Java 静态成员方法
CallStatic<type>Method(jobject obj, jmethodID method, ...);
代码举例
// 静态方法不依赖于任何对象就可以进行访问
// 静态的直接通过类 myCls 来调用,非静态需要通过对象 obj 来调用
void JNI_callNative(JNIEnv *env, jclass thiz, jobject obj)
{
// 获取对象对应的类
jclass myCls = env->GetObjectClass(obj);
// 获取属性
jfieldID mNumFieldID = env->GetFieldID(myCls, "mNumber", "I");
// 获取静态属性
jfieldID mNameFieldID = env->GetStaticFieldID(myCls, "mName", "java/lang/String"); // 获取.设置 Java 成员的属性值
jint mNum = env->GetIntField(obj, mNumFieldID);
env->SetIntField(obj, mNumFieldID, mNum*); // 获取.设置 Java 静态属性值
jstring mName = (jstring)(env->GetStaticObjectField(myCls, mNameFieldID));
jstring newStr = env->NewStringUTF("Hello Native");
env->SetStaticObjectField(myCls, mNameFieldID, newStr); // 获取方法
jmethodID printNumMethodID = env-GetMethodID(myCls, "printNum", "(V)V");
// 获取静态方法
jmethodID printNameMethodID = env-GetStaticMethodID(myCls, "printName", "(V)V"); // 调用 MyClass 对象中的 printNum 方法
CallVoidMethod(obj, printNumMethodID);
// 调用 Myclass 类的静态 printName 方法
CallStaticVoidmethod(myCls, printNameMethodID);
}
5、在 JNI 中创建 Java 对象
(1)在 JNI 中创建 Java 对象
// JNIEnv 中创建 Java 对象的方法
// clazz:要创建的对象的类
// jmethodID:创建对象对应的构造方法ID
// 参数列表:...表示是变长参数,以“V”结尾的方法名表示向量表表示参数列表,以“A”结尾的方法名表示以 jvalue 数组提供参数列表
jobject NewObject(jclass clazz, jmethodID methodID, ...);
jobject NewObjectV(jclass clazz, jmethodID methodID, va_list args);
jobject NewObjectA(jclass clazz, imethodID methodID, const jvalue *args);
获得构造方法 ID 的方法 env->GetMethodID(clazz, method_name, sig) 中的第二个参数固定为类名(也可以用“<init>”代替类名),第三个参数和要调用的构造方法有关,默认的构造方法没有参数和返回值。
void JNI_callNativa(JNIEnv *env, jclass thiz, jobject obj)
{
jclass myCls = env->GetObjectClass(obj);
// 也可以通过完整类名获取
//jclass myCls = env->FindClass("com/test/native/MyClass");
// 获得 MyClass 的构造方法 ID
jmethodID myClassMethodID = env->GetMethodID(myCls, "MyClass", "(V)V");
// 创建 MyClass 对象
jobject newObj = NewObject(myCls, myClassMethodID);
}
(2)在 JNI 中创建 Java String 对象
在 Java 中,字符串 String 对象是 Unicode(UTF-16)编码,每个字符不论是中文还是英文还是符号,一个字符总是占用两个字节。在 C/C++ 中一个字符是一个字节,C/C++ 中的宽字符是两个字节的。
在本地 C/C++ 代码中我们可以通过一个宽字符串,或是一个 UTF-8 编码的字符串创建一个 Java 端的 String 对象。这种情况通常用于返回 Java 环境一个 String 返回值等场合。
// 根据传入的宽字符串创建一个 Java String 对象
jstring NewString(const jchar *unicode, jsize len);
// 根据传入的 UTF-8 字符串创建一个 Java String 对象
jstring NewStringUTF(const char *utf);
在 Java 中 String 类有很多对字符串进行操作的方法,在本地代码中通过 JNI 接口可以将 Java 的字符串转换到 C/C++ 的宽字符串(wchar_t*),或是传回一个 UTF-8 的字符串(char*)到 C/C++,在本地代码中操作。
// 在 Java 端有一个字符串 String str = "abcd"; ,在本地代码中取得并输出
void native_string_operation(JNIEnv *env, jobject obj)
{
// 取得该字符串的 jfieldID
jfieldID id_string = env->GetFieldID(env->GetObjectClass(obj), "str", "Ljava/lang/String");
// 取得该字符串,强制转换为 jstring 类型
jstring string = (jstring)(env->GetObjectField(obj, id_string));
printf("%s\n", string);
}
JNIEnv 提供了一系列的方法来操作字符串:
// str:传入一个指向 Java 中 String 对象的 jstring 引用
// isCopy:传入一个 jboolean 的指针,其值可以为 NULL/JNI_TRUE/JNI_FALSE
// JNI_TRUE:表示在本地开辟内存,然后把 Java 中的 String 复制到这个内存中,然后返回指向这个内存地址的指针
// JNI_FALSE:表示直接返回指向 Java 中 String 的内存指针,这时不要改变这个内存的内容,这将破坏 String 在 Java 中始终是常量的规则
// NULL:表示不关心是否复制字符串 // 将一个 jstring 对象,转换为(UTF-16)编码的宽字符串(jchar*)
const jchar *GetStringChars(jstring str, jboolean *isCopy);
// 将一个 jstring 对象,转换为(UTF-8)编码的宽字符串(char*)
const char *GetStringUTFChars(jstring str, jboolean *isCopy);
使用这两个方法取得的字符串,在不用的时候都要释放,分别对应下面连个方法。
// jstr:需要释放的本地字符串的资源
// str:需要释放的本地字符串
RealeaseStringChars(jstring jstr, const jchar *str);
RealeaseStringUTFChars(jstring jstr, const char *str);
6、在 JNI 中处理 Java 数组
可以使用 GetFieldID 获取一个 Java 数组变量的 ID,然后用 GetObjectField 取得该数组到本地方法,返回值为 jobject,然后可以强制转换为 j<type>Array 类型。
j<type>Array 类型是 JNI 定义的一个对象类型,它并不是 C/C++ 的数组,如 int[]等,所以要把 j<type>Array 转换为 C/C++ 中的数组来操作。
JNIEnv 定义了一系列的方法来把一个 j<type>Array 类型转换为 C/C++ 数组或把 C/C++ 数组转换为 j<type>Array。
(1)获取数组长度
jsize GetArrayLength(jarray array);
(2)对象类型数组操作
// len:新创建对象数组长度
// clazz:对象数组元素类型
// init:对象数组元素的初始值
// array:要操作的数组
// index:要操作数组元素的下标
// val:要设置的数组元素的值 // 创建对象数组
jobjectArray NewObjectArray(jsize len, jclass clazz, jobject init);
// 获得元素
jobject GetObjectArrayElement(jobjectArray array, jsize index);
// 设置元素
void SetObjectArrayElement(jobjectArray array, jsize index, jobject val);
JNI 没有提供直接把 Java 的对象类型数组(Object[])直接转到 C++ 中的 jobject[] 数组的方法,而是直接通过 Get/SetObjectArrayElement 这样的方法来对 Java 的 Object[] 数组进行操作。
(3)对基本数据类型数组的操作
// 获得指定类型的数组
j<type>* Get<type>ArrayElement(j<type>Array array, jboolean *isCopy);
// 释放数组
void Release<type>ArrayElements(j<type>Array array, j<type> *elems, jint mode);
这类函数可以把 Java 基本类型的数组转换到 C/C++ 中的数组。有两种处理方式,一是复制一份传回本地代码,另一种是把指向 Java 数组的指针直接传回到本地代码,处理完本地化的数组后,通过 Realease<type>ArrayElements 来释放数组。处理方式有 Get 方法的第二个参数 isCopy 来决定(取值为 JNI_TRUE 或 JNI_FALSE)。
第三个参数 mode 可以取下面的值:
<1> 0:对 Java 的数组进行更新并释放 C/C++ 的数组
<2> JNI_COMMIT:对 Java 的数组进行更新但是不释放 C/C++ 的数组
<3> JNI_ABORT:对 Java 的数组不进行更新,释放 C/C++ 的数组
Java:
class ArrayTest {
static {
System.loadLibrary("native_array");
} private int[] array = new int[]{1, 2, 3, 4, 5}; public native void show(); public static void main(String[] args) {
new ArrayTest().show();
}
}
JNI:
void JNI_Array_show(JNIEnv *env, jobject obj)
{
jfieldID id_array = env->GetFieldID(env->GetObjectClass(obj), "array", "[I");
jintArray arr = (jintArray)(env->GetObjectField(obj, id_array));
jint *int_arr = env->GetIntArrayElements(arr, NULL);
jsize len = env->GetArrayLength(arr); for(int i; i<len; i++)
cout << int_arr[i] << endl; env->ReleaseIntArrayElements(arr, int_arr, JNI_ABORT);
}