JNI开发

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

JNI (java native interface)即java本地开发接口,JNI 是一个协议,这个协议用来沟通java代码和外部的本地代码(c/c++),通过这个协议java代码就可以调用外部的c/c++代码,外部的c/c++代码也可以调用java代码,使得Java代码和其他语言写的代码(如C/C++代码)进行交互。
使用JNI好处主要有以下三点:
1、Java语言提供的类库无法满足要求,且在数学运算,实时渲染的游戏上,音视频处理等方面上与C/C++相比效率稍低。
2、Java语言无法直接操作硬件,C/C++代码不仅能操作硬件而且还能发挥硬件最佳性能。
3、使用Java调用本地的C/C++代码所写的库,省去了重复开发的麻烦,并且可以利用很多开源的库提高程序效率。


在Android Studio下,添加native支持

在项目下的gradle.properties添加:android.useDeprecatedNdk=true
1、首先新建个 java 类

public class JniTest{
    static {
		//loadLibrary传入的是so库名称,它默认路径是/data/data/+包名+/lib,安装时库文件默认会被复制到该目录下
        System.loadLibrary("jary");
		//指定加载so库的路径
		System.load(EnvironmentUtils.getPackageName() + "/libs/armeabi-v7a/libjary.so");
    }
    public native String getString();
}

然后重新编译下Project: Build–>Make Project,重新编译之后就可以在对应的文件夹看到编译后的 JniTest.class,即在build下intermediates\classes\debug\jary\com\myjnitest。这个路径根据Android studio版本的不同,会有不同的区别,如AS 2021.3.1版本,生产的class在如下路径:
build\intermediates\javac\debug\classes jary.com.myjnitest.JniTest
2、生成 .h 的文件
在 studio 打开 Terminal 命令行工具,在命令行中先进入到工程的 main 目录下,输入命令:
javah -d jni -classpath 编译后的 class 文件的绝对路径。
例如:javah -d jni -classpath ‘C:\ASworkspace\MyJniTest\app\build\intermediates\classes\debug’ jary.com.myjnitest.JniTest
在包名处不能用“ \ ” ,要用“.”的形式,且和前面要有一个空格。编译后的class文件一定要在包的目录结构内,即\jary\com\myjnitest\,否则会有找不类的错误。
命令中参数jni可以不用,-classpath后的地址用单引号号括起来,如果路径中有空格就用双引号。
命令执行后会在 main 目录下(在哪个目录下执行就会在哪个目录下生成 .h 文件)自动生成 “jni” 文件夹(如果使用了参数-jni),同时生成一个 .h 的文件。在jni文件夹下编写C文件,#include 上面生成的 .h文件,按照生成的 .h文件里的方法名,在c文件中实现。
3、到module的build.gradle中进行配置

在android节点下:
externalNativeBuild {
    cmake {
        path 'CMakeLists.txt'
    }
}
在defaultConfig节点 添加如下代码
ndk{
    abiFilters "armeabi" //表示生成哪些平台的so
}

然后创建 CMake 构建脚本,在应用模块下 new 一个 file 文件,命名为 CMakeLists.txt 即可。
内容可直接复制新建项目中生成的,注意将下图 1,3 点改为你自己定义的 moduleName , 第 2 点改为你刚刚创建源文件的路径。
在这里插入图片描述
配置完成后就可以编译了,编译生成的so库在在build下intermediates\cmake里。

动态注册native方法

上面介绍了静态注册native方法的过程,就是Java层声明的nativ方法和JNI函数一一对应。刚开始做JNI的前期,可能会遵守静态注册的流程:
1、编写带有native方法的Java类
2、使用Javah命令生成.h头文件
3、编写代码实现头文件中的方法
这样的单调的标准流程,而且还要忍受这么"长"的函数名。那有没有更简单的方式呢?比如让Java层的native方法和任意JNI函数连接起来?
答案是有的——动态注册,也就是通过RegisterNatives方法把C/C++中的方法映射到Java中的native方法,而无需遵循特定的方法命名格式。

在C中访问java方法

方法一(使用反射的方式,在C代码中调用Java方法)
//jclass (FindClass)(JNIEnv, const char*); 拿到类的字节码
//第二个参数是类的包名
jclass clazz = (env)->FindClass(env, “com/itheima/ccalljava/MainActivity”);
//jmethodID (GetMethodID)(JNIEnv, jclass, const char
, const char*); 拿到方法
//第三个参数是要调用的java方法的名字,第四个参数是被调用的java方法的签名(用javap命令来查询)
//参数四:括号里面的是show方法的参数的签名,外面是V是返回值的签名-----即方法参数是String类型,返回值类型是viod
jmethodID methodID = (*env)->GetMethodID(env, clazz, “show”, “(Ljava/lang/String;)V”);
//void (CallVoidMethod)(JNIEnv, jobject, jmethodID, …); 调用方法
//jmethodID后面就是调用方法的参数,因为C与java的字符串不同,
//故用 (env)->NewStringUTF(env, char cstr)将C的字符串转为jstring
(*env)->CallVoidMethod(env, obj, methodID, (*env)->NewStringUTF(env, “是时候再”));
方法二(与一不同的是:获取jclass的方式不同)

 //jclass
    jclass cls = (*env)->GetObjectClass(env, obj);
   //jmethodID
    jmethodID mid = (*env)->GetMethodID(env, cls, "genRandomInt", "(I)I"); 
    //调用方法
    //规则:Call<Type>Method  Type是返回值类型    如:CallObjectMethod返回的是Object,CallVoidMethod没有返回值
    jint random = (*env)->CallIntMethod(env, obj, mid, 200);

