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 对应的方法,还要指定方法的签名,即参数列表和返回值类型。

JNI 签名类型
类型签名 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 类型。

JNI 方法签名举例
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);
}
原文地址:https://www.cnblogs.com/lialong1st/p/8991802.html