JNI 官方文档:https://docs.oracle.com/en/java/javase/19/docs/specs/jni/index.html
JNI官方文档(中文):https://blog.csdn.net/yishifu/article/details/52180448
NDK 官方文档:https://developer.android.google.cn/training/articles/perf-jni
Android JNI学习(1、2、3、4、5 ):https://www.jianshu.com/p/b4431ac22ec2
JNI 开发总结:https://cloud.tencent.com/developer/article/1356493
Android JNI 原理分析:http://gityuan.com/2016/05/28/android-jni/
JNI 全称是Java Native Interface,为Java本地接口,并提供了若干的 API 连接Java层与Native层。通俗来说,JNI 相当于一个桥梁,实现了 Java 和 C++ 之间互相访问调用。
在 Android 进行 JNI 开发时,可能会遇到 couldn't find "xxx.so" 问题,或者内存泄漏问题,或者令人头疼的 JNI 底层崩溃问题。Java 层如何调用 Native 方法?Java 方法的参数如何传递给 Native层?而 Native 层又如何反射调用 Java 方法?这些问题在本文将得到答案,带着问题去阅读会事半功倍,接下来我们开始全方位介绍与最佳代码实践。
关于ndk编译脚本:https://blog.csdn.net/u011686167/article/details/106458899
关于JNI开发规范:https://blog.csdn.net/u011686167/article/details/81784979
JNI 最常见的两个作用:
- 从 Java 程序调用 C/C++
- 从 C/C++ 程序调用Java代码。
JNI 是一个双向的接口:通过 JNI 可以在 Java 代码中访问 Native 模块,还可以在 Native 代码中嵌入一个 JVM 并通过 JNI 访问运行于其中的 Java 模块。JNI 将 JVM 与 Native 模块联系起来,从而实现了 Java 代码与 Native 代码的互访
在 Android 提供 System.loadLibrary() 或者 System.load() 来加载库。示例如下:
static {
try {
System.loadLibrary("hello");
} catch (UnsatisfiedLinkError error) {
Log.e(TAG, "load library error=" + error.getMessage());
}
}
需要注意的是,如果 .so 动态库或 .a 静态库不存在时,会抛出 couldn't find "libxxx.so" 异常:
如果期待加载的是 64bit 的库,却加载到 32bit 的,会报错如下:
java.lang.UnsatisfiedLinkError: dlopen failed: "xxx.so" is 32-bit instead of 64-bit
System.loadLibrary() 内部调用 Runtime.getRuntime().loadLibrary0(),源码如下:
synchronized void loadLibrary0(ClassLoader loader, String libraryName) {
if (loader != null) {
// 1、调用classLoader查找库
String filename = loader.findLibrary(libraryName);
if (filename == null) {
throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
System.mapLibraryName(libraryName) + "\"");
}
// 2、调用native方法来加载
String error = nativeLoad(filename, loader);
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
return;
}
// 3、拼接完整库名,比如由hello拼接成libhello.so
String filename = System.mapLibraryName(libraryName);
Listcandidates = new ArrayList ();
String lastError = null;
for (String directory : getLibPaths()) {
String candidate = directory + filename;
candidates.add(candidate);
if (IoUtils.canOpenReadOnly(candidate)) {
// 4、调用native方法来加载
String error = nativeLoad(candidate, loader);
if (error == null) {
return; // 加载library成功
}
lastError = error;
}
}
if (lastError != null) {
throw new UnsatisfiedLinkError(lastError);
}
throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates);
}
这里的 nativeLoad() 属于 runtime 底层的 jni 方法,接着调用 art/runtime/java_vm_ext.cc 的load_NativeLibrary(),最终调用 dlopen() 来打开 so 库或 a 库。 调用过程如下图:
java 层调用带 native 关键字的 JNI 方法,需要注册 java层 与 native层 的对应关系,有静态注册和动态注册两种方式。
示例:java 层声明的函数名为 hello 的 JNI 方法 :private native void hello(int num);
静态注册的示例(如果是c++文件(.cpp/.cc/.cxx),需要加extern "C"关键字):
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT void JNICALL Java_com_frank_ffmpeg_FFmpegCmd_hello(JNIEnv *env, jclass thiz, jint num) {
}
#ifdef __cplusplus
}
#endif
如果觉得每个JNI方法都这样写比较麻烦,我们可以写个宏定义:
#define FFMPEG_FUNC(RETURN_TYPE, FUNC_NAME, ...) \
JNIEXPORT RETURN_TYPE JNICALL Java_com_frank_ffmpeg_FFmpegCmd_ ## FUNC_NAME \
(JNIEnv *env, jclass thiz, ##__VA_ARGS__)\
动态注册的示例:
JNINativeMethod nativeMethods[] {
{"hello", "(I)V", (void *)"native_hello"},
{"world", "(J)V", (void *)"native_world"}
};
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv *env = NULL;
vm->GetEnv((void **)&env, JNI_VERSION_1_6);
jclass clazz = env->FindClass("com/frank/ffmpeg/handler/FFmpegHandler");
int numMethods = sizeof(nativeMethods) / sizeof(nativeMethods[0]);
// 注册本地方法到函数表
env->RegisterNatives(clazz, nativeMethods, numMethods);
env->DeleteLocalRef(clazz);
return JNI_VERSION_1_6;
}
JNINativeMethod的结构体位于jni.h,定义如下:
typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;
JNI方法前两个参数分别是 JNIEnv 和 jclass,其中 JNIEnv 是上下文环境,而 jclass 是类的实例对象。其他参数为带 j 开头,比如 jint、jstring。
JNI 提供局部引用和全局引用,还有全局弱引用。顾名思义,局部引用的作用域为局部,在本地方法返回时被GC主动回收,通过如下方法创建:jobject NewLocalRef(JNIEnv *env, jobject ref);
全局引用的作用域为全局,不会被GC回收,需要手动释放引用资源,否则导致内存泄漏。全局引用的创建与释放如下:
// new global reference
jobject NewGlobalRef(JNIEnv *env, jobject obj);
// delete global reference
void DeleteGlobalRef(JNIEnv *env, jobject globalRef);
全局弱引用与全局引用不同的是,它可以被GC回收。另外,它关联到虚引用,用于感知何时被GC回收。全局弱引用的创建与释放如下:
// new weak global reference
jweak NewWeakGlobalRef(JNIEnv *env, jobject obj);
// delete weak global reference
void DeleteWeakGlobalRef(JNIEnv *env, jweak obj);
JNI提供检测异常、抛出异常和清除异常。使用ExceptionOccurred()进行异常检测。在检测到异常后通过ThrowNew()抛出异常,方法如下:
jint ThrowNew(JNIEnv *env, jclass clazz, const char *message);
最后是清除异常,使用ExceptionClear()。完整的示例代码如下:
// 检测异常
if (env->ExceptionOccurred() != NULL) {
// 抛出异常
jclass clazz = env->FindClass("java/lang/NullPointerException");
env->ThrowNew(clazz, "This is a null pointer...");
// 清除异常
env->ExceptionClear();
}
- 调用 Java 函数: JNIEnv 代表 Java 运行环境,可以使用 JNIEnv 调用 Java 中的代码。
- 操作 Java 对象:Java 对象传入 JNI 层就是 Jobject 对象, 需要使用 JNIEnv 来操作这个 Java 对象。
关于 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 方法报告异常。
因为 jni 扮演了 Java 和 C、C++ 之间的 "桥梁" 作用,所以 jni 也有自己的数据类型,用来连接 Java 和 C、C++ 之间的相互转换和调用
JNI 类型包括
基本类型包括:jint、jbyte、jshort、jlong、jdouble、jboolean、jchar、jfloat 等。
如下图所示: jni、java 基本数据类型对比
引用类型的父类是 jobject,包含 jclass、jstring、jarray,而 jarray 又包含各种基本类型对应的数组。层级关系如下图所示:
"Java不同的引用类型" 在 "JNI当中也有对应的引用类型" 如上图。当在 C 语言中使用时,所有的 JNI 引用类型都被定义为 jobject 类型。typedef jobject jclass;
使用场景为反射Java变量或Java方法。
函数签名由参数类型和返回值组成,用参数个数、参数类型和返回值来区分同名方法,即解决方法重载问题。JNI 和 java 基本类型对应的签名如下:
至于引用对象类型,使用类的全限定名作为签名。 比如String对应签名为Ljava/lang/String;
引用类型则为 " L + 该类型类描述符 ",数组 为 " [ + 其类型的域描述符 " 。
int[ ] 描述符为 [I
float[ ] 描述符为 [FString[ ] 描述符为 [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
jni 的常用方法和类型:https://blog.csdn.net/qinjuning/article/details/7595104
我们该如何反射调用java方法呢?首先要获取类的实例对象,然后获取方法id,最后根据方法id来调用方法。获取类的实例对象有两种方式:GetObjectClass()和FindClass(),示例如下:
void get_class(JNIEnv *env, jobject object) {
// 通过类的实例获取
jclass clazz = env->GetObjectClass(object);
// 通过类加载器查找指定的类
jclass claxx = env->FindClass("java/lang/NullPointerException");
}
我们可以通过GetObjectRefType()获取引用类型,包括如下引用类型:
JNIInvalidRefType = 0
JNILocalRefType = 1
JNIGlobalRefType = 2
JNIWeakGlobalRefType = 3
如果要判断是否属于某个类的实例,方法如下:
jboolean IsInstanceOf(JNIEnv *env, jobject obj, jclass clazz);
如果要判断两个对象是否相同,方法如下:
jboolean IsSameObject(JNIEnv *env, jobject ref1, jobject ref2);
反射Java的变量分为两步,首先获取变量的jfieldID,然后获取/设置变量值,示例如下:
jclass clazz = env->GetObjectClass(object);
jfieldID fieldId = env->GetFieldID(clazz, "level", "I");
env->SetIntField(object, fieldId, 8);
反射Java的方法也分为两步,首先获取方法的jmethodID,然后调用方法,示例如下:
jclass clazz = env->GetObjectClass(object);
jmethodID methodId = env->GetMethodID(clazz, "setLevel", "(I)V");
env->CallIntMethod(object, methodId, 8);
如果要读取来自Java层的字符串,可以调用GetStringUTFChars(),使用完毕不要忘记释放资源,否则导致内存泄漏。示例代码如下:
void get_string_from_java(JNIEnv *env, jobject object, jstring jstr) {
const char *str = env->GetStringUTFChars(jstr, JNI_FALSE);
int len = env->GetStringUTFLength(jstr);
printf("from java str=%s, len=%d", str, len);
env->ReleaseStringUTFChars(jstr, str);
}
如果要返回字符串给Java层,使用NewStringUTF(),示例代码如下:
jstring set_string_to_java(JNIEnv *env, jobject object) {
const char *str = "hello, world";
return env->NewStringUTF(str);
}
如果要读取来自Java层的数组,可以调用GetXxxArrayElements()。也可以调用GetXxxArrayRegion(),该函数比较灵活,支持指定数组区间。还有没有第三种方式呢?答案是有的,可以调用GetPrimitiveArrayCritical()获取原始数组,采用内存映射实现。示例代码如下:
void get_array_from_java(JNIEnv *env, jobject object, jintArray jarray) {
int len = env->GetArrayLength(jarray);
// 1、使用GetIntArrayElements,使用完释放内存
jint *array = env->GetIntArrayElements(jarray, JNI_FALSE);
for (int i = 0; i < len; ++i) {
printf("from java array=%d", array[i]);
}
env->ReleaseIntArrayElements(jarray, array, JNI_ABORT);
// 2、使用GetIntArrayRegion,内部会释放内存
env->GetIntArrayRegion(jarray, 0, len, array);
// 3、使用GetPrimitiveArrayCritical获取原始数组
array = (jint*) env->GetPrimitiveArrayCritical(jarray, JNI_FALSE);
}
如果要返回数组给Java层,先创建JNI数组,然后把数据拷贝给数据,示例代码如下:
jintArray set_array_to_java(JNIEnv *env, jobject object) {
jint data[] = {1, 2, 3, 4, 5, 6};
int size = sizeof(data)/sizeof(data[0]);
jintArray array = env->NewIntArray(size);
env->SetIntArrayRegion(array, 0, size, data);
return array;
}
我们可以在本地方法访问java.nio的DirectBuffer。先科普一下,DirectBuffer为堆外内存,实现零拷贝,提升Java层与native层的传输效率。而HeapBuffer为堆内存,在native层多一次拷贝,效率相对低。两者对比如下:
内存位置 | 使用场景 | 优点 | 缺点 | |
DirectBuffer | 堆外内存 | 调用频率高、数据多 | 零拷贝,效率高 | 创建耗时 |
HeapBuffer | 堆内存 | 调用频率低、数据少 | 创建相对快 | 存在拷贝,效率低 |
DirectBuffer在Native层的使用,可以在Java层创建,然后把对象传递到Native层。获取到内存地址后,把数据拷贝给DirectBuffer。整个过程如下:
void copy_to_directBuffer(JNIEnv *env, jobject object, jobject buf) {
uint8_t data[] = {1, 2, 3, 4, 5, 6};
uint8_t *buf_addr = (uint8_t *) (env->GetDirectBufferAddress(buf));
int buf_size = env->GetDirectBufferCapacity(buf);
int data_size = sizeof(data)/sizeof(data[0]);
int size = data_size > buf_size ? buf_size : data_size;
memcpy(buf_addr, data, size);
}
上面提及到反射调用Java方法,如果要根据method去获取对应id,API方法如下:
jmethodID FromReflectedMethod(JNIEnv *env, jobject method);
相反地,如果要根据id去获取对应method,API方法如下:
jobject ToReflectedMethod(JNIEnv *env, jclass cls, jmethodID methodID, jboolean isStatic);
当 Android 的 VM(Virtual Machine) 执行到 System.loadLibrary() 函数时,首先会去执行 C 组件里的 JNI_OnLoad() 函数。它的用途有二:
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,"","()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" ;
}
//获得得构造函数Id
jmethodID list_costruct = env->GetMethodID(list_cls , "","()V"); //创建一个Arraylist集合对象
jobject list_obj = env->NewObject(list_cls , list_costruct);//或得Arraylist类中的 add()方法ID,其方法原型为: boolean add(Object object) ;
jmethodID list_add = env->GetMethodID(list_cls,"add","(Ljava/lang/Object;)Z");
//获得Student类引用
jclass stu_cls = env->FindClass("Lcom/feixun/jni/Student;");
//获得该类型的构造函数 函数名为返回类型必须为 void 即 V
jmethodID stu_costruct = env->GetMethodID(stu_cls , "", "(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);//执行Arraylist类实例的add方法,添加一个stu对象
env->CallBooleanMethod(list_obj , list_add , stu_obj);
}return list_obj ; //返回对象集合
//获得jfieldID 以及 该字段的初始值
jfieldID nameFieldId ;//获得Java层该对象实例的类引用,即HelloJNI类引用
jclass cls = env->GetObjectClass(obj);//获得属性句柄
nameFieldId = env->GetFieldID(cls , "name" , "Ljava/lang/String;");
if(nameFieldId == NULL)
{
cout << " 没有得到name 的句柄Id \n;" ;
}// 获得该属性的值
jstring javaNameStr = (jstring)env->GetObjectField(obj ,nameFieldId);//转换为 char *类型
const char * c_javaName = env->GetStringUTFChars(javaNameStr , NULL);
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); // 设置该字段的值
jstring str = NULL;
jclass clz = env->FindClass("cc/androidos/jni/JniTest");
//获取clz的构造函数并生成一个对象
jmethodID ctor = env->GetMethodID(clz, "", "()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);
}
示例代码:
//获得Java类实例
jclass cls = env->GetObjectClass(obj);//或得该回调方法句柄
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);
//或得Student类引用
jclass stu_cls = env->GetObjectClass(obj_stu);
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;"); // 获得属性IDjint 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 ;
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;
}
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;
}
调用System.loadLibrary()时,系统在加载库成功后,会回调JNI_OnLoad(JavaVM *vm, void *reserved)。带有JavaVM参数可以保存为全局变量,返回值为JNI版本号。示例代码如下:
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
javaVM = vm;
return JNI_VERSION_1_6;
}
当类加载器包含的本地库已经被垃圾回收器回收了,虚拟机会回调JNI_OnUnload()方法。 在该方法回调时,我们可以做内存清理工作。
2.1 创建JavaVM
创建JavaVM需要传入JavaVM指针、JNIEnv指针和VM参数。当前线程变成主线程,得到的env作为主线程的上下文环境。创建JVM方法为JNI_CreateJavaVM()。
2.2 关联JavaVM
当工作线程需要使用env时,必须先调用AttachCurrentThread()方法来关联JVM,因为env是线程私有的上下文环境。如果已经关联,不执行任何操作。需要注意的是,一个本地线程不能关联两个JVM。
2.3 脱离JavaVM
当使用完env时,调用DetachCurrentThread()方法来脱离JVM。
2.4 销毁JavaVM
当不再需要使用JavaVM时,调用DestroyJavaVM()方法用于卸载JVM和清除内存。任何线程,不管有没关联JVM,都可以调用该方法。
JavaVM 的完整使用过程如下:
void callJVM() {
JNIEnv *env = nullptr;
JavaVM *jvm = nullptr;
// 1、创建jvm
JNI_CreateJavaVM(&jvm, &env, nullptr);
// 2、关联jvm
jvm->AttachCurrentThread(&env, nullptr);
// 3、do something with env
// 4、脱离jvm
jvm->DetachCurrentThread();
// 5、销毁jvm
jvm->DestroyJavaVM();
}
做JNI/NDK开发时,经常遇到堆栈崩溃问题,只有一堆杂乱地址,实在让人摸不着头脑。堆栈信息包括:ABI架构、pid进程号、出错信号、崩溃原因、寄存器状态、堆栈地址。空指针引起的崩溃如下图所示:
字符串编码不同而引起的崩溃如下:
Abort message: 'JNI DETECTED ERROR IN APPLICATION: input is not valid Modified UTF-8: illegal continuation byte 0
string: '�'
input: '0xf4'
遇到native层崩溃时,我们可用ndk-stack查看堆栈地址,命令如下:
adb logcat | ndk-stack -sym xxx/libxxx.so
// 0x12345678为堆栈地址,替换为实际崩溃地址
aarch64-linux-android-addr2line -e libxxx.so 0x12345678
objdump可以用-syms查看符号表,命令如下:
objdump -syms libxxx.so
readelf是用来查看ELF文件的工具,ELF(Executable and Linkable Format)是一种可执行、可重定向的二进制目标文件。命令参数选项如下:
使用readelf -d libxxx.so查看其依赖库:
使用readelf -s libxxx.so查看其符号表:
参考: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 方法的类
- 利用 idea 生成 .h 头文件。
- 在 vs 中创建一个动态链接库应用程序的解决方案
- 在解决方案中创建 C++ 文件,实现头文件中的方法
- 生成 动态 链接库
- 回到 idea,运行 java 项目,排错重复以上步骤直到运行成功
实现一个简单的 testHello_1() 函数 和 静态的 testHell0_2() 函数,在 C++ 中实现 testHello_1() 和 testHell0_2()。
注意:java 代码都不要放到默认包下(就是不写 package 语句就会放到默认包),默认包下的方法在其他地方都不能调用!!
步骤如下:
图示:
示例代码:
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,
为什么需要注册?其实就是给 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
/* 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
头文件 说明:
在类中 声明 的常量(static final
)类型会在头文件中以宏的形式出现,这一点还是很方便的。
JNIEXPORT
这是函数的导出方式JNIENV
顾名思义是 JNI 环境,和具体的线程绑定。而第二个参数 jclass
其实是 java 中的 Class
因为上面是一个 static
方法,因此第二个参数是jclass
。如果是一个实例方法则对应第二个参数是 jobject
,相当于 java 中的 this
。2.2 一键生成头文件
头文件可以使用命令行生成(见参考文献),或者熟悉格式后自己手写。但是如果希望能够随便点一下就生成头文件,于是,找到了一种 用idea工具生成头文件的方法,那就是 External Tools。External Tools 其实就是将手动输入的命令存下来,本质也是运行 javah,后面跟着配置参数,这些参数存在 External Tools,避免每次手动输入。
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 是什么 ?
打个比方类似如下: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:
Get Field(jobject , jfieldID);即可获得对应的field,其中field的类型是type,可以是上面类型所叙述的任何一种。 GetStatic Field(jobject , jfieldID);同1,唯一的区别是用来获取静态成员。
set:
- void Set
Field(jobject obj, jfieldID fieldID, val) - void SetStatic
Field(jclass clazz, jfieldID fieldID, value);
成员方法:
调用方法自然要把方法的参数传递进去,JNI中实现了三种参数的传递方式:
Call
其中...
是C中的可变长参数,类似于printf
那样,可以传递不定长个参数。于是你可以把java方法需要的参数在这里面传递进去。
Call
其中的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
哎!这个我知道可以说两句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;
CallMethod(jobject obj, jmethodID methodID, ...);
调用一个具有
类型返回值的方法。 CallMethodV(jobject obj, jmethodID methodID, va_list args);
CallMethodA(jobject obj, jmethodID methodID, const jvalue * args)
CallStaticMethod(jobject obj, jmethodID methodID, ...);
CallStaticMethodV(jobject obj, jmethodID methodID, va_list args);
CallStaticMethodA(jobject obj, jmethodID methodID, const jvalue * args)
CallNonvirtualMethod(jobject obj, jclass clazz, jmethodID methodID, ...)
CallNonvirtualMethodV(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, va_list args);
CallNonvirtualMethodA(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, const jvalue *args);
#### 数组的操作
数组是一个很常用的数据类型,在但是在JNI中并不能直接操作jni数组(比如jshortArray,jfloatArray)。使用方法是:
jsize GetArrayLength(jarray array)
ArrayType NewArray(jsize length);
* GetArrayElements(jshortArray array, jboolean *isCopy)
void GetArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize len, NativeType *buf);
void SetArrayRegion(jshortArray array, jsize start, jsize len,const *buf)
。again,如果是Object数组需要使用:void SetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index, jobject value);
void ReleaseArrayElements(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 中提供了三种类型的引用:
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 (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(&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
#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
####关于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的不同在于:
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~~~)
接下来打开 Visual studio 2019,新建动态链接库: JniProject
填写 项目名,项目所在目录:
创建完成后再添加类:
设置项目包含目录
本来我是按照这篇文章复制jni.h等文件的,但是一直报错“找不到 源 文件 jni.h”。搞来搞去总是不成,后来才发现,我在vs2017直接复制,jni.h并没有到C++项目目录下,而是仍然在原来的目录里,这与java的ide很不同啊。虽然被这个问题搞到差点摔桌子,但我转念一想,在原来的目录下就还不错啊,省得我复制来复制去。于是刷刷刷设置了包含路径
如果不想设置 包含目录,可以直接把文件( 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
然后在 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;
}
使用 C/C++ 实现本地方法生成动态库文件(windows下扩展名为 DDL,linux 下扩展名为 so):
写好了 cpp,就可以生成 dll。右击项目生成/重新生成,就生成了 dll 文件。从控制台输出可看到 dll 的地址
注意:设置为 64位
保存,运行,编译生成 DLL 文件,在工程项目的 release 目录中可以找到。
示例代码 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();
}
}
}
运行截图:
注意:
Program: $JDKPath$\bin\javah.exe
Auguments: -classpath . -jni -o $ModuleFileDir$/src/main/jni/$Prompt$ $FileClass$
Working directory: $ModuleFileDir$\src\main\java
以上为配置Javah过程,到这就配置好了,注意上面几个配置你可以理解为固定配置,其实是一些路径定义,可以不用管的,
在 File ---> Settings ---> appearance ---> system settings ---> Android SDK,下查看 NDK 安装配置情况,如果没有下载配置 NDK ,以及相关的包,对应下载相关的安装包。
打开 sdkManager下载 CMake 和 LLDB
下载安装好后,可以在 File - Project Structure 的 SDK Location 下查看对应的安装配置路径情况,
:https://blog.csdn.net/fengruoying93/article/details/124222174
打开 Android Studio,新建一个 Native C++ 项目。示例:JNIDemo
项目创建成功后,开始创建 jni 文件夹:src 右键 ---> New ---> Folder ---> JNI Fold
创建 JNI 类
public class JNITest {
static {
System.loadLibrary("JniLib");
}
public native String getString();
}
生成 .h 文件
方法 1:
配置 Anroid Studio 外部工具,一劳永逸,往后无需命令行,File ---> Setting ---> Tools ---> External Tools ---> “+” 进入页面
Program:$JDKPath$\bin\javah.exe
Parameters:-classpath . -jni -d $ModuleFileDir$\src\main\jni $FileClass$
Working directory:$ModuleFileDir$\src\main\Java
注释:
-classpath classes 指明类所在的位置
-jni com.jni.jnitest.JNITest 类的绝对路径
-d 产生的.h文件放到指定目录下;
配置成功如图:
开始生成 .h文件,选中 JNI类 右键 ---> New ---> External Tools ---> javah,如图:
成功后如图:
方法 二
右键拖动JNI类所在的包的路径到Terminal,自动切换到该目录下
javac 编译生成 class 文件( 生成class文件的方法有很多,这里提供一种):java JNIText.java
右键拖动 java 文件夹到 Terminal,自动切换到该目录下
必须在包名外使用 javah 命令,编译生成.h文件,把.h文件移动到jni文件夹(生成.h文件后可以删除class文件)如图:
示例命令:javah -d jni -classpath ./java com.example.myapplication.hello
创建文件 JniLib.cpp 、Android.mk、Application.mk
在 jni 目录下分别创建并编写 JniLib.cpp、Android.mk、Application.mk 这三个文件
复制.h文件内容到 JniLib.cpp 并修改,如下(此文件为JNI内容文件):
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class com_jni_jnitest_JNITest */
/*
* Class: com_jni_jnitest_JNITest
* Method: getString
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_jni_jnitest_JNITest_getString
(JNIEnv * env, jobject jobject){
return (*env).NewStringUTF("成功调用JNI内容");
}
Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := JniLib
LOCAL_SRC_FILES =: JniLib.cpp
include $(BUILD_SHARED_LIBRARY)
Application.mk
APP_MODULES := JniLib
APP_ABI := all
修改 app下的 build.gradle 文件
ndk{
moduleName "JniLib"
// abiFilters "armeabi", "armeabi-v7a", "x86" //输出指定的三种abi体系下的so库
}
sourceSets.main{
jni.srcDirs = []
jniLibs.srcDir "src/main/libs"
}
项目下的gradle.properties文件(如果没有此文件,自己新建一个)添加代码:
android.useDeprecatedNdk=true
执行 ndk-build
此处我用的是配置好的工具来执行,和 javah 外部工具 一样的步骤
选中JNI类右键->New->External Tools->ndk-build,结果如图:
调用 so。示例代码:
package com.jni.jnitest;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
Button button;
TextView tv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button = findViewById(R.id.button);
tv = findViewById(R.id.tv);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
tv.setText("结果:"+ new JNITest().getString());
}
});
}
}
错误: 编码GBK的不可映射字符 ( https://blog.csdn.net/talenter111/article/details/53418999 )
解决方法: 应该使用-encoding参数指明编码方式,如:
javah -jni -encoding UTF-8 com.example.XXXX.XXXX.MainActivity
/**************静态方法**********************/
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_calc_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
/*************************************************************/
JNIEXPORT void JNICALL Java_com_example_jni_1demo_MainActivity_javaToC(JNIEnv *env, jobject obj)
{
// 获取 类
jclass fdClass = env->FindClass("com/example/jni_demo/MainActivity");
// 获取 普通方法id
jmethodID _jmethodID = env->GetMethodID(fdClass, "_method", "()V");
// 获取 静态方法id
jmethodID _staticjmethodID = env->GetStaticMethodID(fdClass, "_staticMethod", "()V");
// 调用 java中 的 普通方法
env->CallVoidMethod(obj, _jmethodID);
// 调用 java中 的 静态方法
env->CallStaticVoidMethod(fdClass, _staticjmethodID);
}
/************************* 动态注册 nativate 方法 ********************************/
JNINativeMethod nativeMethod[] = { // 方法数组映射
// 定义数组,用于绑定 java方法 和 C方法的 关系
{"addMethod", "(FF)F", (void*)my_add}, // java中方法名,方法签名,C++中方法名
{"subMethod", "(FF)F", (void*)my_sub},
{"mulMethod", "(FF)F", (void*)my_mul},
{"divMethod", "(FF)F", (void*)my_div}
};
/************************* 实现 JNI_OnLoad 动态注册方法 *******************************/
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
JNIEnv* env;
if(vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK)
{
return JNI_ERR;
}
// 获取 java native 方法对应的 类
jclass fdClass = env->FindClass("com/example/calc/MainActivity");
// 注册 java 层 native 方法
jint retVal = env->RegisterNatives(fdClass, nativeMethod, sizeof(nativeMethod)/sizeof(nativeMethod[0]));
if(retVal != JNI_OK)
{
// 注册失败返回 -1
return JNI_ERR;
}
return JNI_VERSION_1_6; //必须返回一个版本号
}
JNI 动态注册和静态注册的详解:https://blog.csdn.net/bill_xiao/article/details/89095020
Android:JNI 动态注册和静态注册的详解(附android studio实例):https://blog.csdn.net/qq_37858386/article/details/103765111
Android Studio3.0开发JNI流程------JNI静态注册和动态注册(多个类的native动态注册-经典篇):https://blog.csdn.net/cloverjf/article/details/78878814
Android JNI 函数注册的两种方式(静态注册/动态注册):https://www.jianshu.com/p/1d6ec5068d05
JNI_动态注册_静态注册.zip : https://pan.baidu.com/s/1wpTYA9euSdPqE1Z2bA_BHA 提取码: 7h97
安装完jdk后就可以在安装目录的 include 目录中找到 jni.h 头文件(示例:C:\Program Files (x86)\Java\jdk1.8.0_261\include)
jni.h 头文件,其实就是 API 文档,里面有一些方法声明、结构体、等图示:
如果是普通函数,第二个参数是 jobject
如果是静态函数,第二个参数是 jclass