Android NDK——必知必会之从Java 传递各种数据到C/C++进行处理全面详解(四)

版权声明:本文为CrazyMo_原创,转载请在显著位置注明原文链接 https://blog.csdn.net/CrazyMo_/article/details/82080879

引言

前一篇文章基本上把JNI 所涉及到的基本常识都总结了一遍,如果你已经(具有C/C++的基础)认真读完并且亲自动手操作,相信JNI对你来说已经不会陌生了,这篇将会具体总结本地语言和Java之间的交互操作(代码仅仅是为了说明功能,实际使用过程中还得进行优化。),不过在讲解前还需要补充一些进阶的理论知识官方指导文档,此系列文章基链接:

一、JNI_OnLoad()与JNI_OnUnload()概述

1、JNI的入口函数——JNI_OnLoad()

当虚拟机VM(Virtual Machine)通过System类相应的静态函数(如System.loadLibrary()、System.load())加载动态库时,内部就会去查找动态库中的 JNI_OnLoad 函数(也就是说可以通过这个函数是否被执行来判定你的SO是否已经被成功加载,需要注意的是一个项目中只能有一个JNI_OnLoad函数 的实现,如果在多个文件中都实现则会发生异常),通常这个函数可以完成以下任务:

  • 告诉VM此C组件使用那一个JNI版本(目前Android NDK的jni.h中只支持JNI_VERSION_1_2 、JNI_VERSION_1_4、JNI_VERSION_1_6)。若你的动态库没有提供JNI_OnLoad()函数,VM会默认该动态库使用最老的JNI 1.1版本,这样很多JNI的新版功能就无法使用,例如JNI 1.4的java.nio.ByteBuffer就必须藉由JNI_OnLoad()函数来告知VM。

  • 保存全局JavaVM

/**
 *
 * @param vm JavaVM,它是虚拟机在JNI层的代表,从JDK / JRE 1.2开始,不支持在单个进程中创建多个VM。那么在整个进程中javaVM只有一个,
 * 如果后面要使用,可以保存下来。 JNI接口指针(JNIEnv)仅在当前线程中有效。如果另一个线程需要访问Java VM,它必须先调用 AttachCurrentThread函数(在函数内部会自动通过vm为当前线程构造对应的JNIEnv的)自己附加到VM并获得一个JNI接口指针(JNIEnv)。
 * 一旦连接到虚拟机,本地线程就像在本地方法内运行的普通Java线程一样工作。本地线程保持连接到VM直到它调用DetachCurrentThread() 分离自己。
 * 连接到VM的本地线程在退出之前必须调用DetachCurrentThread()以分离它自己。如果调用堆栈上有Java方法,则线程无法自行分离。
    简而言之调用JavaVM的AttachCurrentThread函数,就可得到这个线程的JNIEnv结构体。这样就可以在后台线程中回调Java函数了。
    另外,后台线程退出前,需要调用JavaVM的DetachCurrentThread函数来释放对应的资源。
 * @param reserved 从JVM传过来就是一个NULL所以不用管
 * @return 返回的JNI的版本
 */
jint JNI_OnLoad(JavaVM *vm, void *reserved);
  • 执行一些初始化操作 比如说获取env
//获取java中的类,以便后面调用java中的函数
JNIEnv *env;
if ((*vm)->GetEnv(vm, (void **) &env, JNI_VERSION_1_6) != JNI_OK) {
    log_android(ANDROID_LOG_INFO, "JNI load GetEnv failed");
    return -1;
 }
 const char *packet = "com/xxx/xxx/jniClass/Packet";
clsPacket = jniGlobalRef(env, jniFindClass(env, packet));
  • 执行动态注册

2、JNI_UNLoad

当GC回收了加载这个库的ClassLoader时,该函数被调用,该函数可用于执行清理操作。由于这个函数是在未知的上下文中调用的,所以程序员在使用Java VM服务时应该保持谨慎,并且避免任意的Java回调

    JNIEnv *env;
    if ((*vm)->GetEnv(vm, (void **) &env, JNI_VERSION_1_6) != JNI_OK)
        log_android(ANDROID_LOG_INFO, "JNI load GetEnv failed");
    else {
        (*env)->DeleteGlobalRef(env, clsPacket);
        (*env)->DeleteGlobalRef(env, clsRR);
    }

