Java 中JNI的使用
JNI全称是Java Native Interface
是在JAVA和Native层(包括但不限于C/C++)相互调用的接口规范。
JNI在JAVA1.1中正式推出,在JAVA1.2版本中加入了JNI_OnLoad
,JNI_OnUnload
方法,这两个方法还是很有用的,后面再说。
###JNI基础篇
Java通过JNI调用本地方法的过程大致是
- 写一个Java类,在其中声明对应要调用的native方法,用
native
关键字修饰。 比如private static native int native_newInstance();
- 通过
javah
命令生成java类对应的C/C++头文件。javah -encoding utf-8 -cp src com.young.soundtouch.SoundTouch
- 在C/C++中实现头文件中声明的函数
- 编译C/C++代码为动态库(Windows中的dll,linux(Android)中的so,MAC OSX中的dylib)。
- 在java代码中加载动态库,即可像调用Java方法一样,调用到native函数。
其中第三步在Java1.2中增加了JNI_OnLoad
方法之后有另一种实现方式(后面说)。
javah生成的头文件大致是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
文件开头就是普通的头文件,但是可以发现:
-
包含了jni.h头文件(一般位于
$JAVA_HOME/jd{jdk-version}/include
文目录内)。这是JNI中所有的类型、函数、宏等定义的地方。所以C/C++世界的JNI是由他制定的游戏规则。 -
在类中生命的常量(
static final
)类型会在头文件中以宏的形式出现,这一点还是很方便的。 - 函数的注释还是比较全的,包括了:
- 对应的class
- 对应的java方法名
- 对应java方法的签名
- 方法的声明显得有点奇怪,由以下及部分组成:
JNIEXPORT
这是函数的导出方式- jint 返回值类型(jint由jni.h定义,对应int,下面具体再说吧
- JNICALL 函数的调用方式也就是汇编级别参数的传入方式
- Java_com_young_soundtouch_SoundTouch_native_1getDefaultSampleElementSize —— 超级长的函数名!!!格式是
Java_
+类全名
+_
+ JAVA中声明的native方法名。其中会把包名中的点(.
)替换成下划线(_
),同时为了避免冲突把下划线替换成_1
。 - 方法的参数,上面的这个方法在JAVA的声明中实际上是没有参数的,其中的
JNIENV
顾名思义是JNI环境,和具体的线程绑定。而第二个参数jclass
其实是java中的Class
因为上面是一个static
方法,因此第二个参数是jclass
。如果是一个实例方法则对应第二个参数是jobject
,相当于java中的this
。
下面在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)
所以问题来了:(挖掘机技术哪家强?! o(*≧▽≦)ツ┏━┓ )
怎么得到field 和 method?
通过使用jfieldID和jmethodID: 在JNI中使用类似于放射的方式来进行field和method的操作。JNI中使用jfieldID和jmethodID来表示成员变量和成员方法,获取方式是:
1 2 3 4 |
|
其中最后一个参数是签名。
获取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.
1 2 3 4 5 6 7 8 9 10 11 |
|
- 调用实例方法(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:代码中的定义如下:
1 2 3 4 5 6 7 8 9 10 |
|
所以他有三个字段,分别是
字段 | 含义 |
---|---|
char *name | java class中的native方法名,只需要方法名即可 |
char *signature | 方法签名 |
void *fnPtr | 对应native方法的函数指针 |
于是现在你可以不用导出native函数了,而且可以随意给函数命名,唯一要保证的是参数及返回值的统一。然后需要一个const JNINativeMethod *methods
数组来完成映射工作。
看起来大概是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
|
###实战技巧篇
这里主要是巧用C中的宏来减少重复工作:
####迅速生成全名
1 2 3 4 |
|
比如func(native_1newInstance)
展开成:Java_com_young_soundtouch_SoundTouch_native_1newInstance
即JNI中需要导出的函数名(不过用动态注册方式没太大用了)
constance(AUDIO_FORMAT_PCM16)
展开成com_young_soundtouch_SoundTouch_AUDIO_FORMAT_PCM16
这个着实有用。
而且如果包名改了也可以很方便的适应之。
###安卓的log
1 2 3 4 5 6 7 8 9 10 11 |
|
通过这样的宏定义在打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如下(超级简单):
1 2 |
|
然后在子目录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~~~)
[全文完]
####参考资料
[1] : Oracle java SE documents
[2] : 深入理解Android 卷 1 第二章 ,邓凡平著,机械工业出版社