推荐 原创 视频 Java开发 iOS开发 前端开发 JavaScript开发 Android开发 PHP开发 数据库 开发工具 Python开发 Kotlin开发 Ruby开发 .NET开发 服务器运维 开放平台 架构师 大数据 云计算 人工智能 开发语言 其它开发
Lambda在线 > 中软睿达 > Java 中 JNI 的使用 ( 下 )

Java 中 JNI 的使用 ( 下 )

中软睿达 2018-10-18


数组的操作


数组是一个很常用的数据类型,在但是在 JNI 中并不能直接操作 jni 数组(比如 jshortArray、jfloatArray)。使用方法是:


  1. 获取数组长度:jsize GetArrayLength(jarray array)

  2. 创建新数组: ArrayType New Array(jsize length);

  3. 通过JNI数组获取一个C/C++数组: * Get ArrayElements(jshortArray array, jboolean *isCopy)

  4. 指定原数组的范围获取一个C/C++数组(该方法只针对于原始数据数组,不包括Object数组):void Get ArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize len, NativeType *buf);

  5. 设置数组元素:void Set ArrayRegion(jshortArray array, jsize start, jsize len,const *buf)。again,如果是Object数组需要使用:void SetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index, jobject value);

  6. 使用完之后,释放数组:void Release ArrayElements(jshortArray array, jshort *elems, jint mode)


有点要说明的:


1、上面的3中的 isCopy:当你调用 getArrayElements 时 JVM(Runtime)可以直接返回数组的原始指针,或者是 copy 一份,返回给你,这是由 JVM 决定的。所以 isCopy 就是用来记录这个的。他的值是 JNI_TURE 或者 JNI_FALSE。


2、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中提供了三种类型的引用:


  1. Local Reference:即本地引用。在JNI层的函数,所有非全局引用对象都是Local Reference, 它包括函数调用是传入的jobject和JNI成函数创建的jobject。Local Reference的特点是一旦JNI层的函数返回,这些jobject就可能被垃圾回收。

  2. Glocal Reference:全局引用,这些对象不会主动释放,永远不会被垃圾回收。

  3. Weak Glocal Reference:弱全局引用,一种特殊的Global Reference,在运行过程中有可能被垃圾回收。所以使用之前需要使用jboolean IsSameObject(jobject obj1, jobject obj2)判断它是否已被回收。


Glocal Reference:

  • 创建:jobject NewGlobalRef(jobject lobj);

  • 释放: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 生成的头文件中定义的方法,这样有几个弊端:


  1. 函数名太长。很长,相当长。

  2. 函数会被导出,也就谁说可以在动态库的导出函数表里面找到这些函数。这将有利于别人对动态库的逆向工程,因此带来安全问题。


现在有了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;


所以他有三个字段,分别是


Java 中 JNI 的使用 ( 下 )


于是现在你可以不用导出 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 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 不同在于:


  1. 调用 Android.mkmingling 的 ${CWD} 并不是 Android.ml 所在的目录。所以 Android.mk 中有一个变量 LOCAL_PATH := $(call my-dir) 来记录当前 Android.mk 所在的目录。

  2. 同时还会把所有的 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 这样就不会在动态库中导出不必要的函数了。


附录签名



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~~~)

版权声明:本站内容全部来自于腾讯微信公众号,属第三方自助推荐收录。《Java 中 JNI 的使用 ( 下 )》的版权归原作者「中软睿达」所有,文章言论观点不代表Lambda在线的观点, Lambda在线不承担任何法律责任。如需删除可联系QQ:516101458

文章来源: 阅读原文

相关阅读

关注中软睿达微信公众号

中软睿达微信公众号:bj-zrrd

中软睿达

手机扫描上方二维码即可关注中软睿达微信公众号

中软睿达最新文章

精品公众号随机推荐