被访问的java方法如果有多个参数,如:
public void show(int j , String str ,int k)
在jni里
// I:int类类型签名,Ljava/lang/String;是String的签名
jmethodID methodID = (*env)->GetMethodID(env, clazz, “show”, “(ILjava/lang/String;I)V”);
访问静态方法

JNIEXPORT void JNICALL Java_com_tz_jni_TestNative_accessStaticMethod(JNIEnv * env, jobject cls){
    //如果native方法为static,jobject为子类jclass的实例,也就是native方法所属的类的Class实例,即cls就是jclass
    //故如果是调用该类里面的方法,获取jclass就可以不用写了
    //jclass
    //jclass cls = (*env)->GetObjectClass(env, obj);
    //jmethodID    
    jmethodID mid = (*env)->GetStaticMethodID(env, cls, "getUUID", "()Ljava/lang/String;");    
     //调用
    //规则:CallStatic<Type>Method
    jstring uuid = (*env)->CallStaticObjectMethod(env, cls, mid);
    //jstring转为C字符串  
    char *uuid_str = (*env)->GetStringUTFChars(env, uuid, NULL);
  }

访问构造方法

// jclass    Date
    jclass cls = (*env)->FindClass(env, "java/util/Date");
    //构造方法jmethodID,第3个参数<init>表示是构造方法
    jmethodID contructor_mid = (*env)->GetMethodID(env, cls, "<init>","()V");
    //实例化一个Date对象
    jobject date_obj = (*env)->NewObject(env, cls, contructor_mid);
    //调用Date里的getTime方法
    jmethodID mid = (*env)->GetMethodID(env, cls, "getTime", "()L");
    jlong time = (*env)->CallLongMethod(env, date_obj, mid);

调用父类的方法
在java文件中有一个属性 Hunman hunman = new Man( );//Hunman是Man的父类
sayHi( )是Hunman的一个方法,Man覆写了sayHi()方法。

JNIEXPORT void JNICALL Java_com_tz_jni_TestNative_callNonvirtualMethod(JNIEnv * env, jobject obj){
    //获取一个Hunman对象
    jclass cls = (*env)->GetObjectClass(env, obj);
    jfieldID fid = (*env)->GetFieldID(env, cls, "human", "Lcom/tz/jni/Hunman ;");
    jobject human_obj = (*env)->GetObjectField(env, obj, fid); //取得obj中Hunman 的对象

    //执行sayHi方法
    jclass human_cls = (*env)->FindClass(env, "com/tz/jni/Human");//注意:传父类的类名
    jmethodID mid = (*env)->GetMethodID(env, human_cls, "sayHi", "()Ljava/lang/String;");

    //执行子类覆盖的方法
    //jstring jstr = (*env)->CallObjectMethod(env, human_obj, mid);
    //执行父类的方法
    jstring jstr = (*env)->CallNonvirtualObjectMethod(env, human_obj, human_cls, mid);
    //jstring->char*
    char * str = (*env)->GetStringUTFChars(env, jstr, NULL);
}

