Android JNI访问Java成员
在 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*2); // 获取.设置 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 ", 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); }