JNI
,全称 Java NativeInterface
,是一种为Java编写本地方法和JVM嵌入本地应用程序标准的应用程序接口,它允许运行在JVM上的Java代码能够与C/C++实现的本地库进行交互。
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函数来访问字符串的内容。
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。
boolean ->Z,byte-> B,char -> C,short-> S,int->I,long->J,float-> F,double->D,void -> V; int[]->[I,boolean[][]->[[Z,java.lang.Class[] -> [Ljava/lang/Class;
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 复制代码
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" } } } 复制代码
/** * 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*env
即 JNINativeInterface_**env
,因此,我们在调用 JNINativeInterface_
结构体中的成员时需要使用一级指针来实现,即(*env)->成员。然后,再继续查看 JNINativeInterface_
源码, NewStringUTF
函数需要传入一个 JNIEnv
结构体类型指针变量,该指针变量指向 JNINativeInterface_
结构体存储的地址,因此,还需要将变量env赋值给 NewStringUTF
即可。
structJNINativeInterface_ { jstring (JNICALL *NewStringUTF) (JNIEnv*env, const char *utf); } 复制代码
首先获取构造方法,再通过构造方法获取类对象,根据类对象调用实例方法;构造方法通过进行标识,传递参数为空,返回值也为空。
//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); } } 复制代码
在.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
对象。
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); } 复制代码
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); } 复制代码