注意
1、方法参数或者返回值为java中的对象时,签名中必须以“L”加上其路径,不过此路径必须以“/”分开,自定义的对象也使用本规则
比如说 java.lang.String为“java/lang/String”,com.nedu.jni.helloword.Student为"Lcom /nedu/jni/helloword/Student;"
2、方法参数或者返回值为数组类型时,请前加上[
例如[I表示 int[],[[[D表示 double[][][],即几维数组就加几个[
在这里插入图片描述

在C代码中调用Java属性

例子:在C中调用java属性,修改该属性并返回修改后的值

//得到class
jclass cls = (*env)->GetObjectClass(env, obj); 
    //jfieldID    取得属性的id
    //属性   第三个参数是要调用的java中属性的名称,
    //第四个参数是属性的签名,如果是对象属性,则需要L加上类型路径
jfieldID fid = (*env)->GetFieldID(env, cls, "key", "Ljava/lang/String;");    
//获取key属性的值,这例子里key在java中是字符串String类型
//注意:获取数据的规则如下(*env)->Get<Type>Field();
//如果key是int类型的这样获取:(*env)->GetIntField(); 
//key在java中是对象属性则是下面的写法
jstring jstr = (*env)->GetObjectField(env, obj, fid);
//jstring转为 C/C++字符串
char *str = (*env)->GetStringUTFChars(env, jstr, NULL);
//拼接字符串
char text[50] = "super ";
strcat(text,str);
//拼接完成之后,从C字符串转为jstring
jstr = (*env)->NewStringUTF(env, text);
//修改key的属性
//注意规则:Set<Type>Field
(*env)->SetObjectField(env, obj, fid, jstr);
//修改key的属性
return jstr;

访问静态属性

//jclass
  jclass cls = (*env)->GetObjectClass(env, obj);
 //jfieldID
  jfieldID fid = (*env)->GetStaticFieldID(env, cls, "count", "I");
//获取静态属性的值
 //规则:GetStatic<Type>Field
  jint count = (*env)->GetStaticIntField(env, cls, fid);
  count += 10;
 //修改属性的值
 //规则:SetStatic<Type>Field
  (*env)->SetStaticIntField(env, cls, fid, count);

数组处理

//比较器
int compare(jint *a, jint * b){
    return (*b) - (*a);
}
//传入int数组,对数组进行排序
JNIEXPORT void JNICALL Java_com_tz_jni_TestNative_giveArray(JNIEnv * env, jobject obj, jintArray arr){
    //基本数据类型,传值
    //引用类型,传引用
    //arr,是指向Java数组的指针

    //Java的int数组(jintArray)->C int数组
    jint *elems = (*env)->GetIntArrayElements(env, arr, NULL);
    //数组的长度
    int len = (*env)->GetArrayLength(env, arr);
    //对(jint)long数组进行排序。qsort( )方法是C函数库里的排序函数
    qsort(elems, len, sizeof(jint), compare);

    //同步,将排好序的C数组同步到java数组
    //释放数组的元素
    //第四个参数:mode参数
    //mode = 0,Java数组进行更新,并且释放C/C++数组
    //mode = JNI_ABORT,Java数组不进行更新,但是释放C/C++数组
    //mode = JNI_COMMIT,Java数组进行更新,不释放C/C++数组(函数执行完,数组还是会释放)    
    (*env)->ReleaseIntArrayElements(env, arr, elems, JNI_COMMIT);
}

//返回指定大小的数组
JNIEXPORT jintArray JNICALL Java_com_tz_jni_TestNative_getArray(JNIEnv * env, jobject obj,jint len){
    jintArray jint_arr = (*env)->NewIntArray(env, len);
    //赋值
    //jintArray -> jint *    (将jintArray转成C int数组)
    jint * elems = (*env)->GetIntArrayElements(env, jint_arr, NULL);
    int i = 0;
    //循环赋值
    for (; i < len; i++){
        elems[i] = i;
    }
    //同步   把C数组同步到JNI数组
    (*env)->ReleaseIntArrayElements(env, jint_arr, elems, 0);

    return jint_arr;
}

中文字符串乱码问题

JNIEXPORT jstring JNICALL Java_com_tz_jni_TestNative_chineseChars(JNIEnv * env, jobject obj,jstring jstr){
    //jstring转C字符串     
    //char * str = (*env)->GetStringUTFChars(env, jstr, NULL);
    
    char *cstr = "我说中文";
    //C字符串->jstring
    //如果直接 return (*env)->NewStringUTF(env, cstr); 在java中拿到返回值是乱码
    //   因为NewStringUTF中UTF是UTF16编码

    //原理:调用Java的转码API,返回GB2312中文编码字符串
    //使用这个构造方法,完成转码
    //public String(byte bytes[], String charsetName),使用String类的构造方法并指定编码格式
    //执行这个构造方法需要三个东西
    //1.jmethodID
    //2.byte数组(jbyteArray)参数
    //3.charsetName参数(jstring)

    //String类的jclass
    jclass str_cls = (*env)->FindClass(env, "java/lang/String");
    //取得构造方法id
    jmethodID constructor_mid = (*env)->GetMethodID(env, str_cls, "<init>", "([BLjava/lang/String;)V");
    //char * -> char[] ->jbyteArray -> jbyte * ,new一个jbyteArray
    jbyteArray bytes = (*env)->NewByteArray(env, strlen(cstr));
    //bytes数组赋值,把C字符串复制到上面new出来的jbyteArray
    (*env)->SetByteArrayRegion(env, bytes, 0, strlen(cstr), cstr);
    //charsetName="GB2312",把"GB2312"转成jstring
    jstring charsetName = (*env)->NewStringUTF(env, "GB2312");
    //返回GB2312中文编码jstring
    return (*env)->NewObject(env, str_cls, constructor_mid, bytes, charsetName);    
}

最后,贴出Java数据类型的签名对照表:
在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值