From:较详细的介绍JNI:https://blog.csdn.net/lizhifa2011/article/details/21021177
From:https://www.jb51.net/article/126111.htm
NDK 官方文档:https://developer.android.google.cn/training/articles/perf-jni
JNI / NDK 开发指南:https://wiki.jikexueyuan.com/project/jni-ndk-developer-guide/overview.html
Java Native Interface Specification—Contents:https://docs.oracle.com/javase/1.5.0/docs/guide/jni/spec/jniTOC.html
菜鸟教程 之 JNI 入门教程:https://www.runoob.com/w3cnote/jni-getting-started-tutorials.html
Android JNI学习(1、2、3、4、5 ):https://www.jianshu.com/p/b4431ac22ec2
JNI官方中文资料:https://blog.csdn.net/yishifu/article/details/52180448
JNI 入门教程( 菜鸟教程 ):https://www.runoob.com/w3cnote/jni-getting-started-tutorials.html
JNI 实战全面解析:https://blog.csdn.net/yuzhou_zang/article/details/78410632
Java 与 c++ 通过 JNI 的完美结合:https://blog.csdn.net/xiaoxiaoyusheng2012/article/details/56672173
使用 jni 调用 C++ 的过程:https://www.cnblogs.com/mssyj/p/12148739.html
VS2019 C++的跨平台开发——Android .so 开发:https://blog.csdn.net/luoyu510183/article/details/94590497
Android Studio 开发 JNI 示例:https://blog.csdn.net/wzhseu/article/details/79683045
JNI开发总结:https://cloud.tencent.com/developer/article/1356493
Android JNI原理分析:http://gityuan.com/2016/05/28/android-jni/
较详细的 JNI 简介
JNI 是本地语言编程接口。它允许运行在 JVM 中的 Java 代码和用C、C++ 或 汇编 写的本地代码相互操作。
在 Java中,有时候我们不得不要去使用其他语言的代码,比如说:
- 1、你的应用需要访问系统的各个特性和设备,这些特性和设备通过java平台是无法访问的。
- 2、你已经有了大量的测试过和调试过的用另一种语言编写的代码,并且知道如何将其导出到所有的目标平台上。
- 3、通过基础测试,你已经发现所编写的 Java 代码比用其他语言编写的等价代码要慢得多。
Java 平台有一个用于和本地C代码进行互操作的 API,称为 Java本地接口(JNI)。
JNI 有什么用?
JNI 是 Java Native Interface 的缩写,它提供了若干的 API 实现了Java 和 其他语言的通信(主要是 C/C++)。通俗来说,就是 JAVA 调用 C/C++ 函数的接口。如果你要想调用 C系列的函数,你就必须遵守这样的约定。
JNI 最常见的两个应用:从 Java 程序调用 C/C++,以及从 C/C++ 程序调用Java代码。JNI 是一个双向的接口:开发者不仅可以通过 JNI 在 Java 代码中访问 Native 模块,还可以在 Native 代码中嵌入一个 JVM,并通过 JNI 访问运行于其中的 Java 模块。可见 JNI 担任了一个桥梁的角色,它将 JVM 与 Native 模块联系起来,从而实现了 Java 代码与 Native 代码的互访
1、JNI 组织结构
JNI 函数表的组成就像 C++的虚函数表,虚拟机可以运行多张函数表。
JNI 接口指针仅在当前线程中起作用,指针不能从一个线程进入另一个线程,但可以在不同的线程中调用本地方法。
2、原始数据
Jobject 对象 和 引用类型
1、基本数据类型
以下是 Java的基本数据类型 和 jni中的基本数据类型 的 比较,及各类型所占的字节。
图表:
Java类型 | 本地类型(JNI) | 描述 |
boolean(布尔型) | jboolean | 无符号8个比特 |
byte(字节型) | jbyte | 有符号8个比特 |
char(字符型) | jchar | 无符号16个比特 |
short(短整型) | jshort | 有符号16个比特 |
int(整型) | jint | 有符号32个比特 |
long(长整型) | jlong | 有符号64个比特 |
float(浮点型) | jfloat | 32个比特 |
double(双精度浮点型) | jdouble | 64个比特 |
void(空型) | void | N/A |
2、引用类型
Java不同的引用类型 在 JNI当中也有对应的引用类型,如下树形表示结果:
当在 C 语言中使用时,所有的 JNI 引用类型都被定义为 jobject 类型。typedef jobject jclass;
jvalue 类型
jvalue 类型是一个 基本数据类型 和 引用类型 的集合,定义方式如下:
typedef union jvalue {jboolean z;jbyte b;jchar c;jshort s;jint i;jlong j;jfloat f;jdouble d;jobject l;} jvalue;
class 的说明
- (1):类和接口的描述符在 java 当中使用 ".",如:java.lang.String。而在 JNI 当中是用 "/",如:java/lang/String
- (2):数组类型的引用类型用 "[" 表示。如 int[] ( java中的表示法 ) [I ( [ 大写的 i 是 JNI 中的表示法,[ 的个数表示数组的维数 二维则是 [[ I )
- (3):域的说明,和 java 比较如下表: 注意:引用类型的域 用L开头,并且以”;”作为结尾。数组类型和class说明的一样。
-
(4):Method 说明。JNI 中的方法的声明规则:先写参数列表,再写返回类型,以下是例子。
域描述符
域 | Java 语言 |
Z | boolean |
B | byte |
C | char |
S | short |
I | int |
J | long |
F | float |
D | double |
引用类型则为 L + 该类型类描述符 + 。
数组,其为 : [ + 其类型的域描述符 + 。
int[ ] 描述符为 [I
float[ ] 描述符为 [F String[ ] 描述符为 [Ljava/lang/String;
String 描述符为 Ljava/lang/String; Object[ ] 描述符为 [Ljava/lang/Object;
int [ ][ ] 描述符为 [[I
float[ ][ ] 描述符为 [[F
将参数类型的域描述符按照申明顺序放入一对括号中后跟返回值类型的域描述符,规则如下: (参数的域描述符的叠加)返回类型描述符。对于,没有返回值的,用V(表示void型)表示。
举例如下:( 函数签名 就是 " 参数 + 返回值 " )
Java层方法 JNI函数签名 String test ( ) Ljava/lang/String; int f (int i, Object object) (ILjava/lang/Object;)I void set (byte[ ] bytes) ([B)V
JavaVM 与 JNIEnv 这两个概念的区分:
- JavaVM 是虚拟机在 JNI 层的代表,一个进程只有一个 JavaVM,所有的线程共用一个 JavaVM。JavaVM 是一个全局变量,一个进程只有一个 JavaVM 对象。
- JNIEnv 是一个线程相关的结构体,该结构体代表了 Java 在本线程的运行环境 。JNIEnv 是一个线程拥有一个,不同线程的 JNIEnv 彼此独立。
JNIEnv 作用 :
----- 调用 Java 函数: JNIEnv 代表 Java 运行环境,可以使用 JNIEnv 调用 Java 中的代码。
----- 操作 Java 对象:Java 对象传入 JNI 层就是 Jobject 对象, 需要使用 JNIEnv 来操作这个 Java 对象。
JNIEnv 体系结构
- JNIEnv 是线程相关 : JNIEnv 是线程相关的,即在每个线程中都有一个 JNIEnv 指针,每个 JNIEnv 都是线程专有的, 其它线程不能使用本线程中的 JNIEnv, 线程 A 不能调用 线程 B 的 JNIEnv。
- JNIEnv 不能跨线程 :
--- 当前线程有效 : JNIEnv 只在当前线程有效, JNIEnv 不能在 线程之间进行传递, 在同一个线程中, 多次调用 JNI层方法, 传入的 JNIEnv 是相同的;
--- 本地方法匹配多 JNIEnv : 在 Java 层定义的本地方法, 可以在不同的线程调用, 因此 可以接受不同的 JNIEnv; - JNIEnv 结构 : 由上面的代码可以得出,,JNIEnv 是一个指针, 指向一个线程相关的结构, 线程相关结构指向 JNI 函数指针数组, 这个数组中存放了大量的 JNI 函数指针,这些指针指向了具体的 JNI 函数;
- 注意:JNIEnv 只在当前线程中有效。本地方法不能将 JNIEnv 从一个线程传递到另一个线程中。相同的 Java 线程中对本地方法多次调用时,传递给该本地方法的 JNIEnv 是相同的。但是,一个本地方法可被不同的 Java 线程所调用,因此可以接受不同的 JNIEnv。
关于 UTF-8 编码:JNI 使用改进的 UTF-8 字符串来表示不同的字符类型。Java 使用 UTF-16 编码。UTF-8 编码主要使用于 C 语言,因为它的编码用 \u000 表示为 0xc0,而不是通常的 0×00。非空 ASCII 字符改进后的字符串编码中可以用一个字节表示。
关于错误:JNI不会检查 NullPointerException、IllegalArgumentException 这样的错误,原因是:导致性能下降。在绝大多数 C 的库函数中,很难避免错误发生。JNI 允许用户使用 Java 异常处理。大部分 JNI 方法会返回错误代码但本身并不会报出异常。因此,很有必要在代码本身进行处理,将异常抛给 Java。在 JNI 内部,首先会检查调用函数返回的错误代码,之后会调用 ExpectOccurred() 返回一个错误对象。
jthrowable ExceptionOccurred(JNIEnv *env);
例如:一些操作数组的 JNI 函数不会报错,因此可以调用 ArrayIndexOutofBoundsException 或 ArrayStoreExpection 方法报告异常。
3、JNI 函数 实战
1、*.so 的入口函数
JNI_OnLoad() 与 JNI_OnUnload()
当 Android 的 VM(Virtual Machine) 执行到 System.loadLibrary() 函数时,首先会去执行 C 组件里的 JNI_OnLoad() 函数。它的用途有二:
- (1) 告诉 VM 此 C 组件使用那一个 JNI 版本。如果你的 *.so 没有提供 JNI_OnLoad() 函数,VM 会默认该 *.so 是使用最老的JNI 1.1 版本。由于新版的 JNI 做了许多扩充,如果需要使用 JNI 的新版功能,例如 JNI 1.4 的 java.nio.ByteBuffer,就必须藉由 JNI_OnLoad() 函数来告知 VM 。
- (2) 由于 VM 执行到 System.loadLibrary() 函数时,就会立即先呼叫 JNI_OnLoad(),所以 C 组件的开发者可以藉由JNI_OnLoad() 来进行 C 组件内的初期值之设定 (Initialization) 。
2、返回值
jstring str = env->newStringUTF("HelloJNI"); //直接使用该JNI构造一个jstring对象返回
return str ;
示例:
jobjectArray ret = 0;
jsize len = 5;
jstring str;
string value("hello"); ret = (jobjectArray)(env->NewObjectArray(len, env->FindClass("java/lang/String"), 0));
for(int i = 0; i < len; i++)
{ str = env->NewStringUTF(value..c_str()); env->SetObjectArrayElement(ret, i, str);
}
return ret; 返回数组
示例:
jclass m_cls = env->FindClass("com/ldq/ScanResult"); jmethodID m_mid = env->GetMethodID(m_cls,"<init>","()V"); jfieldID m_fid_1 = env->GetFieldID(m_cls,"ssid","Ljava/lang/String;"); jfieldID m_fid_2 = env->GetFieldID(m_cls,"mac","Ljava/lang/String;"); jfieldID m_fid_3 = env->GetFieldID(m_cls,"level","I"); jobject m_obj = env->NewObject(m_cls,m_mid); env->SetObjectField(m_obj,m_fid_1,env->NewStringUTF("AP1")); env->SetObjectField(m_obj,m_fid_2,env->NewStringUTF("00-11-22-33-44-55")); env->SetIntField(m_obj,m_fid_3,-50); return m_obj; 返回自定义对象
示例:
jclass list_cls = env->FindClass("Ljava/util/ArrayList;");//获得ArrayList类引用 if(listcls == NULL) { cout << "listcls is null \n" ; } jmethodID list_costruct = env->GetMethodID(list_cls , "<init>","()V"); //获得得构造函数Id jobject list_obj = env->NewObject(list_cls , list_costruct); //创建一个Arraylist集合对象 //或得Arraylist类中的 add()方法ID,其方法原型为: boolean add(Object object) ; jmethodID list_add = env->GetMethodID(list_cls,"add","(Ljava/lang/Object;)Z"); jclass stu_cls = env->FindClass("Lcom/feixun/jni/Student;");//获得Student类引用 //获得该类型的构造函数 函数名为 <init> 返回类型必须为 void 即 V jmethodID stu_costruct = env->GetMethodID(stu_cls , "<init>", "(ILjava/lang/String;)V"); for(int i = 0 ; i < 3 ; i++) { jstring str = env->NewStringUTF("Native"); //通过调用该对象的构造函数来new 一个 Student实例 jobject stu_obj = env->NewObject(stucls , stu_costruct , 10,str); //构造一个对象 env->CallBooleanMethod(list_obj , list_add , stu_obj); //执行Arraylist类实例的add方法,添加一个stu对象 } return list_obj ; 返回对象集合
3、操作 Java层 的 类
//获得jfieldID 以及 该字段的初始值 jfieldID nameFieldId ; jclass cls = env->GetObjectClass(obj); //获得Java层该对象实例的类引用,即HelloJNI类引用 nameFieldId = env->GetFieldID(cls , "name" , "Ljava/lang/String;"); //获得属性句柄 if(nameFieldId == NULL) { cout << " 没有得到name 的句柄Id \n;" ; } jstring javaNameStr = (jstring)env->GetObjectField(obj ,nameFieldId); // 获得该属性的值 const char * c_javaName = env->GetStringUTFChars(javaNameStr , NULL); //转换为 char *类型 string str_name = c_javaName ; cout << "the name from java is " << str_name << endl ; //输出显示 env->ReleaseStringUTFChars(javaNameStr , c_javaName); //释放局部引用 //构造一个jString对象 char * c_ptr_name = "I come from Native" ; jstring cName = env->NewStringUTF(c_ptr_name); //构造一个jstring对象 env->SetObjectField(obj , nameFieldId , cName); // 设置该字段的值
4、回调 Java层 方法
jstring str = NULL; jclass clz = env->FindClass("cc/androidos/jni/JniTest"); //获取clz的构造函数并生成一个对象 jmethodID ctor = env->GetMethodID(clz, "<init>", "()V"); jobject obj = env->NewObject(clz, ctor); // 如果是数组类型,则在类型前加[, 如整形数组int[] intArray, 则对应类型为[I, 即整形数组。// String[] strArray 对应为 [Ljava/lang/String; jmethodID mid = env->GetMethodID(clz, "sayHelloFromJava", "(Ljava/lang/String;II[I)I"); if (mid) { LOGI("mid is get"); jstring str1 = env->NewStringUTF("I am Native"); jint index1 = 10; jint index2 = 12; //env->CallVoidMethod(obj, mid, str1, index1, index2); // 数组类型转换 testIntArray能不能不申请内存空间 jintArray testIntArray = env->NewIntArray(10); jint *test = new jint[10]; for(int i = 0; i < 10; ++i) { *(test+i) = i + 100; } env->SetIntArrayRegion(testIntArray, 0, 10, test); jint javaIndex = env->CallIntMethod(obj, mid, str1, index1, index2, testIntArray); LOGI("javaIndex = %d", javaIndex); delete[] test; test = NULL; }
示例代码:
static void event_callback(int eventId,const char* description) { //主进程回调可以,线程中回调失败。 if (gEventHandle == NULL) return; JNIEnv *env; bool isAttached = false; if (myVm->GetEnv((void**) &env, JNI_VERSION_1_2) < 0) { //获取当前的JNIEnv if (myVm->AttachCurrentThread(&env, NULL) < 0) return; isAttached = true; } jclass cls = env->GetObjectClass(gEventHandle); //获取类对象 if (!cls) { LOGE("EventHandler: failed to get class reference"); return; } jmethodID methodID = env->GetStaticMethodID(cls, "callbackStatic", "(ILjava/lang/String;)V"); //静态方法或成员方法 if (methodID) { jstring content = env->NewStringUTF(description); env->CallVoidMethod(gEventHandle, methodID,eventId, content); env->ReleaseStringUTFChars(content,description); } else { LOGE("EventHandler: failed to get the callback method"); } if (isAttached) myVm->DetachCurrentThread();
}
线程中回调
把 c/c++ 中所有线程的创建,由 pthread_create 函数替换为由 Java 层的创建线程的函数 AndroidRuntime::createJavaThread。
static pthread_t create_thread_callback(const char* name, void (*start)(void *), void* arg)
{ return (pthread_t)AndroidRuntime::createJavaThread(name, start, arg);
} static void checkAndClearExceptionFromCallback(JNIEnv* env, const char* methodName) { //异常检测和排除 if (env->ExceptionCheck()) { LOGE("An exception was thrown by callback '%s'.", methodName); LOGE_EX(env); env->ExceptionClear(); }
} static void receive_callback(unsigned char *buf, int len) //回调
{ int i; JNIEnv* env = AndroidRuntime::getJNIEnv(); jcharArray array = env->NewCharArray(len); jchar *pArray ; if(array == NULL){ LOGE("receive_callback: NewCharArray error."); return; } pArray = (jchar*)calloc(len, sizeof(jchar)); if(pArray == NULL){ LOGE("receive_callback: calloc error."); return; } //copy buffer to jchar array for(i = 0; i < len; i++) { *(pArray + i) = *(buf + i); } //copy buffer to jcharArray env->SetCharArrayRegion(array,0,len,pArray); //invoke java callback method env->CallVoidMethod(mCallbacksObj, method_receive,array,len); //release resource env->DeleteLocalRef(array); free(pArray); pArray = NULL; checkAndClearExceptionFromCallback(env, __FUNCTION__);
} public void Receive(char buffer[],int length){ //java层函数 String msg = new String(buffer); msg = "received from jni callback" + msg; Log.d("Test", msg); }
示例代码:
jclass cls = env->GetObjectClass(obj);//获得Java类实例
jmethodID callbackID = env->GetMethodID(cls , "callback" , "(Ljava/lang/String;)V") ;//或得该回调方法句柄 if(callbackID == NULL)
{ cout << "getMethodId is failed \n" << endl ;
} jstring native_desc = env->NewStringUTF(" I am Native"); env->CallVoidMethod(obj , callbackID , native_desc); //回调该方法,并且
5、传对象到 JNI 调用
jclass stu_cls = env->GetObjectClass(obj_stu); //或得Student类引用 if(stu_cls == NULL) { cout << "GetObjectClass failed \n" ; } //下面这些函数操作,我们都见过的。O(∩_∩)O~ jfieldID ageFieldID = env->GetFieldID(stucls,"age","I"); //获得得Student类的属性id jfieldID nameFieldID = env->GetFieldID(stucls,"name","Ljava/lang/String;"); // 获得属性ID jint age = env->GetIntField(objstu , ageFieldID); //获得属性值 jstring name = (jstring)env->GetObjectField(objstu , nameFieldID);//获得属性值 const char * c_name = env->GetStringUTFChars(name ,NULL);//转换成 char * string str_name = c_name ; env->ReleaseStringUTFChars(name,c_name); //释放引用 cout << " at Native age is :" << age << " # name is " << str_name << endl ;
6、与 C++ 互转
jbytearray 转 c++byte 数组
jbyte * arrayBody = env->GetByteArrayElements(data,0);
jsize theArrayLengthJ = env->GetArrayLength(data);
BYTE * starter = (BYTE *)arrayBody;
jbyteArray 转 c++ 中的 BYTE[]
jbyte * olddata = (jbyte*)env->GetByteArrayElements(strIn, 0);
jsize oldsize = env->GetArrayLength(strIn);
BYTE* bytearr = (BYTE*)olddata;
int len = (int)oldsize;
C++ 中的 BYTE[] 转 jbyteArray
jbyte *by = (jbyte*)pData;
jbyteArray jarray = env->NewByteArray(nOutSize);
env->SetByteArrayRegin(jarray, 0, nOutSize, by);
jbyteArray 转 char *
char* data = (char*)env->GetByteArrayElements(strIn, 0);
char* 转 jstring
jstring WindowsTojstring(JNIEnv* env, char* str_tmp)
{ jstring rtn=0; int slen = (int)strlen(str_tmp); unsigned short* buffer=0; if(slen == 0) { rtn = env->NewStringUTF(str_tmp); } else { int length = MultiByteToWideChar(CP_ACP, 0, (LPCSTR)str_tmp, slen, NULL, 0); buffer = (unsigned short*)malloc(length*2+1); if(MultiByteToWideChar(CP_ACP, 0, (LPCSTR)str_tmp, slen, (LPWSTR)buffer, length) > 0) { rtn = env->NewString((jchar*)buffer, length); } } if(buffer) { free(buffer); } return rtn;
}
char* jstring 互转
JNIEXPORT jstring JNICALL Java_com_explorer_jni_SambaTreeNative_getDetailsBy (JNIEnv *env, jobject jobj, jstring pc_server, jstring server_user, jstring server_passwd)
{ const char *pc = env->GetStringUTFChars(pc_server, NULL); const char *user = env->GetStringUTFChars(server_user, NULL); const char *passwd = env->GetStringUTFChars(server_passwd, NULL); const char *details = smbtree::getPara(pc, user, passwd); jstring jDetails = env->NewStringUTF(details); return jDetails;
}
4、Android.mk、Application.mk
1、Android.mk
Android.mk 文件是 GNU Makefile 的一小部分,它用来对 Android 程序进行编译,Android.mk 中的变量都是全局的,解析过程会被定义。一个 Android.mk 文件可以编译多个模块,模块包括:APK程序、JAVA库、C\C++应用程序、C\C++静态库、C\C++共享库。
简单实例如下:
LOCAL_PATH := $(call my-dir) #表示是当前文件的路径
include $(CLEAR_VARS) #指定让GNU MAKEFILE该脚本为你清除许多 LOCAL_XXX 变量
LOCAL_MODULE:= helloworld #标识你在 Android.mk 文件中描述的每个模块
MY_SOURCES := foo.c #自定义变量
ifneq ($(MY_CONFIG_BAR),) MY_SOURCES += bar.c
endif
LOCAL_SRC_FILES += $(MY_SOURCES) #包含将要编译打包进模块中的 C 或 C++源代码文件
include $(BUILD_SHARED_LIBRARY) #根据LOCAL_XXX系列变量中的值,来编译生成共享库(动态链接库)
GNU Make 系统变量
变量 | 描述 |
CLEAR_VARS | 指向一个编译脚本,几乎所有未定义的 LOCAL_XXX 变量都在"Module-description"节中列出。必须在开始一个新模块之前包含这个脚本:include$(CLEAR_VARS),用于重置除LOCAL_PATH变量外的,所有LOCAL_XXX系列变量。 |
BUILD_SHARED_LIBRARY | 指向编译脚本,根据所有的在 LOCAL_XXX 变量把列出的源代码文件编译成一个共享库。 |
BUILD_STATIC_LIBRARY | 一个 BUILD_SHARED_LIBRARY 变量用于编译一个静态库。静态库不会复制到的APK包中,但是能够用于编译共享库。 |
TARGET_ARCH | 目标 CPU平台的名字, 和 android 开放源码中指定的那样。如果是arm,表示要生成 ARM 兼容的指令,与 CPU架构的修订版无关。 |
TARGET_PLATFORM | Android.mk 解析的时候,目标 Android 平台的名字.详情可参考/development/ndk/docs/stable- apis.txt. |
TARGET_ARCH_ABI | 支持目标平台 |
TARGET_ABI | 目标平台和 ABI 的组合,它事实上被定义成$(TARGET_PLATFORM)-$(TARGET_ARCH_ABI) ,在想要在真实的设备中针对一个特别的目标系统进行测试时,会有用。在默认的情况下,它会是'android-3-arm'。 |
模块描述变量
变量 | 描述 |
LOCAL_PATH | 这个变量用于给出当前文件的路径。必须在 Android.mk 的开头定义,可以这样使用:LOCAL_PATH := $(call my-dir) 这个变量不会被$(CLEAR_VARS)清除,因此每 个 Android.mk 只需要定义一次(即使在一个文件中定义了几个模块的情况下)。 |
LOCAL_MODULE | 这是模块的名字,它必须是唯一的,而且不能包含空格。必须在包含任一的$(BUILD_XXXX)脚本之前定义它。模块的名字决定了生成文件的名字。例如,如果一个一个共享库模块的名字是,那么生成文件的名字就是 lib.so。但是,在的 NDK 生成文件中(或者 Android.mk 或者 Application.mk),应该只涉及(引用)有正常名字的其他模块。 |
LOCAL_SRC_FILES | 这是要编译的源代码文件列表。只要列出要传递给编译器的文件,因为编译系统自动计算依赖。注意源代码文件名称都是相对于 LOCAL_PATH的,你可以使用路径部分。 |
LOCAL_CPP_EXTENSION | 这是一个可选变量, 用来指定C++代码文件的扩展名,默认是'.cpp',但是可以改变它。 |
LOCAL_C_INCLUDES | 可选变量,表示头文件的搜索路径。 |
LOCAL_CFLAGS | 可选的编译器选项,在编译 C 代码文件的时候使用。 |
LOCAL_CXXFLAGS | 与 LOCAL_CFLAGS同理,针对 C++源文件。 |
LOCAL_CPPFLAGS | 与 LOCAL_CFLAGS同理,但是对 C 和 C++ source files都适用。 |
LOCAL_STATIC_LIBRARIES | 表示该模块需要使用哪些静态库,以便在编译时进行链接。 |
LOCAL_SHARED_LIBRARIES | 表示模块在运行时要依赖的共享库(动态库),在链接时就需要,以便在生成文件时嵌入其相应的信息。注意:它不会附加列出的模块到编译图,也就是仍然需要在Application.mk 中把它们添加到程序要求的模块中。 |
LOCAL_LDLIBS | 编译模块时要使用的附加的链接器选项。这对于使用‘-l’前缀传递指定库的名字是有用的。 |
LOCAL_ALLOW_UNDEFINED_SYMBOLS | 默认情况下, 在试图编译一个共享库时,任何未定义的引用将导致一个“未定义的符号”错误。 |
LOCAL_ARM_MODE | 默认情况下, arm目标二进制会以 thumb 的形式生成(16 位),你可以通过设置这个变量为 arm如果你希望你的 module 是以 32 位指令的形式。 |
LOCAL_MODULE_PATH 和 LOCAL_UNSTRIPPED_PATH | 在 Android.mk 文件中, 还可以用LOCAL_MODULE_PATH 和LOCAL_UNSTRIPPED_PATH指定最后的目标安装路径. 不同的文件系统路径用以下的宏进行选择: TARGET_ROOT_OUT:表示根文件系统。 TARGET_OUT:表示 system文件系统。 TARGET_OUT_DATA:表示 data文件系统。 用法如:LOCAL_MODULE_PATH :=$(TARGET_ROOT_OUT) 至于LOCAL_MODULE_PATH 和LOCAL_UNSTRIPPED_PATH的区别,暂时还不清楚。 |
GNU Make 功能宏
变量 | 描述 |
my-dir | 返回当前 Android.mk 所在的目录的路径,相对于 NDK 编译系统的顶层。 |
all-subdir-makefiles | 返回一个位于当前'my-dir'路径的子目录中的所有Android.mk的列表。 |
this-makefile | 返回当前Makefile 的路径(即这个函数调用的地方) |
parent-makefile | 返回调用树中父 Makefile 路径。即包含当前Makefile的Makefile 路径。 |
grand-parent-makefile | 返回调用树中父Makefile的父Makefile的路径 |
范例:编译一个简单的 APK
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
# Build all java files in the java subdirectory
LOCAL_SRC_FILES := $(call all-subdir-java-files)
# Name of the APK to build
LOCAL_PACKAGE_NAME := LocalPackage
# Tell it to build an APK
include $(BUILD_PACKAGE)
编译一个依赖静态 .jar 文件的 APK
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) # List of static libraries to include in the package LOCAL_STATIC_JAVA_LIBRARIES := static-library # Build all java files in the java subdirectory LOCAL_SRC_FILES := $(call all-subdir-java-files) # Name of the APK to build LOCAL_PACKAGE_NAME := LocalPackage # Tell it to build an APK include $(BUILD_PACKAGE) 注:LOCAL_STATIC_JAVA_LIBRARIES 后面应是你的APK程序所需要的JAVA库的JAR文件名。
编译一个需要 platform key 签名的 APK
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) # Build all java files in the java subdirectory LOCAL_SRC_FILES := $(call all-subdir-java-files) # Name of the APK to build LOCAL_PACKAGE_NAME := LocalPackage LOCAL_CERTIFICATE := platform # Tell it to build an APK include $(BUILD_PACKAGE) 注:LOCAL_CERTIFICATE 后面应该是签名文件的文件名
编译一个需要特殊 vendor key 签名的 APK
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) # Build all java files in the java subdirectory LOCAL_SRC_FILES := $(call all-subdir-java-files) # Name of the APK to build LOCAL_PACKAGE_NAME := LocalPackage LOCAL_CERTIFICATE := vendor/example/certs/app # Tell it to build an APK include $(BUILD_PACKAGE)
装载一个普通的第三方 APK
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) # Module name should match apk name to be installed. LOCAL_MODULE := LocalModuleName LOCAL_SRC_FILES := $(LOCAL_MODULE).apk LOCAL_MODULE_CLASS := APPS LOCAL_MODULE_SUFFIX := $(COMMON_ANDROID_PACKAGE_SUFFIX) LOCAL_CERTIFICATE := platform include $(BUILD_PREBUILT)
装载需要 .so(动态库)的第三方 apk
LOCAL_PATH := $(my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := baiduinput_android_v1.1_1000e
LOCAL_SRC_FILES := $(LOCAL_MODULE).apk
LOCAL_MODULE_CLASS := APPS
LOCAL_MODULE_SUFFIX := $(COMMON_ANDROID_PACKAGE_SUFFIX)
LOCAL_CERTIFICATE := platform
include $(BUILD_PREBUILT) #################################################################
####### copy the library to /system/lib #########################
#################################################################
include $(CLEAR_VARS)
LOCAL_MODULE := libinputcore.so
LOCAL_MODULE_CLASS := SHARED_LIBRARIES
LOCAL_MODULE_PATH := $(TARGET_OUT_SHARED_LIBRARIES)
LOCAL_SRC_FILES := lib/$(LOCAL_MODULE)
OVERRIDE_BUILD_MODULE_PATH := $(TARGET_OUT_INTERMEDIATE_LIBRARIES)
include $(BUILD_PREBUILT)
编译一个静态 java 库
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) # Build all java files in the java subdirectory LOCAL_SRC_FILES := $(call all-subdir-java-files) # Any libraries that this library depends on LOCAL_JAVA_LIBRARIES := android.test.runner # The name of the jar file to create LOCAL_MODULE := sample # Build a static jar file. include $(BUILD_STATIC_JAVA_LIBRARY)
注:LOCAL_JAVA_LIBRARIES表示生成的java库的jar文件名。
编译 C/C++ 应用程序模板
LOCAL_PATH := $(call my-dir)
#include $(CLEAR_VARS)
LOCAL_SRC_FILES := main.c
LOCAL_MODULE := test_exe
#LOCAL_C_INCLUDES :=
#LOCAL_STATIC_LIBRARIES :=
#LOCAL_SHARED_LIBRARIES :=
include $(BUILD_EXECUTABLE)
注:‘:=’是赋值的意思,'+='是追加的意思,‘$’表示引用某变量的值
LOCAL_SRC_FILES中加入源文件路径,LOCAL_C_INCLUDES中加入需要的头文件搜索路径
LOCAL_STATIC_LIBRARIES 加入所需要链接的静态库(*.a)的名称,
LOCAL_SHARED_LIBRARIES 中加入所需要链接的动态库(*.so)的名称,
LOCAL_MODULE表示模块最终的名称,BUILD_EXECUTABLE 表示以一个可执行程序的方式进行编译。
(4)编译C\C++静态库
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_SRC_FILES := \ helloworld.c
LOCAL_MODULE:= libtest_static #LOCAL_C_INCLUDES :=
#LOCAL_STATIC_LIBRARIES :=
#LOCAL_SHARED_LIBRARIES :=
include $(BUILD_STATIC_LIBRARY)
和上面相似,BUILD_STATIC_LIBRARY 表示编译一个静态库。
编译 C\C++ 动态库 的 模板
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_SRC_FILES := helloworld.c
LOCAL_MODULE := libtest_shared
TARGET_PRELINK_MODULES := false
#LOCAL_C_INCLUDES :=
#LOCAL_STATIC_LIBRARIES :=
#LOCAL_SHARED_LIBRARIES :=
include $(BUILD_SHARED_LIBRARY)
和上面相似,BUILD_SHARED_LIBRARY 表示编译一个共享库。
以上三者的生成结果分别在如下目录中,generic 依具体 target 会变:
out/target/product/generic/obj/APPS
out/target/product/generic/obj/JAVA_LIBRARIES
out/target/product/generic/obj/EXECUTABLE
out/target/product/generic/obj/STATIC_LIBRARY
out/target/product/generic/obj/SHARED_LIBRARY
每个模块的目标文件夹分别为:
1)APK程序:XXX_intermediates
2)JAVA库程序:XXX_intermediates
这里的XXX 3)C\C++可执行程序:XXX_intermediates 4)C\C++静态库: XXX_static_intermediates 5)C\C++动态库: XXX_shared_intermediates
实例:
LOCAL_PATH := $(call my-dir) #项目地址
include $(CLEAR_VARS) #清除变量 LOCAL_MODULE := libvlcjni #库 #源文件
LOCAL_SRC_FILES := libvlcjni.c libvlcjni-util.c libvlcjni-track.c libvlcjni-medialist.c aout.c vout.c libvlcjni-equalizer.c native_crash_handler.c
LOCAL_SRC_FILES += thumbnailer.c pthread-condattr.c pthread-rwlocks.c pthread-once.c eventfd.c sem.c
LOCAL_SRC_FILES += pipe2.c
LOCAL_SRC_FILES += wchar/wcpcpy.c
LOCAL_SRC_FILES += wchar/wcpncpy.c
LOCAL_SRC_FILES += wchar/wcscasecmp.c
LOCAL_SRC_FILES += wchar/wcscat.c
LOCAL_SRC_FILES += wchar/wcschr.c
LOCAL_SRC_FILES += wchar/wcscmp.c
LOCAL_SRC_FILES += wchar/wcscoll.c
LOCAL_SRC_FILES += wchar/wcscpy.c
LOCAL_SRC_FILES += wchar/wcscspn.c
LOCAL_SRC_FILES += wchar/wcsdup.c
LOCAL_SRC_FILES += wchar/wcslcat.c
LOCAL_SRC_FILES += wchar/wcslcpy.c
LOCAL_SRC_FILES += wchar/wcslen.c
LOCAL_SRC_FILES += wchar/wcsncasecmp.c
LOCAL_SRC_FILES += wchar/wcsncat.c
LOCAL_SRC_FILES += wchar/wcsncmp.c
LOCAL_SRC_FILES += wchar/wcsncpy.c
LOCAL_SRC_FILES += wchar/wcsnlen.c
LOCAL_SRC_FILES += wchar/wcspbrk.c
LOCAL_SRC_FILES += wchar/wcsrchr.c
LOCAL_SRC_FILES += wchar/wcsspn.c
LOCAL_SRC_FILES += wchar/wcsstr.c
LOCAL_SRC_FILES += wchar/wcstok.c
LOCAL_SRC_FILES += wchar/wcswidth.c
LOCAL_SRC_FILES += wchar/wcsxfrm.c
LOCAL_SRC_FILES += wchar/wmemchr.c
LOCAL_SRC_FILES += wchar/wmemcmp.c
LOCAL_SRC_FILES += wchar/wmemcpy.c
LOCAL_SRC_FILES += wchar/wmemmove.c
LOCAL_SRC_FILES += wchar/wmemset.c LOCAL_C_INCLUDES := $(VLC_SRC_DIR)/include #包含头 ARCH=$(ANDROID_ABI) #变量 平台 CPP_STATIC=$(ANDROID_NDK)/sources/cxx-stl/gnu-libstdc++$(CXXSTL)/libs/$(ARCH)/libgnustl_static.a #应用静态库 LOCAL_CFLAGS := -std=gnu99 #编译器标识
ifeq ($(ARCH), armeabi) LOCAL_CFLAGS += -DHAVE_ARMEABI # Needed by ARMv6 Thumb1 (the System Control coprocessor/CP15 is mandatory on ARMv6) # On newer ARM architectures we can use Thumb2 LOCAL_ARM_MODE := arm
endif
ifeq ($(ARCH), armeabi-v7a) LOCAL_CFLAGS += -DHAVE_ARMEABI_V7A
endif
LOCAL_LDLIBS := -L$(VLC_CONTRIB)/lib \ #使用本地库 $(VLC_MODULES) \ $(VLC_BUILD_DIR)/lib/.libs/libvlc.a \ $(VLC_BUILD_DIR)/src/.libs/libvlccore.a \ $(VLC_BUILD_DIR)/compat/.libs/libcompat.a \ -ldl -lz -lm -llog \ -ldvbpsi -lebml -lmatroska -ltag \ -logg -lFLAC -ltheora -lvorbis \ -lmpeg2 -la52 \ -lavformat -lavcodec -lswscale -lavutil -lpostproc -lgsm -lopenjpeg \ -lliveMedia -lUsageEnvironment -lBasicUsageEnvironment -lgroupsock \ -lspeex -lspeexdsp \ -lxml2 -lpng -lgnutls -lgcrypt -lgpg-error \ -lnettle -lhogweed -lgmp \ -lfreetype -liconv -lass -lfribidi -lopus \ -lEGL -lGLESv2 -ljpeg \ -ldvdnav -ldvdread -ldvdcss \ $(CPP_STATIC) include $(BUILD_SHARED_LIBRARY) #编译成动态库 include $(CLEAR_VARS) #清除变量 LOCAL_MODULE := libiomx-gingerbread
LOCAL_SRC_FILES := ../$(VLC_SRC_DIR)/modules/codec/omxil/iomx.cpp
LOCAL_C_INCLUDES := $(VLC_SRC_DIR)/modules/codec/omxil $(ANDROID_SYS_HEADERS_GINGERBREAD)/frameworks/base/include $(ANDROID_SYS_HEADERS_GINGERBREAD)/system/core/include
LOCAL_CFLAGS := -Wno-psabi
LOCAL_LDLIBS := -L$(ANDROID_LIBS) -lgcc -lstagefright -lmedia -lutils -lbinder include $(BUILD_SHARED_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE := libiomx-hc
LOCAL_SRC_FILES := ../$(VLC_SRC_DIR)/modules/codec/omxil/iomx.cpp
LOCAL_C_INCLUDES := $(VLC_SRC_DIR)/modules/codec/omxil $(ANDROID_SYS_HEADERS_HC)/frameworks/base/include $(ANDROID_SYS_HEADERS_HC)/frameworks/base/native/include $(ANDROID_SYS_HEADERS_HC)/system/core/include $(ANDROID_SYS_HEADERS_HC)/hardware/libhardware/include
LOCAL_CFLAGS := -Wno-psabi
LOCAL_LDLIBS := -L$(ANDROID_LIBS) -lgcc -lstagefright -lmedia -lutils -lbinder include $(BUILD_SHARED_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE := libiomx-ics
LOCAL_SRC_FILES := ../$(VLC_SRC_DIR)/modules/codec/omxil/iomx.cpp
LOCAL_C_INCLUDES := $(VLC_SRC_DIR)/modules/codec/omxil $(ANDROID_SYS_HEADERS_ICS)/frameworks/base/include $(ANDROID_SYS_HEADERS_ICS)/frameworks/base/native/include $(ANDROID_SYS_HEADERS_ICS)/system/core/include $(ANDROID_SYS_HEADERS_ICS)/hardware/libhardware/include
LOCAL_CFLAGS := -Wno-psabi
LOCAL_LDLIBS := -L$(ANDROID_LIBS) -lgcc -lstagefright -lmedia -lutils -lbinder include $(BUILD_SHARED_LIBRARY)
2、Application.mk
Application.mk 目的是描述在你的应用程序中所需要的模块 (即 静态库 或 动态库)。
变量 | 描述 |
APP_PROJECT_PATH | 这个变量是强制性的,并且会给出应用程序工程的根目录的一个绝对路径。 |
APP_MODULES | 这个变量是可选的,如果没有定义,NDK将由在Android.mk中声明的默认的模块编译,并且包含所有的子文件(makefile文件)如果APP_MODULES定义了,它不许是一个空格分隔的模块列表,这个模块名字被定义在Android.mk文件中的LOCAL_MODULE中。 |
APP_OPTIM | 这个变量是可选的,用来义“release”或"debug"。在编译您的应用程序模块的时候,可以用来改变优先级。 |
APP_CFLAGS | 当编译模块中有任何C文件或者C++文件的时候,C编译器的信号就会被发出。 |
APP_CXXFLAGS | APP_CPPFLAGS的别名,已经考虑在将在未来的版本中废除了 |
APP_CPPFLAGS | 当编译的只有C++源文件的时候,可以通过这个C++编译器来设置 |
APP_BUILD_SCRIPT | 默认情况下,NDK编译系统会在$(APP_PROJECT_PATH)/jni目录下寻找名为Android.mk文件: $(APP_PROJECT_PATH)/jni/Android.mk |
APP_ABI | 默认情况下,NDK的编译系统回味"armeabi"ABI生成机器代码。 |
APP_STL | 默认情况下,NDK的编译系统为最小的C++运行时库(/system/lib/libstdc++.so)提供C++头文件。然而,NDK的C++的实现,可以让你使用或着链接在自己的应用程序中。 例如: APP_STL := stlport_static --> static STLport library APP_STL := stlport_shared --> shared STLport library APP_STL := system --> default C++ runtime library |
实例:
APP_OPTIM := release //调试版还是发行版
APP_PLATFORM := android-8 //平台
APP_STL := gnustl_static //C++运行时库
APP_CPPFLAGS += -frtti //编译标识
APP_CPPFLAGS += -fexceptions //编译标识 异常
APP_CPPFLAGS += -DANDROID //编译标识
APP_MODULES := test //静态模块
JNI 内存泄漏
JAVA 编程中的内存泄漏,从泄漏的内存位置角度可以分为两种:
- JVM 中 Java Heap 的内存泄漏。
Java 对象存储在 JVM 进程空间中的 Java Heap 中,Java Heap 可以在 JVM 运行过程中动态变化。如果 Java 对象越来越多,占据 Java Heap 的空间也越来越大,JVM 会在运行时扩充 Java Heap 的容量。如果 Java Heap 容量扩充到上限,并且在 GC 后仍然没有足够空间分配新的 Java 对象,便会抛出 out of memory 异常,导致 JVM 进程崩溃。
Java Heap 中 out of memory 异常的出现有两种原因①程序过于庞大,致使过多 Java 对象的同时存在;②程序编写的错误导致 Java Heap 内存泄漏。 - JVM 内存中 native memory 的内存泄漏。
从操作系统角度看,JVM 在运行时和其它进程没有本质区别。在系统级别上,它们具有同样的调度机制,同样的内存分配方式,同样的内存格局。
JVM 进程空间中,Java Heap 以外的内存空间称为 JVM 的 native memory。进程的很多资源都是存储在 JVM 的 native memory 中,例如载入的代码映像,线程的堆栈,线程的管理控制块,JVM 的静态数据、全局数据等等。也包括 JNI 程序中 native code 分配到的资源。
在 JVM 运行中,多数进程资源从 native memory 中动态分配。当越来越多的资源在 native memory 中分配,占据越来越多 native memory 空间并且达到 native memory 上限时,JVM 会抛出异常,使 JVM 进程异常退出。而此时 Java Heap 往往还没有达到上限。
多种原因可能导致 JVM 的 native memory 内存泄漏。
例如:
JVM 在运行中过多的线程被创建,并且在同时运行。
JVM 为线程分配的资源就可能耗尽 native memory 的容量。
JNI 编程错误也可能导致 native memory 的内存泄漏。
Native Code 本身的内存泄漏
JNI 编程首先是一门具体的编程语言,或者 C 语言,或者 C++,或者汇编,或者其它 native 的编程语言。每门编程语言环境都实现了自身的内存管理机制。因此,JNI 程序开发者要遵循 native 语言本身的内存管理机制,避免造成内存泄漏。以 C 语言为例,当用 malloc() 在进程堆中动态分配内存时,JNI 程序在使用完后,应当调用 free() 将内存释放。总之,所有在 native 语言编程中应当注意的内存泄漏规则,在 JNI 编程中依然适应。
Native 语言本身引入的内存泄漏会造成 native memory 的内存,严重情况下会造成 native memory 的 out of memory。
Global Reference 引入的内存泄漏
JNI 编程还要同时遵循 JNI 的规范标准,JVM 附加了 JNI 编程特有的内存管理机制。
JNI 中的 Local Reference 只在 native method 执行时存在,当 native method 执行完后自动失效。这种自动失效,使得对 Local Reference 的使用相对简单,native method 执行完后,它们所引用的 Java 对象的 reference count 会相应减 1。不会造成 Java Heap 中 Java 对象的内存泄漏。而 Global Reference 对 Java 对象的引用一直有效,因此它们引用的 Java 对象会一直存在 Java Heap 中。程序员在使用 Global Reference 时,需要仔细维护对 Global Reference 的使用。如果一定要使用 Global Reference,务必确保在不用的时候删除。就像在 C 语言中,调用 malloc() 动态分配一块内存之后,调用 free() 释放一样。否则,Global Reference 引用的 Java 对象将永远停留在 Java Heap 中,造成 Java Heap 的内存泄漏。
LocalReference 的深入理解
Local Reference 在 native method 执行完成后,会自动被释放,似乎不会造成任何的内存泄漏。但这是错误的。
泄漏实例1:创建大量的 JNI Local Reference
Java 代码部分 class TestLocalReference { private native void nativeMethod(int i); public static void main(String args[]) { TestLocalReference c = new TestLocalReference(); //call the jni native method c.nativeMethod(1000000); } static { //load the jni library System.loadLibrary("StaticMethodCall"); } } JNI 代码,nativeMethod(int i) 的 C 语言实现 #include<stdio.h> #include<jni.h> #include"TestLocalReference.h" JNIEXPORT void JNICALL Java_TestLocalReference_nativeMethod (JNIEnv * env, jobject obj, jint count) { jint i = 0; jstring str; for(; i<count; i++) str = (*env)->NewStringUTF(env, "0"); }
运行结果 JVMCI161: FATAL ERROR in native method: Out of memory when expanding local ref table beyond capacity at TestLocalReference.nativeMethod(Native Method) at TestLocalReference.main(TestLocalReference.java:9)
泄漏实例2:建立一个 String 对象,返回给调用函数。
JNI 代码,nativeMethod(int i) 的 C 语言实现 #include<stdio.h> #include<jni.h> #include"TestLocalReference.h" jstring CreateStringUTF(JNIEnv * env) { return (*env)->NewStringUTF(env, "0"); } JNIEXPORT void JNICALL Java_TestLocalReference_nativeMethod (JNIEnv * env, jobject obj, jint count) { jint i = 0; for(; i<count; i++) { str = CreateStringUTF(env); } }
运行结果 JVMCI161: FATAL ERROR in native method: Out of memory when expanding local ref table beyond capacity at TestLocalReference.nativeMethod(Native Method) at TestLocalReference.main(TestLocalReference.java:9)
编译问题:SLES/OpenSLES.h: No such file or directory
解决方法:ndk-build TARGET_PLATFORM=android-9
编译断点问题:有没有好用的断点工具
解决方法:visualGDB 神器
Windows 下 JNI 的使用教程
参考:IntelliJ idea 2018 平台下JNI编程调用 C++ 算法(一):https://www.cnblogs.com/lucychen/p/9771236.html
JNI 的使用大致有以下4个步骤:
- 一、在 Java 中写 native 方法
- 二、用 javah 命令生成 C/C++ 头文件。( 注意:windows 系统生成的动态链接库是 .dll 文件,Linux 是 .so 文件。JDK10 中将 javah 工具取消了,需要使用 javac -h 替代,这是与 jdk8 不同的地方。 )
- 三、写对应的 C/C++ 程序,实现头文件中声明的方法,并编译成库文件
- 四、在 Java 中加载这个库文件并使用
注意:Windows 平台需要注意操作系统位数,32 位 dll 无法在 64位 上被调用。
一、在 Java 中写 native 方法
主要步骤
- 创建一个 java 项目,在其中编写一个带有 native 方法的类
- 利用 idea 生成 .h 头文件。
- 在 vs 中创建一个动态链接库应用程序的解决方案
- 在解决方案中创建 C++ 文件,实现头文件中的方法
- 生成 动态 链接库
- 回到 idea,运行 java 项目,排错重复以上步骤直到运行成功
1. 在 idea 创建 java 项目
实现一个简单的 testHello_1() 函数 和 静态的 testHell0_2() 函数,在 C++ 中实现 testHello_1() 和 testHell0_2()。
注意:java 代码都不要放到默认包下(就是不写 package 语句就会放到默认包),默认包下的方法在其他地方都不能调用!!
步骤如下:
- 在 idea 创建 java 项目(例如:jni_demo),在 src 目录下新建一个 package(示例 包名 com.jni.test )。
- 在包下创建一个类,用来编写 native 方法和 main 函数。示例 类名 JNIDemo
- 声明 native 方法,native 方法就是声明一个非 java 实现的方法,比如用 C/C++ 实现。本地方法可以是静态的,也可以不声明为静态的。
图示:
示例代码:
package com.jni.test;public class JNIDemo {public native void testHello_1();public static native int testHello_2();public static void main(String[] args) {try {// System.loadLibrary("JNIPROJECT.dll");System.load("D:\\jni_demo\\src\\com\\jni\\test\\JNIPROJECT.dll");JNIDemo jniDemo =new JNIDemo();jniDemo.testHello_1();int retVal = testHello_2();System.out.println("retVal : " + retVal);}catch (Exception ex) {ex.printStackTrace();}}
}
其中 testHello_1 是一个类方法,testHello_2 是一个静态方法,前面都有 native 代表是一个本地函数。
main 函数中,调用 testHello_1 函数 和 testHello_2 函数。下面的 static 代码块暂且不谈。
代码写好后,build 一下项目,生成 class文件,build 后,可在左侧目录看到 out/production 目录下生成了对应 class 文件。
注:
load 和 loadLibrary 区别
- 它们都可以用来装载库文件,不论是 JNI 库文件还是非 JNI 库文件。在任何本地方法被调用之前必须先用这个两个方法之一把相应的 JNI 库文件装载。
- System.load 参数为库文件的绝对路径,可以是任意路径。例如,你可以这样载入一个 windows 平台下 JNI 库 文件:System.load("C:\\Documents and Settings\\TestJNI.dll");
- System.loadLibrary 参数为库文件名,不包含库文件的扩展名。例如,你可以这样载入一个 windows 平台下 JNI 库 文件System. loadLibrary ("TestJNI"); 这里,TestJNI.dll 必须是在 java.library.path 这一 jvm 变量所指向的路径中。
可以通过如下方法来获得该变量的值:System.getProperty("java.library.path");
默认情况下,在 Windows 平台下,该值包含如下位置:
1)和 jre 相关的一些目录
2)程序当前目录
3)Windows 目录
4)系统目录(system32)
5)系统环境变量 path 指定目录。
classpath 与 java.library.path 区别
classpath 路径下,只能是 jar 或者 class 文件,否者会报错,因为他们会被 load 到 JVM 中
build ---> build project,
2.生成 头文件
静态注册 和 动态注册
为什么需要注册?其实就是给 Java 的 native 函数找到底层 C/C++ 实现的函数指针。
- 静态注册:通过包名、类名一致来确认,Java 有一个命令 javah,专门生成某一个 JAVA 文件所有的 native 函数的头文件(h文件), 静态方法注册 JNI 有哪些缺点?1:必须遵循某些规则。 2:名字过长。 3:多个 class 需 Javah 多遍。 4:运行时去找效率不高
- 动态注册 :在 JNI 层实现的,JAVA 层不需要关心,因为在 system.load 时就会去掉 JNI_OnLoad,有就注册,没就不注册。
- 区别:静态注册是用到时加载,动态注册一开始就加载好了,这个可以从 DVM 的源代码看出来。
生成 JNI 头文件。(此处有两种方法:2.1手动输入 javah 命令生成头文件、2.2 一键生成头文件)
2.1 手动输入 javah 命令生成头文件
打开 cmd,进入 src 目录,运行 javah 命令,生成 C/C++ 头文件,注意:要带上 java 包名
命令格式:javah -classpath 要加载的类的路径 -jni 包名.类名
执行完命令之后,会在 src 目录生成一个 .h 文件:
在 IntelliJ IDEA 图示:
头文件完整代码:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_jni_test_JNIDemo */#ifndef _Included_com_jni_test_JNIDemo
#define _Included_com_jni_test_JNIDemo
#ifdef __cplusplus
extern "C" {
#endif
/** Class: com_jni_test_JNIDemo* Method: testHello_1* Signature: ()V*/
JNIEXPORT void JNICALL Java_com_jni_test_JNIDemo_testHello_11(JNIEnv *, jobject);/** Class: com_jni_test_JNIDemo* Method: testHello_2* Signature: ()I*/
JNIEXPORT jint JNICALL Java_com_jni_test_JNIDemo_testHello_12(JNIEnv *, jclass);#ifdef __cplusplus
}
#endif
#endif
头文件 说明:
- 包含了 jni.h 头文件。
-
在类中 声明 的常量(
static final
)类型会在头文件中以宏的形式出现,这一点还是很方便的。 - 函数的注释还是比较全的,包括了:
- 对应的 class
- 对应的 java 方法名
- 对应 java 方法 的 签名
- 方法的声明显得有点奇怪,由以下及部分组成:
JNIEXPORT
这是函数的导出方式- jint 返回值类型( jint 由 jni.h定义,对应 int )
- JNICALL 函数的调用方式也就是汇编级别参数的传入方式
- Java_com_jni_test_JNIDemo_testHello_11 超级长的函数名!!!
格式是 :Java_ + 类全名 + _ + JAVA中声明的 native 方法名。其中会把包名中的点(.)替换成下划线(_),同时为了避免冲突把 下划线 替换成 _1。 - 方法的参数,上面的这个方法在 JAVA 的声明中实际上是没有参数的,其中的
JNIENV
顾名思义是 JNI 环境,和具体的线程绑定。而第二个参数jclass
其实是 java 中的Class
因为上面是一个static
方法,因此第二个参数是jclass
。如果是一个实例方法则对应第二个参数是jobject
,相当于 java 中的this
。
2.2 一键生成头文件
头文件可以使用命令行生成(见参考文献),或者熟悉格式后自己手写。但是如果希望能够随便点一下就生成头文件,于是,找到了一种 用idea工具生成头文件的方法,那就是 External Tools。External Tools 其实就是将手动输入的命令存下来,本质也是运行 javah,后面跟着配置参数,这些参数存在 External Tools,避免每次手动输入。
- 添加 External Tools。File -> Settings -> Tools -> ExternalTools,点击添加
- 编辑 Tools
Name: Generate Header File
Program: $JDKPath$/bin/javah
Arguments: -jni -classpath $OutputPath$ -d ./jni $FileClass$
Working directory: $ProjectFileDir$Name:External Tools 的名称,喜欢什么起什么,只要自己明白
Program是javah工具所在地址,即jdk所在路径下的bin,该参数是指tool采用的运行工具是javah
Arguments设置的是javah的参数,具体可在命令行中查看javah的帮助,查看每个函数含义
Working directory:项目名称 -
生成头文件
保存工具后,右击需要生成头文件的类,即我们的SimpleHello,选择External Tool,点击我们刚刚创建的tool。
然后你就会发现我们的目录中多了一个jni文件夹,jni文件夹里面有一个名字长长的.h文件,成功!
提示:该方法适用于 jdk8,jdk10 中取消了 javah,的使用 javac -h。
jni.h 是什么 ?
- jni.h 头文件一般位于 $JAVA_HOME/jd{jdk-version}/include 目录内下面的一个文件,jni.h 里面存储了大量的函数和对象,这是 JNI 中所有的 类型、函数、宏 等定义的地方。C/C++ 世界的 JNI 规则就是由他制定的。它有个很好的方法就是通过 native 接口名来获取 C/C++ 函数。
- 另外还有个 %JAVA_HOME%\bin\include\win32 下的 jni_md.h 。
打个比方类似如下:public static String getCMethod(String javaMethodName);
它可以根据你的 java接口,找到 C函数并调用。但这就意味着你不能在 C 里随意写函数名,因为如果你写的 java 方法叫 native aaa(); C函数也叫 aaa(); 但 jni.h 通过 getCMethod(String javaMethodName) 去找的结果是 xxx(); 那这样就无法调用了。
既然不能随意写,怎么办?
没事,jdk 提供了一个通过 java 方法生成 C/C++ 函数接口名的工具 javah。
javah 是什么?
javah 就是提供具有 native method 的 java 对象的 C/C++ 函数接口。javah 命令可以提供一个 C/C++ 函数的接口。
然后就是在 C/C++ 中实现这个方法就可以了。
但是在动手前现大致了解以下 jni.h 制定的游戏规则。javah 生成的头文件里面使用的类型都是 jni.h 定义的,目的是做到 平台无关,比如保证在所有平台上 jint 都是 32位 的有符号整型。
基本对应关系如下:
jni 类型 | JAVA 类型 | 对应 本地类型 | 类型签名 |
---|---|---|---|
jboolean | boolean | uint8_t | Z |
jbyte | byte | char | B |
jcahr | char | uint16_t | C |
jshort | short | int16_t | S |
jint | int | int32_t | I |
jlong | long | int64_t | J |
jfloat | float | float | F |
jdouble | double | double | D |
void | void | void | V |
引用类型对应关系:
java 类型 | JNI 类型 | java 类型 | JNI 类型 |
---|---|---|---|
所有的实例引用 | jobject | java.lang.Class | jclass |
java.lang.String | jstring | Ocject[] | jobjectArray |
java.lang.Throwable | jthrowable | 基本类型[] | jxxxArray |
通过表格发现,除了上面定义的 String
,Class
,Throwable
,其他的类(除了数组)都是以 jobject
的形式出现的!事实上jstring, jclass 也都是 object 的子类。所以这里还是和 java 层一样,一切皆 jobject。(当然,如果 jni 在 C 语言中编译的话是没有继承的概念的,此时 jstring,jclass 等其实就是 jobject !用了 typedef 转换而已!!)
接下来是 JNIEnv *
这个指针,他提供了 JNI 中的一系列操作的接口函数。
JNI 中操作 jobject
其实也就是在 native 层操作 java 层的实例。 要操作一个实例无疑是:
-
获取/设置 (即 get/set )成员变量(field)的值
-
调用成员方法(method)
怎么得到 field 和 method?
通过使用 jfieldID 和 jmethodID: 在 JNI 中使用类似于放射的方式来进行 field 和 method 的操作。JNI 中使用 jfieldID 和jmethodID 来表示成员变量和成员方法,获取方式是:
jfieldID GetFieldID(jclass clazz, const char *name, const char *sig);
jfieldID GetStaticFieldID(jclass clazz, const char *name, const char *sig);
jmethodID GetMethodID(jclass clazz, const char *name, const char *sig);
jmethodID GetStaticMethodID(jclass clazz, const char *name, const char *sig) ;
其中最后一个参数是签名。 获取 jclass 的方法 除了实用上面静态方法的第二个参数外,还可以手动获取。 jclass FindClass(const char *name)
需要注意的是 name
参数,他是一个类包括包名的全称,但是需要把包名中的点.
替换成斜杠/
。
有了 jfieldID 和 jmethodID 就知道狗蛋住哪了,现在去狗蛋家找他玩 ♪(^∇^*)
成员变量:
-
get:
-
<type> Get<type>Field(jobject , jfieldID);
即可获得对应的field,其中field的类型是type,可以是上面类型所叙述的任何一种。 -
<type> GetStatic<type>Field(jobject , jfieldID);
同1,唯一的区别是用来获取静态成员。
-
-
set:
-
void Set<type>Field(jobject obj, jfieldID fieldID, <type> val)
-
void SetStatic<type>Field(jclass clazz, jfieldID fieldID, <type> value);
-
成员方法:
调用方法自然要把方法的参数传递进去,JNI中实现了三种参数的传递方式:
-
Call<type>Method(jobject obj, jmethod jmethodID, ...)
其中...
是C中的可变长参数,类似于printf
那样,可以传递不定长个参数。于是你可以把java方法需要的参数在这里面传递进去。 -
Call<type>MethodV(jobject obj, jmethodID methodID, va_list args)
其中的va_list
也是C中可变长参数相关的内容(我不了解,不敢瞎说。。。偷懒粘一下Oracle的文档)Programmers place all arguments to the method in an args argument of type va_list that immediately follows the methodID argument. The CallMethodV routine accepts the arguments, and, in turn, passes them to the Java method that the programmer wishes to invoke. -
Call<type>MethodA(jobject obj, jmethodID methodID, const jvalue * args)
哎!这个我知道可以说两句LOL~~这里的jvalue
通过查代码发现就是JNI中各个数据类型的union,所以可以使用任何类型复制!所以参数的传入方式是通过一个jvalue的数组,数组内的元素可以是任何jni类型。
然后问题又来了:(挖掘机技术到底哪家强?!o(*≧▽≦)ツ┏━┓) 如果传进来的参数和java声明的参数的不一致会怎么样!(即不符合方法签名)这里文档中没用明确解释,但是说道: > Exceptions raised during the execution of the Java method.
typedef union jvalue {jboolean z;jbyte b;jchar c;jshort s;jint i;jlong j;jfloat f;jdouble d;jobject l;
} jvalue;
- 调用实例方法(instance method):
<type> Call<type>Method(jobject obj, jmethodID methodID, ...);
调用一个具有<type>
类型返回值的方法。<type> Call<type>MethodV(jobject obj, jmethodID methodID, va_list args);
Call<type>MethodA(jobject obj, jmethodID methodID, const jvalue * args)
- 调用静态方法(static method):
<type> CallStatic<type>Method(jobject obj, jmethodID methodID, ...);
<type> CallStatic<type>MethodV(jobject obj, jmethodID methodID, va_list args);
CallStatic<type>MethodA(jobject obj, jmethodID methodID, const jvalue * args)
- 调用父类方法(super.method),这个就有点不一样了。多了一个jclass参数,jclass可以使obj的父类,也可以是obj自己的class,但是methodID必须是从jclass获取到的,这样就可以调用到父类的方法。
<type> CallNonvirtual<type>Method(jobject obj, jclass clazz, jmethodID methodID, ...)
<type> CallNonvirtual<type>MethodV(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, va_list args);
<type> CallNonvirtual<type>MethodA(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, const jvalue *args);
#### 数组的操作
数组是一个很常用的数据类型,在但是在JNI中并不能直接操作jni数组(比如jshortArray,jfloatArray)。使用方法是:
- 获取数组长度:
jsize GetArrayLength(jarray array)
- 创建新数组:
ArrayType New<PrimitiveType>Array(jsize length);
- 通过JNI数组获取一个C/C++数组:
<type>* Get<type>ArrayElements(jshortArray array, jboolean *isCopy)
- 指定原数组的范围获取一个C/C++数组(该方法只针对于原始数据数组,不包括Object数组):
void Get<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize len, NativeType *buf);
- 设置数组元素:
void Set<type>ArrayRegion(jshortArray array, jsize start, jsize len,const <type> *buf)
。again,如果是Object数组需要使用:void SetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index, jobject value);
- 使用完之后,释放数组:
void Release<type>ArrayElements(jshortArray array, jshort *elems, jint mode)
有点要说明的:
-
面的 3中的 isCopy:当你调用getArrayElements时JVM(Runtime)可以直接返回数组的原始指针,或者是copy一份,返回给你,这是由JVM决定的。所以isCopy就是用来记录这个的。他的值是
JNI_TURE
或者JNI_FALSE
。 -
6释放数组。一定要释放你所获得数组。其中有一个
mode
参数,其有三个可选值,分别表示:
-
0
-
原始数组:允许原数组被垃圾回收。
-
copy: 数据会从get返回的buffer copy回去,同时buffer也会被释放。
-
-
JNI_COMMIT
-
原始数组:什么也不做
-
copy: 数据会从get返回的buffer copy回去,同时buffer不会被释放。
-
-
JNI_ABORT
-
原始数组:允许原数组被垃圾回收。之前由JNI_COMMIT提交的对数组的修改将得以保留。
-
copy: buffer会被释放,同时buffer中的修改将不会copy回数组!
-
####关于引用与垃圾回收 比如上面有个方法传了一个jobject进来,然后我把她保存下来,方便以后使用。这样做是不行哒!因为他是一个LocalReference,所以不能保证jobject指向的真正的实例不被回收。也就是说有可能你用的时候那个指针已经是个野指针的。然后你的程序就直接Segment Fault了,呵呵。。。
在 JNI 中提供了三种类型的引用:
- Local Reference:即本地引用。在JNI层的函数,所有非全局引用对象都是Local Reference, 它包括函数调用是传入的jobject和JNI成函数创建的jobject。Local Reference的特点是一旦JNI层的函数返回,这些jobject就可能被垃圾回收。
- Glocal Reference:全局引用,这些对象不会主动释放,永远不会被垃圾回收。
- Weak Glocal Reference:弱全局引用,一种特殊的Global Reference,在运行过程中有可能被垃圾回收。所以使用之前需要使用
jboolean IsSameObject(jobject obj1, jobject obj2)
判断它是否已被回收。
Glocal Reference:
1. 创建:jobject NewGlobalRef(jobject lobj);
2. 释放:void DeleteGlobalRef(jobject gref);
Local Reference:
LocalReference也有一个释放的函数:void DeleteLocalRef(jobject obj)
,他会立即释放Local Reference。 这个方法可能略显多余,其实也是有它的用处的。刚才说Local Reference会再函数返回后释放掉,但是假如函数返回前就有很多引用占了很多内存,最好函数内就尽早释放不必要的内存。
####关于JNI_OnLoad 开头提到JNI_OnLoad是java1.2中新增加的方法,对应的还有一个JNI_OnUnload,分别是动态库被JVM加载、卸载的时候调用的函数。有点类似于WIndows里的DllMain。
前面提到的实现对应native的方法是实现javah生成的头文件中定义的方法,这样有几个弊端:
- 函数名太长。很长。。相当长。。。
- 函数会被导出,也就谁说可以在动态库的导出函数表里面找到这些函数。这将有利于别人对动态库的逆向工程,因此带来安全问题。
现在有了JNI_OnLoad,情况好多了。你不光能在其中完成动态注册native函数的工作还可以完成一些初始化工作。java对应的有了jint RegisterNatives(jclass clazz, const JNINativeMethod *methods,jint nMethods)
函数。参数分别是:
-
jclass clazz,于native层对应的java class
-
const JNINativeMethod *methods这是一个数组,数组的元素是JNI定义的一个结构体JNINativeMethod
-
上面的数组的长度
JNINativeMethod:代码中的定义如下:
/** used in RegisterNatives to describe native method name, signature,* and function pointer.*/typedef struct {char *name;char *signature;void *fnPtr;
} JNINativeMethod;
所以他有三个字段,分别是
字段 | 含义 |
---|---|
char *name | java class中的native方法名,只需要方法名即可 |
char *signature | 方法签名 |
void *fnPtr | 对应native方法的函数指针 |
于是现在你可以不用导出native函数了,而且可以随意给函数命名,唯一要保证的是参数及返回值的统一。然后需要一个const JNINativeMethod *methods
数组来完成映射工作。
看起来大概是这样的:
//只需导出JNI_OnLoad和JNI_OnUnload(这个函数不实现也行)
/*** These are the exported function in this library.
*/
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved);
JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *reserved);//为了在动态库中不用导出函数,全部声明为static
//native methods registered by JNI_OnLoad
static jint native_newInstance (JNIEnv *env, jclass);//实现native方法
/*
* Class: com_young_soundtouch_SoundTouch
* Method: native_newInstance
* Signature: ()I
*/
static jint native_newInstance
(JNIEnv *env, jclass ) {int instanceID = ++sInstanceIdentifer;SoundTouchWrapper *instance = new SoundTouchWrapper();if (instance != NULL) {sInstancePool[instanceID] = instance;++sInstanceCount;}LOGDBG("create new SouncTouch instance:%d", instanceID);return instanceID;
}//构造JNINativeMethod数组
static JNINativeMethod gsNativeMethods[] = {{"native_newInstance","()I",reinterpret_cast<void *> (native_newInstance)}
};
//计算数组大小
static const int gsMethodCount = sizeof(gsNativeMethods) / sizeof(JNINativeMethod);//JNI_OnLoad,注册native方法。
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {JNIEnv* env;jclass clazz;LOGD("JNI_OnLoad called");if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {return -1;}//FULL_CLASS_NAME是个宏定义,定义了对应java类的全名(要把包名中的点(.)_替换成斜杠(/))clazz = env->FindClass(FULL_CLASS_NAME);LOGDBG("register method, method count:%d", gsMethodCount);//注册JNI函数env->RegisterNatives(clazz, gsNativeMethods,gsMethodCount);//必须返回一个JNI_VERSION_1_1以上(不含)的版本号,否则直接加载失败return JNI_VERSION_1_6;
}
###实战技巧篇
这里主要是巧用C中的宏来减少重复工作:
####迅速生成全名
//修改包名时只需要改以下的宏定义即可
#define FULL_CLASS_NAME "com/young/soundtouch/SoundTouch"
#define func(name) Java_ ## com_young_soundtouch_SoundTouch_ ## name
#define constance(cons) com_young_soundtouch_SoundTouch_ ## cons
比如func(native_1newInstance)
展开成:Java_com_young_soundtouch_SoundTouch_native_1newInstance
即JNI中需要导出的函数名(不过用动态注册方式没太大用了)
constance(AUDIO_FORMAT_PCM16)
展开成com_young_soundtouch_SoundTouch_AUDIO_FORMAT_PCM16
这个着实有用。
而且如果包名改了也可以很方便的适应之。
###安卓的log
//define __USE_ANDROID_LOG__ in makefile to enable android log
#if defined(__ANDROID__) && defined(__USE_ANDROID_LOG__)
#include <android/log.h>
#define LOGV(...) __android_log_print((int)ANDROID_LOG_VERBOSE, "ST_jni", __VA_ARGS__)
#define LOGD(msg) __android_log_print((int)ANDROID_LOG_DEBUG, "ST_jni_dbg", "line:%3d %s", __LINE__, msg)
#define LOGDBG(fmt, ...) __android_log_print((int)ANDROID_LOG_DEBUG, "ST_jni_dbg", "line:%3d " fmt, __LINE__, __VA_ARGS__)
#else
#define LOGV(...)
#define LOGD(fmt)
#define LOGDBG(fmt, ...)
#endif
通过这样的宏定义在打LOGD或者LOGDBG的时候还能自动加上行号!调试起来爽多了!
####C++中清理内存的方式
由于C++里面需要手动清楚内存,因此我的解决方案是定义一个map,给每个实例一个id,用id把java中的对象和native中的对象绑定起来。在java层定义一个release
方法,用来释放本地的对象。 本地的 KEY-对象 映射 static std::map<int, SoundTouchWrapper*> sInstancePool;
####关于NDK 因为安卓的约定是把本地代码放到jni目录下面,但是假如有多个jni lib的时候会比较混乱,所以方案是每一个lib都在jni里面建一个子目录,然后jni里面的Android.mk就可以去构建子目录中的lib了。
jni/Android.mk如下(超级简单):
LOCAL_PATH := $(call my-dir)
include $(call all-subdir-makefiles)
然后在子目录soundtouch_module中的Android.mk就可以像一般的Android.mk一样书写规则了。
同时记录一下在Andoroid.mk中使用makefile内建函数wildcard
的方法。 有时候源文件是一个目录下的所有.cpp/.c文件,这时候wildcard
来统配会很方便。但是Android.mk与普通的Makefile的不同在于:
- 调用Android.mkmingling的${CWD}并不是Android.ml所在的目录。所以Android.mk中有一个变量
LOCAL_PATH := $(call my-dir)
来记录当前 Android.mk所在的目录。 - 同时还会把所有的
LOCAL_SRC_FILES
前面加上$(LOCAL_PATH)
这样写makefile的时候就可以用相对路径了,提供了方便。但是这也导致了坑!
因为1,直接使用相对路径会导致wildcard
匹配不到源文件。所以最好这么写FILE_LIST := $(wildcard $(LOCAL_PATH)/soundtouch_source/source/SoundTouch/*.cpp)
。然而又因为2,这样还是不行的。所以还需要匹配之后把$(LOCAL_PATH)
的部分去掉,因此还得这样$(FILE_LIST:$(LOCAL_PATH)/%=%)
.
还有个小tip:LOCAL_CFLAGS
中最好加上这个定义-fvisibility=hidden
这样就不会在动态库中导出不必要的函数了。
###附录签名
JAVA中的函数签名包括了函数的参数类型,返回值类型。因此即使是重载了的函数,其函数签名也不一样。java编译器就会根据函数签名来判断你调用的到地址哪个方法。 签名中表示类型是这样的
1.基本类型都对应一个大写字母,如下:
JAVA类型 | 类型签名 |
---|---|
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命令来获取签名(javah生成的头文件注释中也有签名):javap -x -p <类全名>
坑爹的是java中并不能通过反射来获取方法签名,需要自己写一个帮助类。 (其实我还写了个小程序可以自动生成签名,和JNI_OnLoad中注册要用到的JNINativeMethod
数组,从此再也不用糟心的去写那该死的数组了。LOL~~~)
3. 在 VS 中创建解决方案
接下来打开 Visual studio 2019,新建动态链接库: JniProject
填写 项目名,项目所在目录:
创建完成后再添加类:
设置项目包含目录
-
本来我是按照这篇文章复制jni.h等文件的,但是一直报错“找不到 源 文件 jni.h”。搞来搞去总是不成,后来才发现,我在vs2017直接复制,jni.h并没有到C++项目目录下,而是仍然在原来的目录里,这与java的ide很不同啊。虽然被这个问题搞到差点摔桌子,但我转念一想,在原来的目录下就还不错啊,省得我复制来复制去。于是刷刷刷设置了包含路径
- 点击项目,我的项目叫 jniCppDemo,在菜单栏选择:项目 -> 属性 -> 配置属性 -> VC++目录 -> 包含目录
- 设置包含路径:
设置 jni.h 所在路径: C:\Program Files\Java\jdk1.8.0_181\include
设置 jni_md.h 所在路径: C:\Program Files\Java\jdk1.8.0_181\include\win32
设置刚刚生成头文件所在路径: D:\javaWorkspace\jniJavaDemo\jni
如果不想设置 包含目录,可以直接把文件( jni.h、com_jni_test_JNIDemo.h、jni_md.h )复制到工程目录下.
JDK 安装目录的 include 目录下有一个 jni.h 的文件,include 的 win32 目录下有个 jni_md.h 文件,还有 java 工程的 src 目录下的C 头文件,一起拷贝到 C工程的 JniProject 目录下:( JniProject ---> jni.h com_jni_test_JNIdemo.h jni_md.h )如下图:
在 C项目的头文件文件夹上面:右键 --- > 添加 ---> 现有项
选择 jni.h、com_jni_test_JNIDemo.h、jni_md.h
添加完可以在 头文件 目录中看到
打开 com_jni_test_JNIDemo.h 文件
将 #include <jni.h> 修改为 #include "jni.h" 错误提示消失。
4. 编写 cpp 文件
然后在 TestJNI.cpp 文件中写入如下代码:
#include "pch.h"
#include "TestJNI.h"
#include "com_jni_test_JNIDemo.h"JNIEXPORT void JNICALL Java_com_jni_test_JNIDemo_testHello_11
(JNIEnv*, jobject) {printf("this is C++ print : Java_com_jni_test_JNIDemo_testHello_11\n");
}JNIEXPORT jint JNICALL Java_com_jni_test_JNIDemo_testHello_12
(JNIEnv*, jclass) {printf("this is C++ print : Java_com_jni_test_JNIDemo_testHello_12\n");return 100;
}
5. 生成 dll 文件
使用 C/C++ 实现本地方法生成动态库文件(windows下扩展名为 DDL,linux 下扩展名为 so):
写好了 cpp,就可以生成 dll。右击项目生成/重新生成,就生成了 dll 文件。从控制台输出可看到 dll 的地址
注意:设置为 64位
保存,运行,编译生成 DLL 文件,在工程项目的 release 目录中可以找到。
6. 运行 Java
示例代码 1:
package com.jni.test;public class JNIDemo {public native void testHello_1();public static native int testHello_2();public static void main(String[] args) {try {// System.loadLibrary("JNIPROJECT.dll");System.load("D:\\jni_demo\\src\\com\\jni\\test\\JNIPROJECT.dll");JNIDemo jniDemo =new JNIDemo();jniDemo.testHello_1();int retVal = testHello_2();System.out.println("retVal : " + retVal);}catch (Exception ex) {ex.printStackTrace();}}
}
运行截图:
示例代码 2:
package com.jni.test;public class JNIDemo {public native void testHello_1();public static native int testHello_2();static {// System.loadLibrary("JNIPROJECT.dll");System.load("D:\\jni_demo\\src\\com\\jni\\test\\JNIPROJECT.dll");}public static void main(String[] args) {try {JNIDemo jniDemo =new JNIDemo();jniDemo.testHello_1();int retVal = testHello_2();System.out.println("retVal : " + retVal);}catch (Exception ex) {ex.printStackTrace();}}
}
运行截图:
注意:
- 1、一般在 static 代码块中加载动态链接库
- 2、如果将 DLL 路径加入 PATH 环境变量的时候,eclipse是开着的,那么要关闭 eclipse 再开,让 eclipse 重新读取环境变量
- 3、必须在本类中使用native方法