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);
相关推荐
用户095 小时前
Kotlin 将会成为跨平台开发的终极选择么?
android·面试·kotlin
Carson带你学Android3 天前
Android PC时代已到来?Chrome OS将和Android合并!
android·google·chrome os
牛蛙点点申请出战3 天前
仿微信语音 WaveView -- Compose 实现
android·前端
没有了遇见4 天前
Android 基于JitPack Fork三方库代码 修改XPopup 资源ID异常BUG 并发布到仓库
android
sxczst4 天前
Launcher3 如何获取系统上的所有应用程序?
android
sxczst4 天前
如何在悬浮窗中使用 Compose?
android
XDMrWu4 天前
Compose 智能重组:编译器视角下的黑科技
android·kotlin
vivo高启强4 天前
R8 如何优化我们的代码(1) -- 减少类的加载
android·android studio
诺诺Okami4 天前
Android Framework-WMS-从setContentView开始
android