Android NDK開發系列教程2:基本方法調用及傳參

終於建了一個自己個人小站:https://huangtianyu.gitee.io,以後優先更新小站博客,歡迎進站,O(∩_∩)O~~

1. 簡介

有時候我寫了個Java層的方法,希望native層也能夠調用(尤其是一個實體類的get,set方法在native一般都會用到)。這在jni開發中也很常見,jni.h中也提供了很多方法。下面利用具體實例進行說明。這裏直接使用AS3.0裏面的CMake進行編譯了,之後會講解下Android.mk和Application.mk的用法和含義。這裏我主要介紹一下幾個:
1. java向native傳遞常用基本數據類型和字符串類型
2. java向native傳遞數組類型
3. java向native傳遞自定義java對象
4. java向native傳遞List對象
5. native向java返回字符串類型
6. native向java返回java對象
7. native向java返回數組類型
8. native向Java返回List對象
對於上面的每個都給出對應的例子。
本節所有案例代碼均已放到GitHub上,歡迎下載:
https://github.com/huangtianyu/JNILearnCourse

1.1 java和jni類型對照表

在我們調用方法時會用到方法的簽名,使用類變量時需要用該變量對應的jni類型。下面給出對應的類型對照表。
1. 基本數據類型對照表:
這裏寫圖片描述
2. 對象類型對照表:
這裏寫圖片描述
3. 簡寫對應表
這裏寫圖片描述

2. 具體例子

2.1 java向native傳遞常用基本數據類型和字符串類型

強大的AS在你寫了java的native方法後,直接快捷鍵按Alt+Enter後即可生成對應的方法。
java層的方法:

package zqc.com.example;
public class NativeTest {
    // Used to load the 'native-lib' library on application startup.
    static {
        System.loadLibrary("native-lib");
    }
    //定義一個native方法,然後傳入基本數據類型和String型
    public native void java2jniMethod1(boolean b, int i, float f, String s);
}

生成後的native方法:

extern "C"
JNIEXPORT void JNICALL
Java_zqc_com_example_NativeTest_java2jniMethod1(JNIEnv *env, jobject instance, jboolean b, jint i,
                                                jfloat f, jstring s_) {
    //在native層會把string轉換成c/c++都特別熟悉的char*,由char*可以轉string,wstring等等。
    //在Java層String是對象,這裏講char*指針指向了該對象,在方法結束的時候記得要是否該指針引用
    if (b == JNI_TRUE) {
        LOGE("b is true");
    } else {
        LOGE("b is false");
    }
    float nativi = i + f;
    LOGE("native i: %f", nativi);
    const char *s = env->GetStringUTFChars(s_, 0);
    LOGE("native string: %s", s);
    env->ReleaseStringUTFChars(s_, s);
}

在上面可以看到,Java層的基本類型方法都會經過jni進行轉換,轉換成相應的jni類型。其操作也很方便。Java的String類型需要注意下,一般是將jstring先轉換爲char*然後對char *進行操作。由於這獲取了一個局部引用,一般在調用結束後需要釋放該局部引用。

2.2 java向native傳遞數組類型

    //向native傳遞數組類型
    public native void java2jniMethod2(int[] as, String[] strs);

對應的jni方法是:

extern "C"
JNIEXPORT void JNICALL
Java_zqc_com_example_NativeTest_java2jniMethod2(JNIEnv *env, jobject instance, jintArray as_,
                                                jobjectArray strs) {
    //獲取數組裏面內容
    jint *as = env->GetIntArrayElements(as_, NULL);
    int result = 0, len = env->GetArrayLength(as_);
    for (int i = 0; i < len; ++i) {
        result += as[i];
    }
    LOGE("intarray sum is %d", result);
    env->ReleaseIntArrayElements(as_, as, 0);
    //這裏可以看出String[]對應的是jobjectArray
    len = env->GetArrayLength(strs);
    for (int i = 0; i < len; ++i) {
        jstring temp = (jstring) env->GetObjectArrayElement(strs, i);
        const char *ctemp = env->GetStringUTFChars(temp, JNI_FALSE);
        LOGE("第%d個:%s", i, ctemp);
    }
}

