安卓开发学习之ndk操作java类

背景

这几天在学习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"));


代码地址

https://github.com/songzeceng/first/tree/studyOfJni

猜你喜欢

转载自blog.csdn.net/qq_37475168/article/details/80391791