二、JNI的静态注册和动态注册

之前我们一直在JNI中使用的 Java_PACKAGENAME_CLASSNAME_METHODNAME 固定格式来与Java方法进行匹配即静态注册。而动态注册则是在JNI_OnLoad方法通过代码动态绑定的,其实在Android aosp源码中就大量的使用了动态注册的形式

1、静态注册的步骤

  • 按照普通语法声明Java语言层中Native方法[编译成.class文件]
  • 在本地语言C/C++层根据Java类的方法名称定义实现。

注意:中括号中的编译成.class字节码这并不是必须的,仅仅是为了让你能够通过javah生成对应的本地语言的方法签名而已,现在在AndroidStudio 已经十分简单了,在配置了对应的环境之后直接按默认万能键 alt+enter键就可以自动为你生成。

2、动态注册步骤

2.1、 按照普通语法声明Java语言层中Native方法

package com.crazymo.ndk.jni;

/**
 * Auther: Crazy.Mo on 2018/8/31 15:04
 * Summary:
 */
public class DynamicRegistJNI {
    native public void dynamicRegist();
    native public void dynamicRegist(int id);
}

2.2、在本地语言C/C++层根据Java类的方法名称定义实现

//1、C++代码 若无参数需要传递,JNIEnv *env, jobject jobj可省,若传递参数必须
void  dynamic(JNIEnv *env, jobject jobj){
    LOG("dynamicRegist 已动态注册到dynamic(无参数)");
}
void  dynamic2(JNIEnv *env, jobject jobj,jint i){
    LOG("dynamicRegist 已动态注册dynamic(%d)",i);
}

2.3、定义一个JNINativeMethod数组用于保存Java代码中对应的native方法

//jni.h这JNINativeMethod包含三部分:{Java层的方法名,方法签名,(void*)本地方法名}
typedef struct {
    const char* name;
    const char* signature;
    void*       fnPtr;
} JNINativeMethod;
//2、需要动态注册的方法数组,这JNINativeMethod包含三部分:{Java层的方法名,方法签名,(返回值*)本地方法名,})
static const JNINativeMethod mMethods[] = {
        {"dynamicRegist","()V", (void *)dynamic},
        {"dynamicRegist", "(I)V", (void *)dynamic2}
};

2.4、声明需要注册Native函数的Java完整类名(把. 改成 /)

2.5、实现JNI_OnLoad方法并在内部完成注册

完整代码如下:

// Created by Crazy.Mo on 2018/8/31.
#include <jni.h>
#include <string>
#include <android/log.h>

#define  LOG(...) __android_log_print(ANDROID_LOG_ERROR,"CrazyMoJNI",__VA_ARGS__);

//1、C++代码 若无参数需要传递,JNIEnv *env, jobject jobj可省,若传递参数必须
void  dynamic(JNIEnv *env, jobject jobj){
    LOG("dynamicRegist 已动态注册到dynamic(无参数)");
}
void  dynamic2(JNIEnv *env, jobject jobj,jint i){
    LOG("dynamicRegist 已动态注册dynamic(%d)",i);
}

//2、需要动态注册的方法数组,这JNINativeMethod包含三部分:{Java层的方法名,方法签名,(返回值*)本地方法名,})
static const JNINativeMethod mMethods[] = {
        {"dynamicRegist","()V", (void *)dynamic},
        {"dynamicRegist", "(I)V", (void *)dynamic2}
};

//3、需要动态注册native方法的Java类名(.改为/)
static const char* mClassName = "com/crazymo/ndk/jni/DynamicRegistJNI";

