背景
这几天在学习ndk开发,今天记录一下如何用ndk操作java类,实现实例化java类对象,修改java类属性、调用java类方法和调用父类方法
我还是用的动态注册,不熟悉的可以翻翻我的安卓开发学习之ndk动态注册一文
步骤
构造java类Person
构造一个类Person,String属性name,int属性age,带参无参构造方法,get/set+toString()方法
再构造一个类Man,继承Person类,覆写toString()方法
这里仅贴出两者的toString()
Person类:
@Override public String toString() { final StringBuilder sb = new StringBuilder("{"); sb.append("\"name\":\"") .append(name).append('\"'); sb.append(",\"age\":") .append(age); sb.append('}'); return sb.toString(); }
Man类:
@Override public String toString() { return "Man`s toString()"; }
添加native方法
在MyNdkUtils类里面,添加四个native方法,分别实现修改java类属性、构造java类对象、调用java类方法和调用父类方法功能
public static native void updatePersonInfo(Person p); public static native Person getPerson(int age, Person p); public static native Person getPerson2(Person p, int age); public static native void callSuper(Man m);
添加c函数和java方法的映射
在JNINativeMethod数组里,添加那四个native方法的映射
/* * 基本数据类型+void:后面不加; * 引用类型:后面加; */ static JNINativeMethod method[] = { {"updatePersonInfo", "(Lcom/example/songzeceng/myndkdemo/model/Person;)V", (void*)updateInfo}, {"getPerson", "(ILcom/example/songzeceng/myndkdemo/model/Person;)Lcom/example/songzeceng/myndkdemo/model/Person;", (void*)getPerson}, {"getPerson2", "(Lcom/example/songzeceng/myndkdemo/model/Person;I)Lcom/example/songzeceng/myndkdemo/model/Person;", (void*)getPerson2}, {"callSuper", "(Lcom/example/songzeceng/myndkdemo/model/Man;)V", (void*)callSuperMethod} // jni方法数组,每一个元素表示一个java方法和native函数的对应关系 // 元素的组成部分:java方法名,java方法签名((方法参数;)方法返回值类型;),对应的native函数名 };
这里的方法签名,可以直接查看字节码,关于这点,请参见文章安卓开发学习之Android Studio下查看java类的字节码
然后就可以分别实现那几个c函数了
实现c函数
由于这里已经实现了映射,所以就不用写JNIExport那些了,只需要像普通的c函数就可以
void updateInfo(JNIEnv* env, jclass type, jobject obj) { // 更改属性 jclass clazz = (*env)->GetObjectClass(env, obj); if (clazz == NULL) { __android_log_print(ANDROID_LOG_INFO, "person_ndk_util", "class为空"); return; } jfieldID ageId = (*env)->GetFieldID(env, clazz, "age", "I"); // 获取属性id,参数列表:jni环境指针,目标类,属性名,属性类型(I为int) jint ageValue = (*env)->GetIntField(env, obj, ageId); // 获取int属性的值,参数列表:jni环境指针,目标对象,属性id ageValue++; (*env)->SetIntField(env, obj, ageId, ageValue); } jobject getPerson(JNIEnv* env, jclass type, jint age, jobject p) { // new对象 jclass clazz = (*env)->GetObjectClass(env, p); jmethodID constructor = (*env)->GetMethodID(env, clazz, "<init>", "()V"); // 构造方法,名字被<init>替代,返回类型为void jobject person = (*env)->NewObject(env, clazz, constructor); // 构造一个对象 jfieldID nameId = (*env)->GetFieldID(env, clazz, "name", "java/lang/String"); // 获取String类型的name属性 jstring nameValue = (*env)->GetObjectField(env, p, nameId); // 获取对象属性的值 jfieldID ageId = (*env)->GetFieldID(env, clazz, "age", "I"); (*env)->SetObjectField(env, person, nameId, nameValue); // 设置属性的值 (*env)->SetIntField(env, person, ageId, age); return person; } jobject getPerson2(JNIEnv* env, jclass type, jobject p, jint age) { // 调用java方法 jclass clazz = (*env)->GetObjectClass(env, p); jmethodID methodId = (*env)->GetMethodID(env, clazz, "setAge", "(I)V"); (*env)->CallVoidMethod(env, p, methodId, age); methodId= (*env)->GetMethodID(env, clazz, "setName", "(Ljava/lang/String;)V"); (*env)->CallVoidMethod(env, p, methodId, (*env)->NewStringUTF(env, "Borne")); // 类似的还有CallVoidMethodA()和CallVoidMethodV()函数,只不过是给java方法传参方式不同 // CallVoidMethodA()以jvalue指针的形式传参,CallVoidMethodV()以va_list方式传参,而CallVoidMethod()则以可变参数传参 // 字符串参数要进行包装,否则会报错accessed stale global reference return p; } void callSuperMethod(JNIEnv* env, jclass type, jobject man) { jclass clazz = (*env)->FindClass(env, "com/example/songzeceng/myndkdemo/model/Person"); jmethodID toString = (*env)->GetMethodID(env, clazz, "toString", "()Ljava/lang/String;"); jstring str = (*env)->CallNonvirtualObjectMethod(env, man, clazz, toString); char* ch = (*env)->GetStringUTFChars(env, str, JNI_FALSE); __android_log_print(ANDROID_LOG_INFO, "person_ndk_util", ch); }
套路都差不多:获取类->根据类获取属性id或方法id->获取值或调用
包括调用java系统类(ArrayList等)的方法,也是这个套路,因为ArrayList这些传过来也是jobject,只需要知道方法签名和属性的签名,就可以调用或访问
另外,可以发现,c访问java类的属性或调用方法是不受访问修饰的限制的,无所谓private与否
如果调用静态方法,就是CallStatic..Method();
调用父类的,就是先获取父类的jclass,再调用CallNonVirtual..Method()就行
运行结果
截图如下:
踩坑记录
1、Fatal signal 11(SIGSEGV),code 1:
这里的原因是因为在日志里,直接把jstring给打印了出去,这是不对的,要拆箱成char*,才能输出
__android_log_print(ANDROID_LOG_INFO, "person_ndk_util", str);
改成
char* ch = (*env)->GetStringUTFChars(env, str, JNI_FALSE); __android_log_print(ANDROID_LOG_INFO, "person_ndk_util", ch);
2、jni error (app bug): accessed stale local reference
也是字符串的原因,给方法传参数时,不能直接传char*,要包装成jstring
(*env)->CallVoidMethod(env, p, methodId, "Borne");
改成
(*env)->CallVoidMethod(env, p, methodId, (*env)->NewStringUTF(env, "Borne"));