这篇主要写一下JNI的Native开发, 没看JNI基础知识的可以去上一篇文章看一下。
JNI初识,函数规范,基本数据类型,与之对应签名
这里我们要在Android Studio中进行JNI的开发,首先创建一个NDK工程,然后打开cpp目录下的 native-lib.cpp 进行jni的开发。其中有一些c与c++的语法,如果又看不懂的可以看我的几篇别的文章了解一下c与c++语法。
首先说一下jni基本数据类型的使用
#include "jni.h"
#include <iostream>
//c++中需要以c的方式编译
extern "C"
//JNIEnv: 由Jvm传入与线程相关的变量。定义了JNI系统操作、java交互等方法。
//jobject: 表示当前调用对象,即 this , 如果是静态的native方法,则获得jclass
JNIEXPORT jstring JNICALL Java_com_dongnao_jniTest_ExampleUnitTest_test
(JNIEnv *env, jobject, jint i, jstring j, jfloat k) {
// 获得c字符串
// 参数1 需要获取的字符串
// 参数2 是否拷贝
//提供一个boolean(int)指针,用于接收jvm传给我们的字符串是否是拷贝的。
//通常,我们不关心这个,一般传个NULL就可以
const char* str = env->GetStringUTFChars(j, JNI_FALSE);
char returnStr[100];
//格式化字符串
sprintf_s(returnStr, "C++ string:%d,%s,%f\n", i, str, k);
//释放掉内存 x
env->ReleaseStringUTFChars(j,str);
// 返回java字符串
return env->NewStringUTF(returnStr);
}
在java 中的定义与调用该Native方法:
//调用Nitive 方法
String s = test(1,"java", 1.1f);
//写一个方法,但是不实现,直接就按这种方式定义,以供与Nativie方法链接
native String test(int i,String j,float k);
输出:
这个Native方法中我们可以看出,除了jstring类型的数据外,其他我们可以直接使用。而jstring类型的数据我们需要使用 env 变量,然后通过 GetStringUTFChars 获取char 指针,而且使用完之后需要 ReleaseStringUTFChars 进行手动释放。‘运行之后输出了相应的结果。
接下来我们看一下c/c++中获取java数组的写法:
java 中的定义和使用 :
int i[] = {11,22,33,44};
String j[] = {"测","试"};
testarr(i,j);
native int testarr(int[] i, String[] s);
Native方法:
extern "C"
JNIEXPORT jint JNICALL Java_com_example_costomviewtext_ExampleUnitTest_testarr
(JNIEnv *env, jobject instance,jintArray i_,jobjectArray s_)
{
//指向数组首元素地址
//第二个参数:指针;指向内存地址
//在这个地址存数据
//true:是拷贝的一个新数据(新申请内存)
//false:就是使用的java的数组(地址)
jint *i = env->GetIntArrayElements(i_, NULL);
int32_t length = env->GetArrayLength(i_);
for (int k = 0; k < length; ++k)
{
printf("java---jint%d\n",*(i+k));
}
env->ReleaseIntArrayElements(i_,i,0);
jint strlength = env->GetArrayLength(s_);
for (int k = 0; k < strlength; ++k)
{
jstring str = static_cast<jstring>(env->GetObjectArrayElement(s_, k));//根据下标获取数组中的
//转成可操作的字符串
const char* s = env->GetStringUTFChars(str, 0);
printf("java----jstring %s\n",s);
env->ReleaseStringUTFChars(str, s);
}
return 100;
}
输出两个数组中的值:
这个方法接收了两个java数组,我们这里针对有差异性的两种数组进行针对性处理。
首先试 jint 类型的数组,在这里我们使用 GetIntArrayElements 获取数组指针,接着使用 GetArrayLength 获取数组长度,之后使用循环数组指针,最后使用数组指针记得释放。
然后我们处理 jobject 类型的数组,这里我们也是使用 GetArrayLength 获取数组长度,之后循环获取,然后使用c++中的类型转换转换到 jstring 类型,最后在使用上面同样的获取jstring类型的方法,就可以使用了,同样不能忘记释放内存。
最后看看Native中c/c++反射java,反射调用方法,反射修改属性 :
java代码:
//java中的类 :
public class Bean {
private static final String TAG = "Bean";
private int i = 100;
public int getI() {
return i;
}
public void setI(int i) {
System.out.println("setI success :"+i);
this.i = i;
}
public static void printInfo(String msg){
System.out.println(msg);
}
}
//java中的定义与调用:
native void paseObject(Bean bean, String str);
Bean bean = new Bean();
paseObject(bean);
Native方法:
extern "C"
JNIEXPORT void JNICALL Java_com_example_costomviewtext_ExampleUnitTest_paseObject
(JNIEnv *env,jobject instance,jobject bean)
{
//反射调用java的方法
//1.获取java对应的class对象
jclass beanCls = env->GetObjectClass(bean);
//2.找到要调用的方法
jmethodID getI = env->GetMethodID(beanCls, "getI", "()I"); // 签名
//3.调用
jint i = env->CallIntMethod(bean, getI);
cout << i << endl;
//set方法
jmethodID setI = env->GetMethodID(beanCls, "setI","(I)V");
env->CallVoidMethod(bean, setI, 200);
//static 方法
jmethodID printInfo = env->GetStaticMethodID(beanCls, "printInfo", "(Ljava/lang/String;)V");
//创建java字符串
jstring str2 = env->NewStringUTF("printInfo is called ");
env->CallVoidMethod(beanCls,printInfo, str2);
//释放局部引用
env->DeleteLocalRef(str2);
//修改属性值
jfieldID fileId = env->GetFieldID(beanCls,"i","I");//获取属性
env->SetIntField(bean,fileId,300);
jint amendi = env->CallIntMethod(bean, getI);
cout << "修改后的i:" << amendi << endl;
env->DeleteLocalRef(beanCls);
}
输出:
在这里我们通过 GetObjectClass 反射获取 jclass 对象, 然后使用 GetMethodID 找到要调用的方法,这里就用到了上篇
文章中说的签名了。
GetMethodID 这个方法第一个参数需要传 jclass 对象,第二个参数需要传我们要找的方法名,第三个参数需要传方法的
签名,可以看到上面我们的Bean类中的 getI 方法不需要传参数,返回值是int ,所以这里对应的就是 ()I 括号内的是穿的参数,
后面带着返回值,int对应的是I,所以这里就传 “()I”。
然后接着 CallVoidMethod 调用就可以了。
后面的 setI 的调用流程和 getI 一样了,不同的是,setI 的需要传一个int值,返回值是void,所以这里它的签名就是"(I)V"。
在 CallVoidMethod 调用 setI 的时候还得将参数传过去。
还有一个static方法的示例,与普通不同的是 这里要使用的方法是 GetStaticMethodID ,参数都一样,上面我举例的这个
static方法的参数是一个string值,所以这里他的签名就是 "(Ljava/lang/String;)V" 这样。
修改属性的就使用 GetFieldID 获取,SetIntField 修改。
说了这么多,其实具体说来不怎么难,都是一些api的调用,不涉及一些逻辑算法这些,大家看过之后练习几遍自然就掌握了,这些东西在进行Ndk开发,比如我们自己编译了ffmpeg的库,然后在调用其中的一些方法,与之链接的话就需要使用JNI进行开发,JNI在其中扮演的角色就是中间链接,java 调用 Native 方法 使用native 定义之后使用,而Native调用java的时候就需要用到反射了,这部分比较重要,稍微麻烦点。