//4、重写JNI_OnLoad
jint JNI_OnLoad(JavaVM* vm, void* reserved){
    JNIEnv* env = 0;
    //从vm 中获得 JniEnv
    int r = vm->GetEnv((void**) &env, JNI_VERSION_1_6);
    if( r != JNI_OK){
        return -1;
    }
    jclass javaClz = env->FindClass(mClassName);//反射获取Java层的Class,这个类不能被混淆,所有有Native方法的类都不能混淆
    // 注册 如果小于0则注册失败
    r = env->RegisterNatives(javaClz,mMethods,sizeof(mMethods)/sizeof(JNINativeMethod));//要注册Java类对应的class 数组  要注册的方法数即数组长度
    if(r!= JNI_OK )
    {
        return -1;
    }
    return JNI_VERSION_1_6;
}



这里写图片描述

三、在C/C++ 语言层接收处理Java层的数据

1、接收处理Java层的基本数据类型的简单数据

接收并处理Java基本数据类型(不包括基本类型的包装类)很简单,JNI框架会自动把Java 中的数据类型映射为JNI中对应的类型直接获取即可。

//Java代码
public native void sendBasicType(double df,int k,long lk,float f,boolean b,char c);
//C/C++代码
JNIEXPORT void JNICALL
Java_com_crazymo_ndk_jni_JNIHelper_sendBasicType(JNIEnv *env, jobject instance, jdouble df, jint k,
                                                 jlong lk, jfloat f, jboolean b, jchar c) {

    LOGE("CrazyMoJNI","接收Java传递而来的参数:df=%lf,k=%d,lk=%ld",
         df,k,lk);
    LOGE("CrazyMoJNI","接收Java传递而来的参数:f=%f b=%d,c=%c",f ,b,c);
    ///LOGE("CrazyMoJNI","接收Java传递而来的参数:df=%lf,k=%d,lk=%ld,f=%f b=%d,c=%c",df,k,lk,f ,b,c);//这是个LOG库里的天坑,这样子输出有问题
}

2、接受处理字符串及字符串数组

由于C中是没有字符串这个变量类型的而且String在Java 中是引用类型,所以处理String类型略有点特殊与其他引用类型的操作一样,都是通过JNI内部定义的函数进行操作,如果一定要总结就是使用env指针调用相应的函数

  • 通过env指针调用相应的GetStringUTFChars、GetStringChars函数把jstring字符串转为char*指针,再根据语法操作char*指针即可。

  • 通过env调用GetArrayLength函数得到数组的长度

  • 在遍历时,还需要进行static_cast把env->GetObjectArrayElement的结果转换为jstring

  • 得到jstring之后再按照处理普通jstring进行处理

  • 由于在读取字符串过程中会产生一些局部引用,为了尽量避免内存泄漏,一定要记得通过env调用对应的函数主动释放

extern "C"
JNIEXPORT void JNICALL Java_com_crazymo_ndk_jni_JNIHelper_sendString(JNIEnv *env, jobject instance, jstring s_, jobjectArray strArr)
{
    const char* s = env->GetStringUTFChars(s_, 0);//得到对应的jstring字符串指针,会自动产生局部引用
    LOGE("CrazyMoJNI","Java 传递过来的字符串为:%s",s);
    ///LOGE("CrazyMoJNI","Java 传递过来的字符串为:%c",*s);这样子输出的是首字符
    int32_t arr_len=env->GetArrayLength(strArr);
    LOGE("CrazyMoJNI","Java 传递过来的字符串数组长度为:%d",arr_len);
    for(int i=0;i<arr_len;i++){
        jstring str= static_cast<jstring >(env->GetObjectArrayElement(strArr,i));
        const char* c_str=env->GetStringUTFChars(str,0);
        LOGE("CrazyMoJNI","遍历字符串数组:%s",c_str);
        env->ReleaseStringUTFChars(str,c_str);//主动释放局部引用
    }
    env->ReleaseStringUTFChars(s_, s);//主动释放局部引用
}

3、接收并反射处理复杂引用类型