其中函數: jsize GetArrayLength(jxxxarray array);用於獲取數組的長度
在Java端調用代碼如下:

    NativeTest test = new NativeTest();
    int a[] = new int[3];
    for (int i=0;i<a.length;i++) {
        a[i] = i + 10;
    }
    String[] strs = new String[4];
    for (int i=0;i<strs.length;i++) {
        strs[i] = "我的值:"+i;
    }
    test.java2jniMethod2(a,strs);
2.2.1 處理基本數據類型有以下幾個相關函數:

(1) GetXXXArrayElements(Array arr , jboolean* isCopide);
這類函數可以把Java基本類型的數組轉換到C/C++中的數組,有兩種處理方式,一種JNI_TRUE是拷貝一份傳回本地代碼,另一個是JNI_FALSE把指向Java數組的指針直接傳回到本地代碼中,處理完本地化的數組後,通過ReleaseXXXArrayElements來釋放數組

(2) ReleaseXXXArrayElements(Array arr , * array , jint mode)
用這個函數可以選擇將如何處理Java跟C++的數組,是提交,還是撤銷等,內存釋放還是不釋放等mode可以取下面的值:
0 :對Java的數組進行更新並釋放C/C++的數組
JNI_COMMIT :對Java的數組進行更新但是不釋放C/C++的數組
JNI_ABORT:對Java的數組不進行更新,釋放C/C++的數組

(3) GetPrimitiveArrayCritical(jarray arr , jboolean* isCopied);
在獲得數組上的鎖後將返回一個句柄給數組。如果沒有建立任何鎖,則isCopy被置爲JNI_TRUE,否則置爲NULL或JNI_FALSE:

(4) ReleasePrimitiveArrayCritical(jarray arr , void* array , jint mode);
釋放從GetPrimitiveArrayCritical調用中返回的數組。也是JDK1.2出來的,爲了增加直接傳回指向Java數組的指針而加入的函數,同樣的也會有同GetStringCritical的死鎖的問題。mode取值如下:
0:從carray中複製值到數組中,並釋放分配給carray的存儲器
JNI_COMMIT:從carray中複製值到數組中,但是不釋放分配給carray的存儲器
JNI_ABORT:不從carray中複製值到數組中

(5) GetXXXArrayRegion(Array arr , jsize start , jsize len , * buffer);
在C/C++預先開闢一段內存,然後把Java基本類型的數組拷貝到這段內存中。用於一個數組子集的複製操作。參數start指定了從何處複製的起始索引,參數len則指定了從數組中複製到本機數組的多個位置數量。

(6) SetXXXArrayRegion(Array arr , jsize start , jsize len , const * buffer);
用來複制本機數組的一段內容回Java數組中。元素一般從本機數組起始處(索引爲0)開始複製,但是隻是從位置start開始將len個元素複製到Java數組中。

(7) Array NewXXXArray(jsize sz)
創建一個包含length個元素的Java數組。

2.2.2 處理對象數組類型有以下幾個相關函數:

(1) jobjectArray NewObjectArray(jsize length, jclass elementClass, jobject initialElement );
創建對象數組,創建一個長度爲length,並且持有類型爲elementClass的對象的對象數組,數組中的所有元素都被置爲initialElement

(2) jobject GetObjectArrayElement(jobjectArray array, jsize Index);
獲取數組元素,通過Index指定的索引在array中獲取一個對象,如果索引超出邊界,會拋出一個IndexOutOfBoundsException

(3) void SetObjectArrayElement(jobjectArray array, jsize index,jobject value);
設置元素值。在array中通過index指定的索引處設置元素值爲value,如果index超出邊界,會拋出一個IndexOutOfBoundException。

2.3 java向native傳遞自定義java對象

定義一個Java層方法:

package zqc.com.example;
/**
 * Created by zhangqianchu on 2018/2/1.
 */
class Person {
    long id;
    String name;
    int age;

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

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

定義一個Java的native方法:

