Android NDK——必知必会之C/C++传递各种类型数据到Java进行处理及互相调用详解(五)

引言

前一篇文章基本上动态注册和静态注册以及从Java传递各种数据到C/C++进行处理,正所谓来而不往非礼也,这篇是针对从在C/C++封装各种数据并传递到Java层。此系列文章基链接:

一、从C/C++ 语言层传递各种数据到Java层

前面我们知道JNI的类型是和Java中的类型是一一对应的,只要严格遵守规则,对于能直接映射的数据类型要传递到Java层很简单,只要C/C++创建完毕之后直接返回即可,JNI框架会自动去进行转换,繁杂之处在于准确调用对应的函数来创建并封装成复杂的数据对象。

1、向Java层传递简单基本数据类型数据

// public native int getBasicData();

extern "C"
JNIEXPORT jint JNICALL
Java_com_crazymo_ndk_jni_JNIHelper_getBasicData(JNIEnv *env, jobject instance) {
    return 888888;
}

这里写图片描述

2、向Java 层传递字符串类型数据

//public native String getStringData();

extern "C"
JNIEXPORT jstring JNICALL
Java_com_crazymo_ndk_jni_JNIHelper_getStringData(JNIEnv *env, jobject instance) {
    char* tmpstr = "从JNI发送到Java的字符串";
    jstring rtstr = env->NewStringUTF(tmpstr);
    return rtstr;
}

这里写图片描述

3、向Java层传递数组类型数据

3.1、基本数据类型的数组

//public native int[] getArrayData(int pI);

extern "C"
JNIEXPORT jintArray JNICALL
Java_com_crazymo_ndk_jni_JNIHelper_getArrayData(JNIEnv *env, jobject instance,jint len) {
    //主要调用 set<Type>ArrayRegion 来填充数据,其他数据类型类似操作
    jintArray array = NULL;
    if (len > 0) {
        array = env->NewIntArray(len);
        jint buf[len];
        for (int i = 0; i < len; ++i) {
            buf[i] = i * 2;
        }
        // 使用 setIntArrayRegion 来赋值
        env->SetIntArrayRegion(array, 0, len, buf);
        return array;
    }
    return array;
}

这里写图片描述

3.2、复杂引用类型的数组

//public native Blog[] getObject();

extern "C"
JNIEXPORT jobjectArray JNICALL
Java_com_crazymo_ndk_jni_JNIHelper_getObject(JNIEnv *env, jobject instance) {
    jobjectArray result_arr = NULL;
    jint len = 2;
    jclass blog_clz = env->FindClass("com/crazymo/ndk/bean/Blog");//获取Blog对应的jclass
    result_arr = env->NewObjectArray(len, blog_clz, NULL);
    //获取构造方法的
	jmethodID construct_methd = env->GetMethodID(blog_clz, "<init>", "(Ljava/lang/String;I)V");
    for (int i = 0; i < len; ++i) {
        //1、创建java字符串
        jstring newAddr = env->NewStringUTF("在C++创建Java Blog对象");
        const char *c_newAddr = env->GetStringUTFChars(newAddr, 0);
        //2、调用构造方法 创建对象
        jobject blog = env->NewObject(blog_clz, construct_methd, newAddr, 6666+i*100);//这里传入c_newAddr则会直接报错
        env->SetObjectArrayElement(result_arr,i,blog);
        env->ReleaseStringUTFChars(newAddr, c_newAddr);
    }
    env->DeleteLocalRef(blog_clz);
    return result_arr;
}

这里写图片描述

4、向Java层传递集合类型数据

// public native List<String> getList(int size);

JNIEXPORT jobject JNICALL
Java_com_crazymo_ndk_jni_JNIHelper_getList(JNIEnv *env, jobject instance,jint size) {
    jclass list_clz=env->FindClass("java/util/ArrayList");
    jmethodID construct_methd=env->GetMethodID(list_clz,"<init>","()V");
    jmethodID add_method=env->GetMethodID(list_clz,"add","(Ljava/lang/Object;)Z");
    jobject list_obj=env->NewObject(list_clz,construct_methd);
    jstring newAddr = env->NewStringUTF("在C++创建Java List对象");
    for (int i = 0; i < size; i++) {
        ///const char *c_newAddr = env->GetStringUTFChars(newAddr, 0);
        ///env->CallBooleanMethod(list_obj,add_method,c_newAddr);
        env->CallBooleanMethod(list_obj,add_method,newAddr);
        //env->ReleaseStringUTFChars(newAddr, c_newAddr);
    }
    env->ReleaseStringChars(newAddr,0);
    env->DeleteLocalRef(list_clz);
    return list_obj;
}

这里写图片描述

5、向Java层传递Map类型数据

//public native Map<String,String> getMap();

