零基礎帶你吃掉JNI全家桶(三)

散人丶發表於2019-02-17

前言

之前兩篇主要從整體角度講解了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開發。

裡面的坑其實還是挺多的,所以小夥伴一定要自己動手去操作一下,搭一下環境,寫一些程式碼,最後肯定是有所收穫的,有疑惑或者想法的朋友可以留言討論,比心!~

相關文章