    //Java向native傳自定義類對象
    public native void java2jniMethod3(Person person);

在native層實現

extern "C"
JNIEXPORT void JNICALL
Java_zqc_com_example_NativeTest_java2jniMethod3(JNIEnv *env, jobject instance, jobject person) {
    jclass cls = env->FindClass("zqc/com/example/Person");
    if (cls == 0) {
        LOGE("find class fail");
        return;
    }
    jmethodID mid_ID = env->GetMethodID(cls, "setId", "(J)V");
    jmethodID mid_Name = env->GetMethodID(cls, "setName", "(Ljava/lang/String;)V");
    jmethodID mid_Age = env->GetMethodID(cls, "setAge", "(I)V");
    if (mid_ID && mid_Name && mid_Age) {
        env->CallVoidMethod(person, mid_ID, 100L);
        jstring name = env->NewStringUTF("Tianyu");
        env->CallVoidMethod(person, mid_Name, name);
        env->CallVoidMethod(person, mid_Age, 18);
        return;
    }
}

在Java端調用

Person person = new Person();
test.java2jniMethod3(person);
Toast.makeText(this, person.toString(),Toast.LENGTH_SHORT).show();

從Java端傳對象實例給native時,到native端任何對象都變爲jobject類型,如果要做對該對象實例的任何操作需先獲取該對象的jfieldID ,jmethodID,然後通過env->CallXXXMethod來操作該對象的方法其中第一個參數是該對象的具體實例,其中env->CallStaticXXXMethod方法用來調用該類的靜態方法,調用靜態方法的時候就不用傳具體的對象過去了。

2.4 java向native傳遞List對象

定義Java的native方法

//Java向native傳List對象
public native void java2jniMethod4(List<Person>people);

在native中實現具體方法:

extern "C"
JNIEXPORT void JNICALL
Java_zqc_com_example_NativeTest_java2jniMethod4(JNIEnv *env, jobject instance, jobject people) {
    //下面所有操作都得先判斷是否爲空。。。
    jclass cls = env->GetObjectClass(people);
    jclass pcls = env->FindClass("zqc/com/example/Person");
    //jmethodID getNameMid = env->GetMethodID(pcls, "getName", "()Ljava/lang/String;");
    jmethodID  setNameMid = env->GetMethodID(pcls, "setName", "(Ljava/lang/String;)V");
    //獲取List的get方法id
    jmethodID getMid = env->GetMethodID(cls, "get", "(I)Ljava/lang/Object;");
    //獲取List的長度
    jmethodID sizeMid = env->GetMethodID(cls, "size", "()I");
    int len = env->CallIntMethod(people, sizeMid);
    for (int i = 0; i < len; ++i) {
        //獲取第i個元素
        jobject  data = env->CallObjectMethod(people, getMid, i);
        env->CallVoidMethod(data, setNameMid, env->NewStringUTF("全部隨我native"));
    }
}

jclass cls = env->GetObjectClass(people);這是獲取一個對象實例相應的類的最好的辦法。從上面可以看出List在傳到native時也是變成了jobject,然後具體操作都得通過env->GetObjectClass先獲取到該類,然後獲取到該類的具體jmethodID,jfieldID來完成相應的操作。調用的方法也是env->CallXXXMethod()。
然後在Java端調用該native方法:

    NativeTest test = new NativeTest();
    List<Person> people = new ArrayList<>();
    for (int i = 0; i < 3; i++) {
        Person person1 = new Person();
        person1.setName("我是Java層");
        people.add(person1);
    }
    test.java2jniMethod4(people);
    Iterator<Person> iterator = people.iterator();
    while (iterator.hasNext()) {
        Person person1 = iterator.next();
        Log.e("myndk", person1.getName() + "\n");
    }

在上面即可通過native將Person的name全部進行了更改。

上面都是Java向native傳參,基本用法都類似。基本數據類型有相應的對照表,對象類型的都轉爲jobject,對對象的操作都是先獲取該對象jclass,jmethodID,jfielID後再對對象實例進行操作。

3. 總結

在JNI裏面的方法有很多,記起來也較爲麻煩。當有不會用時,可以參考以下手冊,裏面翻譯了JNI常用的方法。
http://www.ceeger.com/Script/AndroidJNI/AndroidJNI.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章