无论是任何复杂的引用类型,传递到JNI时都会被自动映射为jobject(相当于是JNI中的Object),而且在JNI层是不能直接进行各种new 操作创建对象或者调用方法的,所以JNI是通过另一种**“反射机制”来处理引用类型的,语法和Java中使用的反射机制大同小异,区别是在于需要通过env来调用对应的方法**。

  • 通过env调用GetObjectClass(jobject)或者FindClass(className)获取Java对应的class字节码对象
  • 通过env调用GetMethodID(beanclz,方法名,方法签名)获取指定的非静态普通函数Id
  • 通过env调用CallMethod(jobject,函数名)执行对应的非静态普通函数
  • 通过env调用CallStaticMethod(jobject,函数名)执行对应的非静态普通函数
extern "C"
JNIEXPORT void JNICALL Java_com_crazymo_ndk_jni_JNIHelper_sendBean(JNIEnv *env, jobject instance, jobject bean) {

    //反射调用java方法
    //1、获取java对应的class对象
    jclass beanClz = env->GetObjectClass(bean);
    //2、找到要调用的方法
    // 参数3: 签名
    //getBlogAdrr方法
    jmethodID  getAddr = env->GetMethodID(beanClz,"getBlogAdrr","()Ljava/lang/String;");
    //3、调用
    jstring addr=static_cast<jstring>(env->CallObjectMethod(bean,getAddr));
    ///jstring addr=(jstring)(env->CallObjectMethod(bean,getAddr));
    const char* blogAddr = env->GetStringUTFChars(addr, 0);//得到对应的jstring字符串指针,会自动产生局部引用
    LOG("C++ 调用Java getBlogAdrr方法:%s",blogAddr);


    jmethodID setI=env->GetMethodID(beanClz,"setId","(I)V");///GetMethodIDX接收的引用类型的字节码
    env->CallVoidMethod(bean,setI,2008);//调用无返回值的非静态方法
    jmethodID getI=env->GetMethodID(beanClz,"getId","()I");
    jint id=env->CallIntMethod(bean,getI);///CallXxxxMethod传递的是引用类型的jobject
    LOG("C++ 调用Java setId更新为:%d",id);
    //调用静态方法,无论是私有方法还是公有方法
    jmethodID showMethod=env->GetStaticMethodID(beanClz,"show","()V");
    env->CallStaticVoidMethod(beanClz,showMethod,"()V");//传递过来的是jclass

    //在Jni创建java对象:
    jclass newBeanClz = env->FindClass("com/crazymo/ndk/bean/Blog");
    //反射创建对象
    //1、获得类的构造方法,方法名固定为<init>,后面的签名为对应构造方法的签名
    jmethodID constuct = env->GetMethodID(newBeanClz,"<init>","(Ljava/lang/String;I)V");
    //创建java字符串
    jstring  newAddr = env->NewStringUTF("在JNI赋值的Addr");
    const char* c_newAddr=env->GetStringUTFChars(newAddr,0);
    LOG("C++ 调用Java创建BlogAddr字符串:%s",c_newAddr);
    //2、调用构造方法 创建对象
    jobject  bean2 = env->NewObject(newBeanClz,constuct,newAddr,999);//这里传入c_newAddr则会直接报错
    jmethodID show2=env->GetMethodID(beanClz,"show2","()V");///GetMethodIDX接收的引用类型的字节码
    env->CallVoidMethod(bean2,show2);

    //修改属性值
    jfieldID  fileId = env->GetFieldID(newBeanClz,"id","I");
    env->SetIntField(bean2,fileId,66666);
    jstring  newAddr2 = env->NewStringUTF("在JNI赋值的Addr6666");
    jfieldID newAddrField=env->GetFieldID(newBeanClz,"blogAdrr","Ljava/lang/String;");
    env->SetObjectField(bean2,newAddrField,newAddr2);
    jint id2=env->GetIntField(bean2,fileId);
    jstring blogAddr2= static_cast<jstring>(env->GetObjectField(bean2,newAddrField));
    const char* c_blog2=env->GetStringUTFChars(blogAddr2,0);
    LOG("C++ 获取Java的成员变量id=%d,blogAdrr=%s",id2,c_blog2);
    env->CallVoidMethod(bean2,show2);

    env->ReleaseStringUTFChars(newAddr,c_newAddr);
    env->ReleaseStringUTFChars(addr,blogAddr);
    env->ReleaseStringUTFChars(blogAddr2,c_blog2);
    //后面不再使用bean2了 ,我希望它引用对象占用的内存可以被马上回收
    env->DeleteLocalRef(bean2);
    env->DeleteLocalRef(newAddr2);
    
}

