文章大纲
引言
前一篇文章基本上动态注册和静态注册以及从Java传递各种数据到C/C++进行处理,正所谓来而不往非礼也,这篇是针对从在C/C++封装各种数据并传递到Java层。此系列文章基链接:
- Android NDK——必知必会之配置Windows与Linux共享及 Linux NDK 交叉编译环境配置(一)
- Android NDK——必知必会之JNI和NDK基础全面详解(二)
- Android NDK——必知必会之JNI的C++操作函数详解和小结(三)
- Android NDK——必知必会之从Java 传递各种数据到C/C++进行处理全面详解(四)
- Android NDK——必知必会之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 * 获取),然后在经过反射调用响应的方法实现的。
未完待续……