专栏首页Android 进阶JNI 数据类型及Java与C++之间互调

JNI 数据类型及Java与C++之间互调

JNI

什么是JNI

JNI,全称Java NativeInterface,是一种为Java编写本地方法和JVM嵌入本地应用程序标准的应用程序接口。

它允许运行在JVM上的Java代码能够与C/C++实现的本地库进行交互。

JNI 数据类型

Java中有两种类型:基本数据类型(int、float、char等)和引用类型(类、对象、数组等)。 JNI定义了一个C/C++类型的集合,集合中每一个类型对应于Java中的每一个类型,其中,对于基本类型而言,JNI与Java之间的映射是一对一的,比如Java中的int类型直接对应于C/C++中的jint;而对引用类型的处理却是不同的,JNI把Java中的对象当作一个C指针传递到本地函数中,这个指针指向JVM中的内部数据结构,而内部数据结构在内存中的存储方式是不可见的,本地代码必须通过在JNIEnv中选择适当的JNI函数来操作JVM中的对象。比如,对于java.lang.String对应的JNI类型是jstring,但本地代码只能通过GetStringUTFChars这样的JNI函数来访问字符串的内容。

JNI映射表

Java 类型

JNI本地类型

方法签名

描述

boolean

jboolean

Z

unsigned 8 bits

byte

jbyte

B

signed 8 bits

char

jchar

C

unsigned 16 bits

short

jshort

S

signed 16 bits

int

jint

I

signed 32 bits

long

jlong

J

signed 64 bits

float

jfloat

F

32bits

double

jdouble

D

64bits

void

Void

V

-

方法签名

