Android JNI中數據和方法的傳遞與調用
題記
說到jni相關的內容,滿打滿算至少搞了倆年了,基本上都是與算法或者說底層驅動做交互,這篇文章的細節其實也放在文件夾裏面一年之久了,最近心有不安,還是拿出來晾曬下,還望有緣人指正交流。文章有以下倆點前置條件:
- 我前面博客已寫過Jni的動態加載,本篇代碼仍已動態加載作爲範本。傳送門:動態註冊流程
- 按照習慣,上層我還是會用Kotlin代碼做示範。
- 上層的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函數和方法簽名,順便做下動態加載。
我們得到如下函數:
/*
* 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,最後看下輸出結果:
沒有任何問題,簡單如此。
2.基礎數組類型的傳遞
- 上層接口代碼:
// 2. 數組類型
external fun putArray(intArray: IntArray): IntArray
- 生成頭文件的jni函數:
/*
* Class: com_heima_jnitest_JniUtils
* Method: putArray
* Signature: ([I)[I
*/
JNIEXPORT jintArray JNICALL Java_com_heima_jnitest_JniUtils_putArray
(JNIEnv *, jobject, jintArray);
- 函數實現。
這裏就牽扯到使用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對比,符合預期。
3.String/String數組類型的傳遞
- 上層接口代碼
// 3. string和數組
external fun putString(string: String, a: Array<String>): String
- 生成的頭文件以及簽名
*
* 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);
- 函數實現
這裏注意一下上面生成的函數裏面的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層數據");
}
- 上層調用
var returnString = JniUtils.instance!!.putString("java", arrayOf("a", "b", "c"))
Log.d("HM", returnString)
看下log,與預期一致:
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的接口方法,在javap
和javah
下才能生成簽名。
#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就會得到這個方法的簽名文件了,雖然很蠢,但是我當時真的懶得查,至於現在,我純粹爲了做一下以前的蠢舉動。言歸正傳,得到如下簽名,我們只要看其中的Method
和Signature
就好。
/*
* 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);
}