零基础带你吃掉JNI全家桶(三)

3,820 阅读8分钟

前言

之前两篇主要从整体角度讲解了native方法与java方法的通信以及so文件的作用,一些细节就没有太讲解详细,可能有些朋友对其中有些还不太清晰,本文就从最基本的JNI语法带大家熟悉下,怎么编写native方法,与java方法有哪些区别,两者怎么进行对象传输以及调用。

零基础带你吃掉JNI全家桶(一)

零基础带你吃掉JNI全家桶(二)

本文篇幅较长,前面基础知识较多,后面直接开撸代码,请耐心观看~

1、JNI语法

1.1 JNIEnv 和 jobject是什么?

在native方法中,我们总会看到这两个参数,比如下面的方法

JNIEXPORT void JNICALL Java_com_jni_demo_JNIDemo_sayHello (JNIEnv * env, jobject obj)
{
cout<<"Hello World"<<endl;
}

对于JNIEnv,指代了Java本地接口环境(Java Native Interface Environment),是一个JNI接口指针,指向了本地方法的一个函数表,该函数表中的每一个成员指向了一个JNI函数,本地方法通过JNI函数来访问JVM中的数据结构,也就是通过这个JNIEnv* 指针,就可以对Java端的代码进行操作。

对于jobject,如果native方法不是static的话,这个obj就代表这个native方法的类实例,如果native方法是static的话,这个obj就代表这个native方法的类的class对象实例,也就是这个方法在哪个类里面,就代表这个类的对象实例或者class实例

1.2 JNI数据类型

众所周知,在Java中存在2中数据类型,8种基本数据类型以及引用类型,那么在JNI中也是对应的2种数据类型,引用2张图,具体关系如下:

基本数据类型

引用数据类型

基本数据类型都是可以在Native层直接使用的

引用数据类型则不能直接使用,需要根据JNI函数进行相应的转换后,才能使用

多维数组(包括二维数组)都是引用类型,需要使用 jobjectArray 类型存取其值

1.3 域描述符

基本数据类型基本以特定的大写字母表示

Java类 类型签名
int I
float F
double D
long J
boolean Z
byte B
char C
short S

