Android JNI手冊——Java/Kotlin與Native層的相互調用

題記

說到jni相關的內容,滿打滿算至少搞了倆年了,基本上都是與算法或者說底層驅動做交互,這篇文章的細節其實也放在文件夾裏面一年之久了,最近心有不安,還是拿出來晾曬下,還望有緣人指正交流。文章有以下倆點前置條件:

  1. 我前面博客已寫過Jni的動態加載,本篇代碼仍已動態加載作爲範本。傳送門:動態註冊流程
  2. 按照習慣,上層我還是會用Kotlin代碼做示範。
  3. 上層的Jni接口文件我採用了kotlin的單例模式,當然也可以用java,其實都是大同小異,無傷大雅。我前面的博客有對比過差異,完整的代碼也有。傳送門:Kotlin與Java單例模式的比較

1.基礎數據類型的傳遞

java基礎數據類型的傳遞基本大同小異,這裏簡單用int做示範。這裏流程講的仔細點,後面的類型就會簡略的敘述。

1.1 新建一個jni接口

input:int
return:int

    // 1. 基礎數據類型
    external fun putBasic(int: Int): Int

1.2 生成頭文件

我們可以用javah命令生成頭文件,得到jni函數和方法簽名,順便做下動態加載
javah
我們得到如下函數:

/*
 * Class:     com_heima_jnitest_JniUtils
 * Method:    putBasic
 * Signature: (I)I
 */
 jint JNICALL Java_com_heima_jnitest_JniUtils_putBasic
  (JNIEnv *, jobject, jint);

1.3 jni中Android的Log

我感覺有必要補充一下,在jni函數中打印Android 的log需要引入android/log.h,我這裏爲了省事,直接自己寫了個頭文件,以後工程肯定會用得到。

#define TAG "HM"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG,  __VA_ARGS__);
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG,  __VA_ARGS__);
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG,  __VA_ARGS__);

1.4 實現函數

從上面的生成函數可以發現,int在傳遞到c層之後,編程了jint類型,基礎數據類型相似的轉換還有很多double→jdouble float→jfloat。這些都沒有必要可以記住,用的多了自然記住了,或者直接javah生成 / 百度大法,不要浪費時間在記憶這些零碎上面。直接看下函數實現,我這裏用了動態加載,當然可以選擇不用,直接看函數實現就好。

jint putBasic(JNIEnv *jniEnv, jobject obj, jint j_int) {
    LOGD("jni input : %d", j_int);
    int result = 0;//int類型可以作爲jint類型直接返回
    return result;
}

上層調用:

        var int = JniUtils.instance!!.putBasic(1)
        Log.d("HM",int.toString())
        sample_text.text = int.toString()

我這裏直接選擇打印2個log,最後看下輸出結果:
basic log
沒有任何問題,簡單如此。

2.基礎數組類型的傳遞

  1. 上層接口代碼:
   // 2. 數組類型
   external fun putArray(intArray: IntArray): IntArray
  1. 生成頭文件的jni函數:
/*
 * Class:     com_heima_jnitest_JniUtils
 * Method:    putArray
 * Signature: ([I)[I
 */
JNIEXPORT jintArray JNICALL Java_com_heima_jnitest_JniUtils_putArray
  (JNIEnv *, jobject, jintArray);
  1. 函數實現。
    這裏就牽扯到使用JNIEnv 這個Jni的指針,創建數組和多線程的操作離不開他。直接看代碼。
/*
 * Class:     com_heima_jnitest_JniUtils
 * Method:    putArray
 * Signature: ([I)[I
 */
jintArray putArray(JNIEnv *jniEnv, jobject jObj, jintArray jArray) {
    /*第一部分:讀取數組*/
    //1.獲取數組長度 GetArrayLength(java中Int數組)
    int arraySize = jniEnv->GetArrayLength(jArray);
    // 2.java中數組 → C語言數組  GetIntArrayElements(java中Int數組,是否copy)
    int *cIntArray = jniEnv->GetIntArrayElements(jArray, NULL);
    LOGD("input array");
    for (int i = 0; i < arraySize; ++i) {
        LOGD("%d", cIntArray[i]);
        *(cIntArray + i) += 10; //將數組中的每個元素加10
    }

    /*第二部分:返回數組*/

    /* 1. new一個 jintArray
     * NewIntArray(數組長度)
     */
    jintArray returnArray = jniEnv->NewIntArray(arraySize);

    /* 2. 把上面修改過的cIntArray賦值到新建的returnArray中去
     *  SetIntArrayRegion(jintArray,起始位置,長度,c中已經準備好的數組int *cIntArray)
     */
    jniEnv->SetIntArrayRegion(returnArray, 0, arraySize, cIntArray);


    /* 既然開闢了空間,一定要去釋放
     *  關於第三個參數:mode:
     *  0 → 刷新Java數組並釋放C數組
     *  1 → 只刷新Java數組,不釋放C數組
     *  2 → 只釋放
     * */
    jniEnv->ReleaseIntArrayElements(jArray, cIntArray, 0);

    return returnArray;
}