由于Java支持方法重载,在JNI访问Java层方法时仅靠函数名是无法唯一确定一个方法,因此JNI提供了一套签名规则(如:Z、B、[Z等),用一个字符串来唯一确定一个方法,其规则:(参数1类型签名参数2类型签名…)返回值类型签名,比如Java方法long getDeviceId(int n, String s, int[] arr)、long getDeviceId(int n)的类型签名分别为(ILjava/lang/String;[I)J、(I)J。

  1. 基本类型: boolean ->Z,byte-> B,char -> C,short-> S,int->I,long->J,float-> F,double->D,void -> V;
  2. 如果是类的类型:L+类全名,类名中的.用/代替,比如java.lang.String就是Ljava/lang/String;
  3. 如果是数组类型:则在前面加上 然后加类型签名,几位数组就加几个,比如int[]->[I,boolean[][]->[[Z,java.lang.Class[] -> [Ljava/lang/Class; 可以通过javap -s来打印该方法的签名 Example: public static native String getName(); public static native int calculate(int a,int b); 通过Rebuild Project才会在app中的intermediates目录下javac/debug 生成class文件,找到 类到地址 然后右键打开命令行

在命令行输入 javap -s JNIUtils.class 即可获取到2个方法到签名,我这里是JNIUtils获取的两个签名如下: 警告: 二进制文件JNIUtils包含com.example.nativejnidemo.JNIUtils Compiled from "JNIUtils.java" public class com.example.nativejnidemo.JNIUtils { public com.example.nativejnidemo.JNIUtils(); descriptor: ()V public static native java.lang.String getName(); //括号为空表示当前没有参数 descriptor: ()Ljava/lang/String; //括号中有两个参数则表示为该类型的签名I I在返回该方法签名类型I public static native int calculate(int, int); descriptor: (II)I

Android studio 3.0 打印日志

  • 首先在app下的build.gradle中添加ldLibs("log") // 指定ABI ndk { ldLibs("log") abiFilters 'x86', 'x86_64',  'armeabi-v7a',         'arm64-v8a' } 然后在cpp文件中添加 #include <android/log.h> #define  LOGI(...) __android_log_print(ANDROID_LOG_INFO, "========= Info =========   ", __VA_ARGS__) #define  LOGE(...)  __android_log_print(ANDROID_LOG_ERROR, "========= Error =========   ", __VA_ARGS__) #define  LOGD(...)  __android_log_print(ANDROID_LOG_INFO, "========= Debug =========   ", __VA_ARGS__) #define  LOGW(...)  __android_log_print(ANDROID_LOG_WARN, "========= Warn =========   ", __VA_ARGS__) 最后调用 LOGI("from c log");
  • 还有一种通过Cmake构建的ndk工程, 导入log库 在build.gradle中加入ldLibs "log": android { defaultConfig {     ndk{         ldLibs "log"     } } }

JNI 函数解析

Java调用C/C++ 本地函数

/**
 * CPP 源文件,返回一个字符串
 * @param env
 * @return
 */
Java_com_example_jnilearndemo_MainActivity_stringFromJNI(
     JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}
/**
 * C 源文件,返回一个字符串
 * @param env
 * @return
 */
Java_com_example_jnilearndemo_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return (*env)->NewStringUTF(env,hello.c_str());
}

当源文件为.cpp时,只需要传env ->就可以;而当源文件为.c时,就需要传入(*env)->。 上面两个函数作用都是当Java层调用本地方法时向Java层返回一个UTF-8格式的字符串。两个函数使用方法不同原因:主要是因为这两个函数是在不同的源文件中实现的。通过查看jni.h源码可知,当源文件为.cpp时,JNIEnv实际为结构体JNIEnv_,然后我们再查看JNIEnv_结构体的内容,找到NewStringUTF(constchar *utf)函数,它实际执行了functions->NewStringUTF(this,utf)函数,而这个函数默认传递了一个this参数,该this参数则指的是getStringFromC函数原型中JNIEnv指针变量参数。因此,使用C++开发JNI时就无需再传递JNIEnv指针变量且在使用JNIEnv_结构体的成员时,直接使用结构体变量指向成员即可。

#ifdef__cplusplus
// 如果为C++,JNIEnv表示JNIEnv_
typedef JNIEnv_  JNIEnv;
#else
// 如果不为C++,JNIEnv表示JNINativeInterface_*
typedef const struct JNINativeInterface_ * JNIEnv;
#endif
structJNIEnv_ {
    const struct JNINativeInterface_*functions;
#ifdef__cplusplus
……
jstringNewStringUTF(const char *utf) {
        returnfunctions->NewStringUTF(this,utf);
}
……
#endif

当源文件为.c时,JNIEnv实际表示的JNINativeInterface_*JNIEnv*envJNINativeInterface_**env,因此,我们在调用JNINativeInterface_结构体中的成员时需要使用一级指针来实现,即(*env)->成员。然后,再继续查看JNINativeInterface_源码,NewStringUTF函数需要传入一个JNIEnv结构体类型指针变量,该指针变量指向JNINativeInterface_结构体存储的地址,因此,还需要将变量env赋值给NewStringUTF即可。

structJNINativeInterface_ {
       jstring (JNICALL *NewStringUTF) (JNIEnv*env, const char *utf);
}

C/C++ 访问Java层属性及方法

C/C++层访问Java层对象的实例变量与实例方法

首先获取构造方法,再通过构造方法获取类对象,根据类对象调用实例方法;构造方法通过进行标识,传递参数为空,返回值也为空。

//C++调用java的实例方法与实例变量
extern "C"
JNIEXPORT void JNICALL
Java_com_example_jnilearndemo_JNIUtils_callInstanceMethod(JNIEnv *env, jobject instance, jint i) {
   jclass cls_jniutils = env->FindClass("com/example/jnilearndemo/JNIUtils");
   if(cls_jniutils==NULL){
       return;
   }
   jmethodID method_instance = env->GetMethodID(cls_jniutils,"method","(Ljava/lang/String;)V");
   if(method_instance==NULL){
       return;
   }
   //首先获取构造方法,再通过构造方法获取类对象,根据类对象调用实例方法;构造方法通过<init>进行标识,传递参数为空,返回值也为空
   jmethodID method_construct = env->GetMethodID(cls_jniutils,"<init>","()V");
   if(method_construct==NULL){
       return;
   }
   //创建相应的对象,最后参数为空,不需要传递参数
   jobject jnutils = env->NewObject(cls_jniutils,method_construct,NULL);
   if(jnutils==NULL){
       return;
   }
   jstring msg = env->NewStringUTF("call instance method")
   //调用Java中的实例变量
   jfieldID filed_instance = env->GetFieldID(cls_jniutils,"address","Ljava/lang/String;");
   if(filed_instance==NULL){
       return;
   }
   jstring address = env->NewStringUTF("suzhou");
   //设置实例变量,需要传递对象
   env -> SetObjectField(jnutils,filed_instance,address);
   env -> CallVoidMethod(jnutils,method_instance,msg);
 
   env->DeleteLocalRef(msg);
   env->DeleteLocalRef(cls_jniutils);
   env->DeleteLocalRef(jnutils);
   env->DeleteLocalRef(address);
}
public class JNIUtils {
    static {
        //加载动态库
        System.loadLibrary("JNILearning");
    }
    public static String name = "ztz";
    public String address = "hangzhou";
    public static native int calculate(int a,int b);
    public static native void callInstance(int i);
    public native void callInstanceMethod(int i);
    public static void LogMessage(String msg){
        Log.i("C++调用java中的static方法", "LogMessage: "+msg);
    }
    public static void staticMethod(String msg){
        LogMessage(msg);
        LogMessage(name);
    }
 
    public void method(String msg){
        LogMessage(msg);
        LogMessage(address);
    }
}

C/C++层访问Java类的静态属性

在.cpp格式的源码文件中: Java 代码

public class JNIUtils {
    static {
        //加载动态库
        System.loadLibrary("JNILearning");
    }
    public static String name = "ztz";
    public static native int calculate(int a,int b);
    public static native void callInstance(int i);
    public static void LogMessage(String msg){
        Log.i("C++调用java中的static方法", "LogMessage: "+msg);
    }
    public static void staticMethod(String msg){
        LogMessage(msg);
        LogMessage(name);
    }
}

JNI C++层代码

extern "C"
JNIEXPORT void JNICALL
Java_com_example_jnilearndemo_JNIUtils_callInstance(JNIEnv *env, jclass type, jint i) {
 
    // TODO 当源文件为.cpp时,只需要传env ->就可以;而当源文件为.c时,就需要传入(*env)->
    //查找类
    jclass cls_jniutils = env -> FindClass("com/example/jnilearndemo/JNIUtils");
    //判断是否找到,没找到返回
    if(cls_jniutils==NULL){
        return;
    }
    //修改java中的静态变量
    jfieldID field_name = env->GetStaticFieldID(cls_jniutils,"name","Ljava/lang/String;");
//这里要进行空安全检查,JNI与Java处理异常机制不一样,Java遇到异常如果没有捕获,程序就立即停止运行,而JNI遇到异常,程序会继续执行下去,
这样针对后面的操作非常危险,所以要return跳过后面代码执行。
    if(field_name==NULL){
        return;
    }
    jstring new_name = env->NewStringUTF("yif");
//对成员变量进行重新设置
    env->SetStaticObjectField(cls_jniutils,field_name, new_name);
    //调用之前methodId所对应的的静态方法
    env ->CallStaticVoidMethod(cls_jniutils,method_static,data);
    //最后释放之前创建的对象,这里为局部引用
    env ->DeleteLocalRef(cls_jniutils);
    env->DeleteLocalRef(new_name);
}

NewStringUTF函数: 通过调用NewStringUTF函数,会构建一个新的java.lang.String字符串对象。这个新创建的字符串会自动转换成Java支持的Unicode编码。如果JVM不能为构造java.lang.String分配足够的内存,NewStringUTF会抛出一个OutOfMemoryError异常,并返回NULL。在这个例子中我们不必检查它的返回值,如果NewStringUTF创建java.lang.String失败,OutOfMemoryError这个异常会被在调用JNI层方法的Java类方法中抛出,比如这里的JNIUtils类。如果NewStringUTF创建java.lang.String成功,则返回一个JNI引用,这个引用指向新创建的java.lang.String对象。

C/C++层访问Java类的静态方法

  1. 在.c格式的源文件中的操作: Java代码 public class JNIUtils { static {     System.loadLibrary("HelloJNI"); }   public static void LogMessage(String msg){     Log.i("C++调用java中的static方法", "LogMessage: "+msg); } public static void staticMethod(String msg){     LogMessage(msg); } public static native void callInstance(int i); } C++层代码 JNIEXPORT void JNICALL Java_com_example_nativejnidemo_JNIUtils_callInstance__I(JNIEnv *env, jclass type, jint i) {   // TODO 当源文件为.cpp时,只需要传env ->就可以;而当源文件为.c时,就需要传入(*env)-> //查找类 jclass cls_hello = (*env)->FindClass(env,"com/example/jnilearndemo/JNIUtils"); //判断是否找到,没找到返回 if(cls_hello==NULL){     return; } //然后在查找该类下的方法,参数env,查找的类,查找的方法,方法对应的签名 jmethodID method_static = (*env)->GetStaticMethodID(env,cls_hello,"staticMethod","(Ljava/lang/String;)V"); if(method_static==NULL){     return; } //传递参数为String,所以要创建String对象 jstring data = (*env) -> NewStringUTF(env,"call static method"); if(data==NULL){     return; } //调用之前methodId所对应的的静态方法 (*env) ->CallStaticVoidMethod(env,cls_hello,method_static,data); //最后释放之前创建的对象,这里为局部引用 (*env)->DeleteLocalRef(env,cls_hello); (*env)->DeleteLocalRef(env,data); }
  2. 在.cpp格式的源文件中的操作: extern "C" JNIEXPORT void JNICALL Java_com_example_jnilearndemo_JNIUtils_callInstance(JNIEnv *env, jclass type, jint i) {   // TODO 当源文件为.cpp时,只需要传env ->就可以;而当源文件为.c时,就需要传入(*env)-> //查找类 jclass cls_jniutils = env -> FindClass("com/example/jnilearndemo/JNIUtils"); //判断是否找到,没找到返回 if(cls_jniutils==NULL){     return; } //然后在查找该类下的方法,参数查找的类,查找的方法,方法对应的签名 jmethodID method_static = env ->GetStaticMethodID(cls_jniutils,"staticMethod","(Ljava/lang/String;)V"); if(method_static==NULL){     return; } //传递参数为String,所以要创建String对象 jstring data = env->NewStringUTF("call static method"); if(data==NULL){     return; } //调用之前methodId所对应的的静态方法 env ->CallStaticVoidMethod(cls_jniutils,method_static,data); //最后释放之前创建的对象,这里为局部引用 env ->DeleteLocalRef(cls_jniutils); env->DeleteLocalRef(data); }

Java 与 C++ 的区别

再说一下Java与C++比较

  1. Java 是纯粹的面向对象语言,所有的对象都继承自 java.lang.Object,C++ 为了兼容 C 即支持面向对象也支持面向过程。
  2. Java 通过虚拟机从而实现跨平台特性,但是 C++ 依赖于特定的平台。
  3. Java 没有指针,它的引用可以理解为安全指针,而 C++ 具有和 C 一样的指针。
  4. Java 支持自动垃圾回收,而 C++ 需要手动回收。
  5. Java 不支持多重继承,只能通过实现多个接口来达到相同目的,而 C++ 支持多重继承。
  6. Java 不支持操作符重载,虽然可以对两个 String 对象执行加法运算,但是这是语言内置支持的操作,不属于操作符重载,而 C++ 可以。
  7. Java 的 goto 是保留字,但是不可用,C++ 可以使用 goto。
  8. Java 不支持条件编译,C++ 通过 #ifdef #ifndef 等预处理命令从而实现条件编译。

源码

注释的已经非常详细了,如果还有不清楚的,可以查看源码。 最后附上源码链接:https://github.com/ztz12/JNIDemo

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Kotlin 轻量级Android开发

    它由Jetbrains创建,而Jetbrains则是诸多强大的工具(如知名的Java IDE IntelliJ IDEA)背后的公司。Kotlin是一门非常简单...

    Yif
  • Kotlin 协程之Practice

    Kotlin 练习参考https://www.kotlincn.net/docs/reference/

    Yif
  • Retrofit 解析

    Retrofit在生成Retrofit对象和ServiceMethod对象时候都用到了Builder模式。通过Builder来生成类的实例对象更加优雅,尤其在如...

    Yif
  • Kotlin 轻量级Android开发

    它由Jetbrains创建,而Jetbrains则是诸多强大的工具(如知名的Java IDE IntelliJ IDEA)背后的公司。Kotlin是一门非常简单...

    Yif
  • 大数据入门基础系列之浅谈Hive的执行原理

    在前面的博文里,我已经介绍了 在前面的博文里,我已经介绍了 Hive的执行原理 ? Hive的执行原理 Hive构建在Hadoop之上 (1) HQL中对查询语...

    企鹅号小编
  • 如何在 Flink 1.9 中使用 Hive?

    阿里巴巴技术专家,Apache Hive PMC成员,加入阿里巴巴之前曾就职于Intel、IBM等公司,主要参与Hive、HDFS、Spark等开源项目。

    用户6259908
  • 一分钟看完 Hive 体系结构

    ①CLI(command line interface):CLI启动的时候会同时启动一个Hive副本;

    Lenis
  • 「大数据系列」:Apache Hive 分布式数据仓库项目介绍

    Apache Hive™数据仓库软件有助于读取,编写和管理驻留在分布式存储中的大型数据集并使用SQL语法进行查询

    首席架构师智库
  • ffmpeg Operation not permitted 报错的解决过程记录

    由于视频的录制过程出现了一些小问题,需要重新将视频文件切割和合并,找了几个视频编辑软件来做这个事情,最终的结果都不是特别满意,当时已经挺晚的了,本来打算上床睡觉...

    我是十三
  • Spark性能调优01-资源调优

    在开发完Spark作业之后,就该为作业配置合适的资源了。 Spark的资源参数,基本都可以在spark-submit命令中作为参数设置。

    CoderJed

扫码关注云+社区

领取腾讯云代金券

玩转腾讯云 有奖征文活动