一般引用类型则为 L + 该类型类描述符 + ; (注意,这儿的分号“;”只得是JNI的一部分,而不是我们汉语中的分段,下同) 例如:String类型的域描述符为 Ljava/lang/String; 对于数组,其为 : [ + 其类型的域描述符 + ; int[ ] 其描述符为 [I float[ ] 其描述符为 [F String[ ] 其描述符为 [Ljava/lang/String; Object[ ]类型的域描述符为 [Ljava/lang/Object; 多维数组则是 n个[ +该类型的域描述符 , N代表的是几维数组。例如: int [] []其描述符为[[I

1.4 方法操作符

将参数类型的域描述符按照申明顺序放入一堆括号中跟返回值类型的域描述符, 规则如下: (参数的域描述符的叠加)返回类型描述符。 对于没有返回值的, 用V(表示void型)

比如:String test() 对应的就是()Ljava/lang/String; 注意";"不可忘记

​ int f(int i, Object object) 对应就是(ILjava/lang/Object;)I

依次类推,注意要仔细,很容易出错

2. JNI native方法访问 Java

2.1 获取方法和属性id

上面也说过了,引用数据类型是不能直接使用,在native层,你想直接通过java对象操作方法属性不太现实,JNI在jni.h头文件中定义了jfieldID和jmethodID类型来分别代表Java对象的属性和方法。我们在访问或是设置Java属性的时候,首先就要先在本地代码取得代表该Java属性的jfieldID,然后才能在本地代码进行Java属性操作

public class Person {
    private int age;
    private String name;

    public int getAge() {
        return age;
    }

    public void setAge(int mAge) {
        age = mAge;
    }

    public String getName() {
        return name;
    }

    public void setName(String mName) {
        name = mName;
    }
}

比如这个实体类,比如想要操作setName方法,设置一些值进去

首先获取到这个class对象,熟悉反射的朋友应该一眼就看出来,大致差不多

//获取class对象

jclass clazz_NativeTest=env->FindClass(“com/example/hik/cmake");

//获取methodId
//第三个参数就是方法的操作符,参数是String,返回值是空,所以是(Ljava/lang/String;)V
jmethodID id_show=env->GetMethodID(clazz_NativeTest,“setName”,"(Ljava/lang/String;)V");
//同理获取filedId也是一样的
jfieldID  jfieldID1 = env->GetFieldID(student,"name","Ljava/lang/String;")
//下面是调用方法,person是对象实例,类似反射效果
char *c_new_name = "lisi";
jstring str = env->NewStringUTF(c_new_name);
env->CallVoidMethod(person, id_show, str);

2.2本地创建Java对象

JNIEnv提供了下面几个方法来创建一个Java对象:

jobject NewObject(jclass clazz, jmethodID methodID,...);

jobject NewObjectV(jclass clazz, jmethodIDmethodID,va_list args);

jobject NewObjectA(jclass clazz, jmethodID methodID,const jvalue *args) ;

本地创建Java对象的函数和前面本地调用Java方法很类似:

第一个参数jclass class 代表的你要创建哪个类的对象

第二个参数jmethodID methodID 代表你要使用哪个构造方法ID来创建这个对象。

只要有jclass和jmethodID ,我们就可以在本地方法创建这个Java类的对象。

指的一提的是:由于Java的构造方法的特点,方法名与类名一样,并且没有返回值,所以对于获得构造方法的ID的方法env->GetMethodID(clazz,method_name ,sig)中的第二个参数是固定为“”,第三个参数和要调用的构造方法有关,默认的Java构造方法没有返回值,没有参数。例如:

jclassclazz=env->FindClass("java/util/Date");                                   
//取得java.util.Date类的jclass对象
jmethodID id_date=env->GetMethodID(clazz,"<init>","()V");    
//取得某一个构造方法的jmethodID
jobject date=env->NewObject(clazz,id_date);                            
 //调用NewObject方法创建java.util.Date对象

2.3 实例代码

2.3.1 改变Java对象属性

public class Person {
    private int age;
    private String name;
    public Person() {
    }
    public Person(int mAge, String mName) {
        age = mAge;
        name = mName;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int mAge) {
        age = mAge;
    }

    public String getName() {
        return name;
    }

    public void setName(String mName) {
        name = mName;
    }

    @Override
    public String toString() {
        return "Person{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}

Java层,我们新建一个实体类Bean,用来操作通信,然后再增加一个native方法changePersonName

public class NativeHelper  {
    static {
        System.loadLibrary("native-lib");
    }
    public  native String stringFromJNI();
    public  native int add(int a,int b);

    public  native void changePersonName(Person mPerson);

   public native Person getStudent();

   public native List<Person> getPeronList();
}

native方法,我们基于第一篇的基础上,去增加一个方法

//调用java层对象,改变属性
void changeName(JNIEnv *env, jobject instance, jobject person) {
	//获取person的class对象
    jclass student = env->GetObjectClass(person);
    //获取setName方法的id
    jmethodID setNameMethond = env->GetMethodID(student, "setName", "(Ljava/lang/String;)V");
    char *c_new_name = "lisi";
    jstring str = env->NewStringUTF(c_new_name);
    //调用方法,因为返回值是void,所以是CallVoidMethod,再把改变后的str传进去
    env->CallVoidMethod(person, setNameMethond, str);
}

记得在动态注册里,把方法添加进去

JNINativeMethod jniNativeMethod[] = {{"stringFromJNI",    "()Ljava/lang/String;",                       (void *) backStringToJava},
                                         {"add",              "(II)I",                                      (void *) addNum},
                                         {"changePersonName", "(Lcom/example/taolin/jni_project/Person;)V", (void *) changeName}};

调用之后,发现name已经被改变成了“lisi”,主界面代码就不贴了,直接调用native方法就好了

2.3.2 返回Java层实体对象

我们再添加一个方法

public native Person getStudent();

native方法,同样的增加一个

//返回java层对象
jobject returnPerson(JNIEnv *env, jobject instance) {
	//获取到person class对象
    jclass jclass1 = env->FindClass("com/example/taolin/jni_project/Person");
    //获取到构造函数的methodId
    jmethodID jmethodID1 = env->GetMethodID(jclass1, "<init>", "(ILjava/lang/String;)V");
    jint age = 20;
    char *back_name = "wangwu";
    jstring str = env->NewStringUTF(back_name);
    //NewObject,根据class对象返回一个实例对象
    jobject perosn = env->NewObject(jclass1, jmethodID1, age, str);
    return perosn;
}

动态注册关联一下

    JNINativeMethod jniNativeMethod[] = {{"stringFromJNI",    "()Ljava/lang/String;",                       (void *) backStringToJava},
                                         {"add",              "(II)I",                                      (void *) addNum},
                                         {"changePersonName", "(Lcom/example/taolin/jni_project/Person;)V", (void *) changeName},
                                         {"getStudent",       "()Lcom/example/taolin/jni_project/Person;",  (void *) returnPerson}};

主页面直接调用getStudent(),发现返回一个student对象,name为“wangwu”,native层返回对象成成功

2.3.3 native返回list对象给Java

添加一个方法

public native List<Person> getPeronList();

来,native层,对应添加

//返回java层一个list
jobject returnList(JNIEnv *env, jobject instance) {
	//因为list是无法实例对象,找到Arraylist,返回class对象
    jclass jclass1 = env->FindClass("java/util/ArrayList");
    //拿到构造函数id
    jmethodID contructMethod = env->GetMethodID(jclass1,"<init>","()V");
    //生成一个Arraylist对象,就是我们要返回的对象
    jobject list = env->NewObject(jclass1,contructMethod);
    //拿到 list的 add方法的methodId,准备往method添加几个数据
    jmethodID methodAdd = env->GetMethodID(jclass1,"add","(Ljava/lang/Object;)Z");
    //拿到Person的class对象
    jclass studentClass = env->FindClass("com/example/taolin/jni_project/Person");
    //拿到person的构造函数的methodId
    jmethodID jmethodID1 = env->GetMethodID(studentClass, "<init>", "(ILjava/lang/String;)V");
    for(int i =0;i<4;i++){
        jobject person = env->NewObject(studentClass,jmethodID1,i,env->NewStringUTF("tl"));
        //调用 list的add方法,因为返回时boolean值,所以CallBooleanMethod
        env->CallBooleanMethod(list,methodAdd,person);
    }
    return list;
}

最后,注册绑定不要忘了

 JNINativeMethod jniNativeMethod[] = {{"stringFromJNI",    "()Ljava/lang/String;",                       (void *) backStringToJava},
                                         {"add",              "(II)I",                                      (void *) addNum},
                                         {"changePersonName", "(Lcom/example/taolin/jni_project/Person;)V", (void *) changeName},
                                         {"getStudent",       "()Lcom/example/taolin/jni_project/Person;",  (void *) returnPerson},
                                         {"getPeronList",     "()Ljava/util/List;",                         (void *) returnList}};

主页面调用getPeronList(),可以发现返回list,长度是4,调用成功~

3.总结

JNI学习就暂时告一段落了,因为本人也是刚接触这一块,让我讲的多深,我也是心有力而与不足,因为C++学的也不是太好,所以不敢误人子弟,但是还是希望能够帮助到一些准备入门的小伙伴来学习JNI开发。

里面的坑其实还是挺多的,所以小伙伴一定要自己动手去操作一下,搭一下环境,写一些代码,最后肯定是有所收获的,有疑惑或者想法的朋友可以留言讨论,比心!~