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

扫描二维码关注公众号,回复: 11310470 查看本文章
       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);
}

猜你喜欢

转载自blog.csdn.net/ma598214297/article/details/106456535