extern "C"
JNIEXPORT jobject JNICALL
Java_com_crazymo_ndk_jni_JNIHelper_getMap(JNIEnv *env, jobject instance) {
    jclass map_clz=env->FindClass("java/util/HashMap");
    jmethodID construct_methd=env->GetMethodID(map_clz,"<init>","()V");
    jmethodID put_method=env->GetMethodID(map_clz,"put","(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
    jobject map_obj=env->NewObject(map_clz,construct_methd);
    jstring key = env->NewStringUTF("CrazyMo_");
    jstring value = env->NewStringUTF("在C++创建Java Map对象");
    env->CallObjectMethod(map_obj,put_method,key,value);
    return map_obj;
}

这里写图片描述

二、C/C++ 接收来自Java层的各种数据

  • 对于基础类型的数据,直接通过对应的JNI层的类型来直接赋值
  • 对于基础类型的数组,直接调用相应的API方法获取对应JNI层的类型的数组
  • 对于引用类型,通过反射间接获取
  • 对于String类型的数组,首先通过GetArrayLength函数获取数组的长度,然后再通过GetObjectArrayElement函数获取Item,再转为char*
  • 对于其他引用类型的数组,首先通过GetArrayLength函数获取数组的长度,然后再通过GetObjectArrayElement函数获取Item的,再使用反射间接获取
#include <jni.h>
#include <string>
#include <android/log.h>

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

extern "C"
JNIEXPORT void JNICALL
Java_com_crazymo_jnidemo_MainActivity_jniTest(JNIEnv *env, jobject thiz, jint num, jstring name,
                                              jintArray arry, jobjectArray strs) {
    int number=num;
    const char *str_name=env->GetStringUTFChars(name,NULL);
    LOG("num=%d\tname=%s",number,str_name);
    //用完之后,记得及时进行回收
    env->ReleaseStringUTFChars(name,str_name);
    jint *arry_int= env->GetIntArrayElements(arry,NULL);
    jsize size=env->GetArrayLength(arry);
    if(arry_int!=NULL){
        for (int i = 0; i <size ; ++i) {
            //对于基本类型的可以直接操作指针,修改值
            *(arry_int)+=100;
            LOG("arry:%d\t",*(arry_int+i));
        }
        //0 代表要刷新,改动native时java层也跟着改变的意思
        env->ReleaseIntArrayElements(arry,arry_int,0);
    }
    //遍历引用类型的数组,不能直接操作指针修改值
    jsize ref_size=env->GetArrayLength(strs);
    jobject jobj=NULL;
    for (int i = 0; i < ref_size; ++i) {
        jobj=env->GetObjectArrayElement(strs,i);
        jstring item= static_cast<jstring>(jobj);
        const char *my_str=env->GetStringUTFChars(item,NULL);
        LOG("遍历引用型字符串数组:%s\t",my_str);
        env->ReleaseStringUTFChars(item,my_str);
    }
}

测试代码:

public void test(){
    int num=2020;
    String name="crazymo_";
    int[] aryy={2018,2008,2020};
    String[] strs={"CrazyMo_","https://crazymo.blog.csdn.net/"};
    jniTest(num,name,aryy,strs);
}

public native void jniTest(int num,String name,int[] arry,String[] strs);

三、C/C++ 操作Java层的对象

  • 首先通过全类名(".“改为”_")获取对象的JNI层的jclass字节码
  • 反射调用方法,获取方法签名和jmethodID
  • 调用对应的API反射执行。
#include <jni.h>
#include <string>
#include <android/log.h>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>

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

extern "C"
JNIEXPORT void JNICALL
Java_com_crazymo_jnidemo_MainActivity_updUser(JNIEnv *env, jobject thiz, jobject user) {
    const char *usr_clz_name = "com/crazymo/jnidemo/User";
    //1、首先获取对象的字节码
    jclass usr_clz = env->FindClass(usr_clz_name);
    //2、获取对应的methodId
    const char *setName_sig = "(Ljava/lang/String;)V";//可以在Build目录下javac 目录通过javap -s去查看
    jmethodID setName = env->GetMethodID(usr_clz, "setName", setName_sig);
    //3、调用方法
    const char *params = "CrazyMo";
    jstring new_name = env->NewStringUTF(params);
    //TODO 必须注意要传入相对应的类型,此处传入char *类型则会报错,虽然都可以表示字符串
    env->CallVoidMethod(user, setName, new_name);
    LOG("当前进程:%lu", getpid())
    //!!!!一定要记得尽快回收
    env->DeleteLocalRef(usr_clz);
    env->DeleteLocalRef(user);
    //不能马上进行释放 env->ReleaseStringUTFChars(new_name,params);
}

测试

    public void testObject(){
        User user=new User();
        user.name="crazy";
        user.no=666666;
        Log.e("CrazyJava","经native 更新前:"+Thread.currentThread().getId()+"name="+user.name+"\tno="+user.no);
        updUser(user);
        Log.e("CrazyJava","经native 更新后:"+Thread.currentThread().getId()+"name="+user.name+"\tno="+user.no);
    }
    
    public native void updUser(User user);

四、C/C++ 和 Java 互相调用

  • 定义一个JavaVM 全局变量(因为JavaVM 相当于是单例的,可能很多地方都需要用到)

  • 重写JNI_OnLoad函数,目的是为了指定JNI版本和初始化我们定义的全局JavaVM变量

JavaVM* _vm=NULL;

//重写JNI_OnLoad
int JNI_OnLoad(JavaVM* vm, void* reserved){
    LOG("【JNI_OnLoad 被执行并完成JavaVM的初始化】");
    _vm=vm;
    JNIEnv* env = 0;
    //从vm 中获得 JniEnv
    int r = vm->GetEnv((void**) &env, JNI_VERSION_1_6);
    if( r != JNI_OK)
    {
        return -1;
    }
    return JNI_VERSION_1_6;
}
  • 定义本地Java接口类方法和Java回调方法

  • 实现Native 层的函数,如果需要接收来自Java层的对象,最好定义一个全局的结构体变量保存,而且还需要保存为全局引用

  • 像创建Native线程,因为需要通过反射调用Java层的回调方法,所以需要获取到对应的env,这里就是前面为什么要获取JavaVM 实例的原因了,通过JavaVM实例可以调用AttachCurrentThread函数获取对应的env

  • 通过上一步获取到的env 反射方式调用Java的方法,最后使用完毕之后再线程函数中JavaVM实例调用DetachCurrentThread 函数并且return 0

extern JavaVM *_vm;//如果已经在其他.cpp定义了,直接通过extern使用全局变量

/**
 * 因为需要回调Java层的方法,而在C/C++层回调Java方法需要一个对应的Java Class字节码对象,所以需要传递过来
 */
struct JavaContext {
    jobject instance;
};

void *interactWithJava(void *args) {
    JNIEnv *env = 0;
    LOGE("【通过JavaVM传入env执行AttachCurrentThread把env 附加到虚拟机,附加成功之后env就被赋值了】");
    //等价于_vm不等于NULL
    if (_vm) {
        jint ret = _vm->AttachCurrentThread(&env, 0); // native线程附加到Java 虚拟机,附加成功之后env就被赋值了
        if (ret == JNI_OK) {
            LOGE("【AttachCurrentThread 成功,env赋值】");
            JavaContext *context = static_cast<JavaContext *>(args);
            //等价于 context!=NULL
            if (context) {
                sleep(2);//模拟下载耗时2S
                //获得MainActivity的class对象
                jclass clz = env->GetObjectClass(context->instance);
                //com.crazymo.ndk.MainActivity
               //!!!!java.lang.ClassNotFoundException: Didn't find class "com.crazymo.ndk.jni.Blog" on path: DexPathList[[directory "."],nativeLibraryDirectories=[/system/lib64, /vendor/lib64, /product/lib64, /system/lib64, /vendor/lib64, /product/lib64]]为什么找不到?因为线程里attach时的jni 的env中 findclass用的BootClassLoader只能找到系统的类,不能找到自己编写的类。
                ///jclass clz = env->FindClass("com/crazymo/ndk/MainActivity");
                if (clz) {
                    // 反射获得方法
                    jmethodID updui_method = env->GetMethodID(clz, "update", "()V");
                    if (updui_method) {
                        LOGE("【通过反射找到要回调的Java方法的methodID并调用】");
                        env->CallVoidMethod(context->instance, updui_method);
                        delete (context);
                        env->DeleteGlobalRef(context->instance);
                        context = 0;
                    }
                }
            }
        }
    } else {
        LOGE("context==NULL");
    }
    //分离
    _vm->DetachCurrentThread();
    return 0;//线程函数必须记得返回0
}

extern "C"
JNIEXPORT void JNICALL
Java_com_crazymo_ndk_jni_JNIHelper_runNativeCallJava(JNIEnv *env, jobject instance, jobject obj) {
    //注意这里的instance 代表的是runNativeCallJava所属类的实例
    LOGE("在C/C++ 层创建Native线程...")
    pthread_t pid;
    //必须把传递过来的对象保存为全局引用,然后才能传递到另一个线程使用,否则报attempt to use stale WeakGlobal 0x3 (should be 0x7)
    JavaContext *context = new JavaContext;
    context->instance = env->NewGlobalRef(obj);
    pthread_create(&pid, 0, interactWithJava, context);//创建并启动Native线程
}

注意:在自己创建的线程中反射获取对应的class 时,在使用FindClass 的时候需要注意,因为线程里attach时的jni 的env中 findclass用的BootClassLoader只能找到系统的类,不能找到自己编写的类。

这里写图片描述

小结

无论是在本地Native 层C/C++源码中调用Java层的方法还是解析Java 传递过来的复杂数据,都是先获取对应的Java类的实例对象(通过JNIEnv * 获取),然后在经过反射调用响应的方法实现的。
未完待续……

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CrazyMo_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值