这里写图片描述

4、接收并反射处理数组类型

接收和处理数组类型的数据,对于具体类型数组来说调用对应的方法返回的指向数组首元素的指针,而对于对象数组则是放回数组的元素。

extern "C"
JNIEXPORT void JNICALL Java_com_crazymo_ndk_jni_JNIHelper_sendArrays(JNIEnv *env, jobject instance,
                                                                     jintArray idArry,
                                                                     jobjectArray blogArr, jobject list) {
    //读取整形数组
    int32_t id_len=env->GetArrayLength(idArry);
    LOG("id数组的长度:%d",id_len);
    if(id_len>0){
        jint* id_p=env->GetIntArrayElements(idArry, NULL);
        for (int i = 0; i < id_len; ++i) {
           LOG("id数组的元素:%d",*(id_p+i));
        }
    }
    //读取对象数组
    int32_t blog_len=env->GetArrayLength(blogArr);
    LOG("Blog对象数组的长度:%d",blog_len);
    for (int i = 0; i <blog_len ; ++i) {
        jobject blog_obj=env->GetObjectArrayElement(blogArr,i);
        jclass blog_clz=env->GetObjectClass(blog_obj);
        ///jclass blog_clz=env->FindClass("com/crazymo/ndk/jni/JNIHelper");
        jmethodID showMethod=env->GetMethodID(blog_clz,"show2","()V");
        env->CallVoidMethod(blog_obj,showMethod);
    }
}

这里写图片描述

5、接收和处理List集合类型数据

extern "C"
JNIEXPORT void JNICALL
Java_com_crazymo_ndk_jni_JNIHelper_sendList(JNIEnv *env, jobject instance, jobject listobj,
                                                  jobject map_obj) {
    //访问List
    jclass list_clz = env->GetObjectClass(
            listobj);//两种形式都可以jclass list_clz=env->FindClass("java/util/List");
    jclass list_clz2 = env->FindClass("java/util/ArrayList");//可以写ArrayList,反正是要反射调用get和size函数
    jmethodID get_methd = env->GetMethodID(list_clz, "get", "(I)Ljava/lang/Object;");
    jmethodID size_methd = env->GetMethodID(list_clz2, "size", "()I");
    jint list_len = env->CallIntMethod(listobj, size_methd);
    LOG("List集合的size:%d", list_len);
    for (int i = 0; i < list_len; i++) {
        jstring str = static_cast<jstring >(env->CallObjectMethod(listobj, get_methd, i));
        const char *s = env->GetStringUTFChars(str, 0);
        LOG("List集合的size:%s", s);
        env->ReleaseStringUTFChars(str, s);
    }
}

这里写图片描述

6、接收和处理Map集合型数据

其实处理复杂类型的数据即简单也繁杂,简单的是语法简单,繁杂的是每一步骤都需要通过反射来调用,就像是“剥洋葱”逐层剥离,注意些细节,尤其是获取方法和调用方法的时候传递的参数和签名。


