Android JNI中Java&Kotlin与C语言的相互调用

基本上都是与算法或者说底层驱动做交互。文章有以下俩点前置条件:

  1. Jni的动态加载比较重要,本篇代码仍以动态加载作为范本。传送门:动态注册流程
  2. 按照习惯,上层还是会用Kotlin代码做示范。

1.基础数据类型的传递

java基础数据类型的传递基本大同小异,这里简单用int做示范。这里流程讲的仔细点,后面的类型就会简略的叙述。

1.1 新建一个jni接口

input:int

return:int

kotlin 复制代码
    // 1. 基础数据类型
    external fun putBasic(int: Int): Int

1.2 生成头文件

我们可以用javah命令生成头文件,得到jni函数和方法签名,顺便做下动态加载 我们得到如下函数:

c 复制代码
/*
 * 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,我这里为了省事,直接自己写了个头文件,以后工程肯定会用得到。

c 复制代码
#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生成 / 百度大法,不要浪费时间在记忆这些零碎上面。直接看下函数实现,我这里用了动态加载,当然可以选择不用,直接看函数实现就好。

c 复制代码
jint putBasic(JNIEnv *jniEnv, jobject obj, jint j_int) {
    LOGD("jni input : %d", j_int);
    int result = 0;//int类型可以作为jint类型直接返回
    return result;
}

上层调用:

kotlin 复制代码
        var int = JniUtils.instance!!.putBasic(1)
        Log.d("HM",int.toString())
        sample_text.text = int.toString()

我这里直接选择打印2个log,最后看下输出结果: 没有任何问题,简单如此。

2.基础数组类型的传递

  1. 上层接口代码:
kotlin 复制代码
   // 2. 数组类型
   external fun putArray(intArray: IntArray): IntArray
  1. 生成头文件的jni函数:
c 复制代码
/*
 * 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的指针,创建数组和多线程的操作离不开他。直接看代码。
c 复制代码
/*
 * 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

kotlin 复制代码
       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数组类型的传递

  1. 上层接口代码
kotlin 复制代码
    // 3. string和数组
    external fun putString(string: String, a: Array<String>): String
  1. 生成的头文件以及签名
c 复制代码
 *
 * 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
c 复制代码
/*
* 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. 上层调用
kotlin 复制代码
    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方法。

kotlin 复制代码
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下才能生成签名。

c 复制代码
#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添加如下:

c 复制代码
  external fun setID(int: Int)
  external fun getID(): Int
  external fun  setName(string: String)
  external fun  getName(): String

然后build → javah就会得到这个方法的签名文件了,虽然很蠢,但是我当时真的懒得查,至于现在,我纯粹为了做一下以前的蠢举动。言归正传,得到如下签名,我们只要看其中的MethodSignature就好。

c 复制代码
/*
 * 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接口:

kotlin 复制代码
   // 4.  类的实例
   external  fun putObj(person: Person)

然后jni的cpp中去实现他。哥哥姐姐们,注意看下我在todo写的俩个坑!!!这俩点至少耽误我半个小时的时间,至今为止不知道是啥原因。知道的大佬可以可以给我留言科普下!!!

c++ 复制代码
/*
 * 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);
}
​

上层调用:

kotlin 复制代码
 JniUtils.instance!!.putObj(Person("java",0))

类的初始化值为java,最后打印出Log:E/HM: Java getName = JNI,说明setName()getName()均调用成功。类的传递于反过来调用类的方法,简单如此。

4.2 在C层new类

这种方式是直接在JNI的C里面通过包名+类名路径的方式,直接实例化一个类。

1.上层接口

kotlin 复制代码
 // 5.  C层直接新建
    external  fun newObj()

2.cpp实现

注意一点是,如果一个jobject需要升级为全局变量,不能按照正常的思路赋值全局变量,一定要用到NewGlobalRef,详情大家直接百度这个函数。 详细的说明不在赘述,都在注释中说明清楚了。

c 复制代码
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);
}

5. 释放释放释放,泄露泄露泄露

JNI 基本数据类型是不需要释放的 :jint , jlong , jchar

引用数据类型需要释放:jstring,jobject ,jobjectArray,jintArray

释放 XXReleaseStringDeleteXX

c++ 复制代码
// 1.释放String
jstring getNameResult = static_cast<jstring>(env->CallObjectMethod(java_bean, getName));
const char *getNameString = env->GetStringUTFChars(getNameResult, NULL);
//调用方法
jstr = (*jniEnv)->CallObjectMethod(jniEnv, mPerson, getName);
cstr = (char*) (*jniEnv)->GetStringUTFChars(jniEnv,jstr, 0);        
//释放资源
env->ReleaseStringUTFChars(getNameResult, getNameString);
​
// 2.释放 类 、对象、方法
jclass  beanClass = env->FindClass(objPath);
env->DeleteLocalRef(beanClass);
jobject beanObj = env->AllocObject(beanClass);
env->DeleteLocalRef(beanObj);
(*jniEnv)->DeleteLocalRef(jniEnv, XXX);//注意c++和c语法差异
3 数组
jniEnv->ReleaseIntArrayElements(jArray, cIntArray, 0);
相关推荐
循环不息优化不止25 分钟前
Jetpack Compose 状态管理
android
友人.2272 小时前
Android 底部导航栏 (BottomNavigationView) 制作教程
android
努力学习的小廉3 小时前
初识MYSQL —— 事务
android·mysql·adb
阿里云云原生3 小时前
深度解析 Android 崩溃捕获原理及从崩溃到归因的闭环实践
android
.豆鲨包3 小时前
【Android】Android内存缓存LruCache与DiskLruCache的使用及实现原理
android·java·缓存
JulyYu4 小时前
【Android】针对非SDK接口的限制解决方案
android·客户端
猪哥帅过吴彦祖4 小时前
Flutter 系列教程:应用导航 - Navigator 1.0 与命名路由
android·flutter·ios
2501_916008895 小时前
iOS 跨平台开发实战指南,从框架选择到开心上架(Appuploader)跨系统免 Mac 发布全流程解析
android·macos·ios·小程序·uni-app·iphone·webview
stevenzqzq6 小时前
Android Hilt教程_构造函数
android
鹏多多6 小时前
flutter图片选择库multi_image_picker_plus和image_picker的对比和使用解析
android·flutter·ios