说到jni相关的内容,满打满算至少搞了俩年了,基本上都是与算法或者说底层驱动做交互,这篇文章的细节其实也放在文件夹里面一年之久了,最近心有不安,还是拿出来晾晒下,还望有缘人指正交流。文章有以下俩点前置条件:
java基础数据类型的传递基本大同小异,这里简单用int做示范。这里流程讲的仔细点,后面的类型就会简略的叙述。
input:int
return:int
// 1. 基础数据类型
external fun putBasic(int: Int): Int
我们可以用javah命令生成头文件,得到jni函数和方法签名,顺便做下动态加载。
我们得到如下函数:
/*
* Class: com_heima_jnitest_JniUtils
* Method: putBasic
* Signature: (I)I
*/
jint JNICALL Java_com_heima_jnitest_JniUtils_putBasic
(JNIEnv *, jobject, jint);
我感觉有必要补充一下,在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__);
从上面的生成函数可以发现,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. 数组类型
external fun putArray(intArray: IntArray): IntArray
/*
* Class: com_heima_jnitest_JniUtils
* Method: putArray
* Signature: ([I)[I
*/
JNIEXPORT jintArray JNICALL Java_com_heima_jnitest_JniUtils_putArray
(JNIEnv *, jobject, jintArray);
/*
* 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())
}
// 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
,CjobjectArray
。/*
* 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)
这个算是稍微复杂的部分,其实也是一直以来我认为最能提升效率的部分,可以在上层传入一个new好的类,操作其中的变量和方法,简直不要太好用。
俩个变量 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")
}
}
照常找到这个文件的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);
首先我们传递类的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,又快第二天了。努力可能会撒谎,但是努力一定不会白费。
这种方式是直接在JNI的C里面通过包名+类名路径的方式,直接实例化一个类。
// 5. C层直接新建
external fun newObj()
注意一点是,如果一个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);
}