4.上層調用與log

       var inputIntArray:IntArray=intArrayOf(0,1,2);
        var returnArray=JniUtils.instance!!.putArray(inputIntArray)
        Log.d("HM", "return array")
        for (element in returnArray){
            Log.d("HM", element.toString())
        }

通過下圖的倆個log對比,符合預期。
array log

3.String/String數組類型的傳遞

  1. 上層接口代碼
    // 3. string和數組
    external fun putString(string: String, a: Array<String>): String
  1. 生成的頭文件以及簽名
 *
 * Class:     com_heima_jnitest_JniUtils
 * Method:    putString
 * Signature: (Ljava/lang/String;[Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_heima_jnitest_JniUtils_putString
  (JNIEnv *, jobject, jstring, jobjectArray);
  1. 函數實現
    這裏注意一下上面生成的函數裏面的jobjectArray,我們傳遞的是Array<String>,C
    中沒有這個明確的類型,所以就轉換爲一個jobjectArray
/*
* Class:     com_heima_jnitest_JniUtils
        * Method:    putString
        * Signature: (Ljava/lang/String;[Ljava/lang/String;)Ljava/lang/String;
*/
jstring putString(JNIEnv *env, jobject obj, jstring jstring1, jobjectArray jobjectArray1) {
    /* 一. string相關 */
    LOGD("jstring: %s", jstring1)//這樣直接打印jstring是打印不出來東西的,需要轉換jstring → const char 才能打印出來內容
    const char *str = env->GetStringUTFChars(jstring1, NULL);
    LOGD("const char:%s", str)

    /* 二. string數組相關 簡單說就是類型轉換 → 遍歷數組 → 轉換string */
    // 1.獲取數組長度
    jsize size = env->GetArrayLength(jobjectArray1);
    LOGD("java input ")
    for(int i=0;i<size;i++)
    {
        // 2. 遍歷並強轉爲其中的每個jstring
        jstring obj = (jstring)env->GetObjectArrayElement(jobjectArray1,i);
        // 3.得到字符串
        //std:: string str = (std::string)env->GetStringUTFChars(obj,NULL); //Android的 log無法打印std:: string???我懵逼了
        const char * str = env->GetStringUTFChars(obj,NULL);
        LOGD("const char:%s", str)
        // 4.必須記得釋放!!!
        env->ReleaseStringUTFChars(obj, str);
    }
    return env->NewStringUTF("C層數據");
}
  1. 上層調用
    var returnString = JniUtils.instance!!.putString("java", arrayOf("a", "b", "c"))
    Log.d("HM", returnString)

看下log,與預期一致:
put string

4.類與方法調用

4.1 上層傳入類

這個算是稍微複雜的部分,其實也是一直以來我認爲最能提升效率的部分,可以在上層傳入一個new好的類,操作其中的變量和方法,簡直不要太好用。

1. 新建一個類

倆個變量 name和id,注意我是用Kotlin寫的,自動生成的有get和set方法,用Java寫的小夥伴記住自己加上set和get方法。

class Person{
    private val tag = Person::class.java.name
    var name:String = "init"
    var id:Int = 0

    constructor(name: String, id: Int) {
        this.name = name
        this.id = id
    }

    fun printVar(): Unit {
        Log.d(tag, "name:$name,id:$id")
    }
}

2. 獲取類的簽名

照常找到這個文件的class路徑,然後輸入javah命令查看整個類的簽名,裏面自然包括所有方法和屬性。然而你看下面,你現在活得不到任何有用的簽名信息,爲什麼呢?因爲只有Java中的public native void和Kotlin中的public final external fun作爲開頭的方法作爲Jni的接口方法,在javapjavah下才能生成簽名。

#ifndef _Included_com_heima_jnitest_Person
#define _Included_com_heima_jnitest_Person
#ifdef __cplusplus
extern "C" {
#endif
#ifdef __cplusplus
}
#endif
#endif

如果你功力深厚,可以不需要這些,如果功力不夠,就只能百度查表了。但是一年前的我其實都沒有選擇,我手動把方法加上前面說的Jni方法關鍵字,然後再敲命令生成(這次無奈選擇的Kotlin作爲主語言,用到的set/get方法都是自動生成的,迫於無奈,我把方法直接粘貼到Jni接口類裏面去生成了)。
接口文件JniUtils添加如下:

  external fun setID(int: Int)
  external fun getID(): Int
  external fun  setName(string: String)
  external fun  getName(): String

然後build → javah就會得到這個方法的簽名文件了,雖然很蠢,但是我當時真的懶得查,至於現在,我純粹爲了做一下以前的蠢舉動。言歸正傳,得到如下簽名,我們只要看其中的MethodSignature就好。

/*
 * Class:     com_heima_jnitest_JniUtils
 * Method:    setID
 * Signature: (I)V
 */
JNIEXPORT void JNICALL Java_com_heima_jnitest_JniUtils_setID
  (JNIEnv *, jobject, jint);

/*
 * Class:     com_heima_jnitest_JniUtils
 * Method:    getID
 * Signature: ()I
 */
JNIEXPORT jint JNICALL Java_com_heima_jnitest_JniUtils_getID
  (JNIEnv *, jobject);

/*
 * Class:     com_heima_jnitest_JniUtils
 * Method:    setName
 * Signature: (Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_com_heima_jnitest_JniUtils_setName
  (JNIEnv *, jobject, jstring);

/*
 * Class:     com_heima_jnitest_JniUtils
 * Method:    getName
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_heima_jnitest_JniUtils_getName
  (JNIEnv *, jobject);

3. 實現

首先我們傳遞類的Jni接口:

   // 4.  類的實例
   external  fun putObj(person: Person)

然後jni的cpp中去實現他。哥哥姐姐們,注意看下我在todo寫的倆個坑!!!這倆點至少耽誤我半個小時的時間,至今爲止不知道是啥原因。知道的大佬可以可以給我留言科普下!!!

/*
 * Class:     com_heima_jnitest_JniUtils
 * Method:    putObj
 * Signature: (Lcom/heima/jnitest/Person;)V
 */
void putObj(JNIEnv *env, jobject thiz, jobject person) {
    // 1.找到jclass
    jclass personJClass = env->GetObjectClass(person);

    // 2.尋找想要調用的方法ID
    const char *sig = "(Ljava/lang/String;)V"; // 方法簽名
    //todo 第一個坑:GetMethodID第三個參數直接寫入字符串,有時候報錯,我編了倆遍,clean多次才成功運行
    jmethodID setName = env->GetMethodID(personJClass, "setName", sig);
    //jmethodID setName = env->GetMethodID(personJClass, "setName", "(Ljava/lang/String;)V");//有時報錯!!!
    const char *sig2 = "()Ljava/lang/String;"; // 方法簽名
    jmethodID getName = env->GetMethodID(personJClass, "getName", sig2);

    // 3.調用
    jstring value = env->NewStringUTF("JNI");
    // todo  第二個坑:CallVoidMethod傳入上面獲得的personJClass會調用失敗(不報錯) 傳入函數入口的jobject=person調用成功
    //env->CallVoidMethod(personJClass, setName, value);
    env->CallVoidMethod(person, setName, value);
    //返回類型jobject 需要轉換類型
    jstring getNameResult = static_cast<jstring>(env->CallObjectMethod(person, getName));
    //轉爲const char*方可打印
    const char *getNameString = env->GetStringUTFChars(getNameResult, NULL);
    LOGE("Java getName = %s", getNameString)

    //4.用完一定釋放啊baby!!!!!!
    env->ReleaseStringUTFChars(getNameResult, getNameString);
}

上層調用:

 JniUtils.instance!!.putObj(Person("java",0))

類的初始化值爲java,最後打印出Log:E/HM: Java getName = JNI,說明setName()getName()均調用成功。類的傳遞於反過來調用類的方法,簡單如此。

2020年6月3日00:02:09,又快第二天了。努力可能會撒謊,但是努力一定不會白費。

4.2 在C層new類

這種方式是直接在JNI的C裏面通過包名+類名路徑的方式,直接實例化一個類。

1.上層接口:

 // 5.  C層直接新建
    external  fun newObj()

2.cpp實現:

注意一點是,如果一個jobject需要升級爲全局變量,不能按照正常的思路賦值全局變量,一定要用到NewGlobalRef,詳情大家直接百度這個函數。
詳細的說明不在贅述,都在註釋中說明清楚了。

void newObj(JNIEnv *env, jobject obj) {
    // 1.包名+類名路徑找到類
    const char * personPath = "com/heima/jnitest/Person";
    jclass  personClass = env->FindClass(personPath);

    // 2.jclass → (實例化jobject對象
    /*
     * 創建方法的倆種方式
     * NewObject:  初始化成員變量,調用指定的構造方法
     * AllocObject:不初始成員變量,也不調用構造方法
     * */
    jobject personObj = env->AllocObject(personClass);
    // 3.調用方法簽名 一定要匹配。PS:不匹配也沒關係,因爲編譯器會報錯提示;當時輸入第二個參數時候其實第三個參數也會自己跳出來。
    const char *sig = "(Ljava/lang/String;)V";
    jmethodID setName = env->GetMethodID(personClass, "setName", sig);
    sig="(I)V";
    jmethodID setId = env->GetMethodID(personClass, "setId", sig);
    sig="()V";
    jmethodID printVar= env->GetMethodID(personClass, "printVar", sig);
    // 4.實例化對象 → 調用方法
    env->CallVoidMethod(personObj, setName,  env->NewStringUTF("CPP"));
    env->CallVoidMethod(personObj, setId, 666);
    //調用類中打印方法,看是否生效
    env->CallVoidMethod(personObj, printVar);

    // 5. 老規矩,釋放。C沒有GC,在C裏new了就要手動釋放。
    /*
     * 釋放的倆種方式:
     * DeleteLocalRef 釋放局部變量
     * DeleteGlobalRef 釋放全局變量 → JNI函數創建(NewGlobalRef)
     * */
    env->DeleteLocalRef(personClass);
    env->DeleteLocalRef(personObj);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章