extern "C"
JNIEXPORT void JNICALL
Java_com_crazymo_ndk_jni_JNIHelper_sendMap(JNIEnv *env, jobject instance,jobject map_obj) {
    /**
     * //keySet方式遍历
    Set<Person> set=map.keySet();
    Iterator<Person> it=set.iterator();//得到Set集合的迭代器
    while(it.hasNext()){
        Object key=it.next();//通过迭代器获取键
        String value=map.get(key);//map根据键获取对应的值
        System.out.println(key.toString()+":"+value);
    }
     */
    jclass map_clz = env->GetObjectClass(map_obj);//根据Map的实例获取对应的Class字节码对象
    jmethodID size_map_methd = env->GetMethodID(map_clz, "size", "()I");//获取Map字节码对象反射获取size()方法ID
    jint map_len = env->CallIntMethod(map_obj, size_map_methd);//通过Map实例调用size方法
    LOG("Map映射的size:%d", map_len);
    jmethodID keyset_method = env->GetMethodID(map_clz, "keySet",
                                               "()Ljava/util/Set;");//获取Map字节码对象获取Set<K> keySet()方法
    jobject keyset_obj = env->CallObjectMethod(map_obj,
                                               keyset_method);//调用keySet方法得到键的Set集合<=>set=map.keySet()
    jclass keyset_clz = env->GetObjectClass(
            keyset_obj);///jclass keyset_clz=env->FindClass("java/util/Set");
    jmethodID iterator_methd = env->GetMethodID(keyset_clz, "iterator", "()Ljava/util/Iterator;");
    jobject iterator_obj = env->CallObjectMethod(keyset_obj, iterator_methd);//等价于调用了set.iterator()
    jclass iterator_clz = env->GetObjectClass(iterator_obj);
    jmethodID hasnext_method = env->GetMethodID(iterator_clz, "hasNext", "()Z");
    jboolean hasnext = env->CallBooleanMethod(iterator_obj, hasnext_method);
    LOG("C++ Map的容量--hasNext=%d", hasnext);
    while (hasnext) {
        jmethodID next_method = env->GetMethodID(iterator_clz, "next",
                                                 "()Ljava/lang/Object;");//获取Iterator的next方法
        jmethodID getv_methd = env->GetMethodID(map_clz, "get",
                                                "(Ljava/lang/Object;)Ljava/lang/Object;");
        jobject key_obj = env->CallObjectMethod(iterator_obj, next_method);//调用it.next得到Key
        jstring value_obj = static_cast<jstring>(env->CallObjectMethod(map_obj, getv_methd,
                                                                       key_obj));
        const char *c_blog2 = env->GetStringUTFChars(value_obj, 0);
        LOG("C++ Map遍历的元素:%s", c_blog2);
        hasnext = env->CallBooleanMethod(iterator_obj, hasnext_method);
        env->ReleaseStringUTFChars(value_obj,c_blog2);
    }
    env->DeleteLocalRef(map_clz);
    env->DeleteLocalRef(keyset_obj);
    env->DeleteLocalRef(keyset_clz);
    env->DeleteLocalRef(iterator_obj);
    env->DeleteLocalRef(iterator_clz);
}

四、及时释放不再使用的内存避免不必要的内存泄漏

总所周知Java语言天生拥有一个GC自动回收机制,所以在Java编程时开发者不必过多关注内存的申请和释放,而C/C++则不同内存的申请和释放都绝大部分都是由开发者自己去管理的,所以在C/C++编程时候需要时刻遵守一个总则:及时主动释放不再使用的内存。一般在我们的代码中可以把所用到的元素分为(或许这样分类并不具有理论依据,仅仅个人理解):
jobjectjclass全局引用弱全局引用jstring其他局部普通变量

1、针对jobject和jclass

在编码过程中,在函数体内产生的jobject和jclass类型的变量时且没有手动创建为全局引用或弱全局引用,如果确定不再使用的话,都应该主动使用 env->DeleteLocalRef进行释放,因为所有的jobject(包括子类)默认就是局部引用。

这里jobject 类型指的是真正产生的是jobject这个类型的,而非其子类类型(jclass除外)

2、针对全局和弱全局引用

在编码过程中,在函数体内产生的*全局和弱全局引用的变量时,如果确定不再使用的话,都应该主动使用 env->DeleteGlobalRef进行释放。

3、针对jstring

通过jstring 获取C/C++字符串或者char指针时,在确定不再使用的时候都需要调用对应的ReleaseStringXxxx方法进行释放C/C++中的内存。

4、针对其他局部普通变量

虽然代码中其他变量原则上说也是继承自jobject,但是仅仅像jfieldID、jmethodID等这些非以上三种普通局部变量,在编码过程,不需要去调用相关方法进行释放。

未完待续……

猜你喜欢

转载自blog.csdn.net/CrazyMo_/article/details/82080879