数组的操作
数组是一个很常用的数据类型,在但是在 JNI 中并不能直接操作 jni 数组(比如 jshortArray、jfloatArray)。使用方法是:
jsize GetArrayLength(jarray array)
ArrayType New<PrimitiveType>Array(jsize length);
<type>* Get<type>ArrayElements(jshortArray array, jboolean *isCopy)
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)
有点要说明的:
1、上面的3中的 isCopy:当你调用 getArrayElements 时 JVM(Runtime)可以直接返回数组的原始指针,或者是 copy 一份,返回给你,这是由 JVM 决定的。所以 isCopy 就是用来记录这个的。他的值是 JNI_TURE
或者 JNI_FALSE
。
2、6释放数组。 一定要释放你所获得数组 。其中有一个 mode
参数,其有三个可选值,分别表示:
比如上面有个方法传了一个 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 是 Java1.2 中新增加的方法,对应的还有一个 JNI_OnUnload,分别是动态库被 JVM 加载、卸载的时候调用的函数。有点类似于 Windows 里的 DllMain。
前面提到的实现对应 native 的方法是实现 javah 生成的头文件中定义的方法,这样有几个弊端:
现在有了JNI_OnLoad,情况好多了。你不光能在其中完成动态注册 native 函数的工作还可以完成一些初始化工作。Java 对应的有了 jint RegisterNatives(jclass clazz, const JNINativeMethod *methods,jint nMethods)
函数。参数分别是:
JNINativeMethod:代码中的定义如下
/* * used in RegisterNatives to describe native method name, signature, * and function pointer. */ typedef struct { char *name; char *signature; void *fnPtr; } JNINativeMethod;
所以他有三个字段,分别是
于是现在你可以不用导出 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
这个着实有用。
而且如果包名改了也可以很方便的适应之。
//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++ 里面需要手动清除内存,因此我的解决方案是定义一个 map,给每个实例一个 id,用 id 把 Java 中的对象和 native 中的对象绑定起来。在 Java 层定义一个 release
方法,用来释放本地的对象。 本地的 KEY-对象 映射 static std::map<int, SoundTouchWrapper*> sInstancePool;
因为安卓的约定是把本地代码放到 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.基本类型都对应一个大写字母,如下:
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~~~)