1.JNI的初步认识
Java语言的执行环境是JVM(JAVA虚拟机),JVM其实是主机环境中的一个进程,每个JVM虚拟机进程在本地环境中都对应有一个JavaVM的结构体,用来记录当前JVM进程的相关环境数据。该结构体在创建Java虚拟机时被返回,在JNI中创建JVM的函数为JNI_CreateJavaVM。
在JNI的双向调用(Java<->C/C++)过程中,JNIEnv是一个很重要的变量,一般在调用nativeXXX方法或者java方法被native回调在JNI方法中第一个参数就会是JNIEnv对象,首先了解一下Java->C/C++的JNIEnv获取,其通过JNI方法的定义将JNIEnv参数加上,而C/C++->Java则一般通过JNIEnv* env = base::android::AttachCurrentThread();获取到JNIEnv对象。
那么JNIEnv是一个什么东西呢?JNIEnv是当前Java线程的执行环境,一个JVM进程对应一个JavaVM结构,而一个JVM进程可能会创建多个线程,每个线程会对应一个JNIEnv结构,该JNIEnv对象会保存在线程的本地存储(TLS,Thread Local Storage)中。因此每个线程的JNIEnv是不同,也不能共享使用的。
TLS科普一下,大家都知道同属于一个进程的多个线程是共享此进程的数据以及内存空间的,那么在一个线程中的修改所有其它线程都能够看得见,这样的优点是做到的快速的数据共享,而缺点是一个线程Game Over,那么整个进程就死掉了,而多个线程共享数据,需要昂贵的同步开销,同时也容易造成同步相关的Bug。那么通过TLS这种机制就可以为线程创建一些仅供线程内部函数访问的变量,其称为线程局部静态变量(static memory local to a thread)。
线程本地存储 (TLS) 是一种方法,给定的多线程进程中的每个线程可以使用这种方法分配用以存储线程特定的数据的位置。
TLS API([TlsAlloc]、[TlsGetValue]、[TlsSetValue] 和 [TlsFree])支持动态绑定(运行时)线程特定的数据。
这种概念在不同平台的实现方式不同,如果需要使用这种机制可以查阅对应平台TLS的实现方式(即TLS API)
JNIEnv结构是一个函数表,本地代码(即native的C/C++代码)通过JNIEnv的函数表来操作Java数据以及调用Java方法,即在本地代码中拿到了JNIEnv结构,就可以在本地代码中调用Java代码了。
其实JNI也就是Java世界与C/C++世界之间沟通的一种方式,这种方式需要依赖JavaVM和JNIEnv结构中定义的函数表,这些函数表负责将Java中的方法调用转换成对本地代码的调用,反之将native的调用转换成Java中的代码调用。
2.JNI的数据类型
既然要native<->java两种语言之间调用,那么必然会涉及到参数,两种语言的参数类型要如何互通,这个工作同样由JNI来完成。这里native的C/C++数据类型长度通常依赖于不同平台会不一致,而java不依赖平台通常同一个类型在不同平台所占空间长度一致,因此JNI机制定义了一些C/C++类型与java中的类型进行对应,取名与java中的类型对应,在java中类型名前加上j,例如java中的int,short对应jni中的类型为jint,jshort。具体类型的对应关系,查阅JDK中的jni.h和jni_md.h,ubuntu jdk路径/usr/lib/jvm/java-7-openjdk-amd64/include,例如:
1)基本类型
jni_md.h
typedef int jint;
#ifdef _LP64 /* 64-bit Solaris */
typedef long jlong;
#else
typedef long long jlong;
#endif
......
jni.h
typedef unsigned char jboolean;
typedef unsigned short jchar;
typedef short jshort;
typedef float jfloat;
typedef double jdouble
......
2)引用类型
class _jobject {};
class _jclass : public _jobject {};
class _jthrowable : public _jobject {};
class _jstring : public _jobject {};
class _jarray : public _jobject {};
class _jbooleanArray : public _jarray {};
class _jbyteArray : public _jarray {};
class _jcharArray : public _jarray {};
class _jshortArray : public _jarray {};
class _jintArray : public _jarray {};
class _jlongArray : public _jarray {};
class _jfloatArray : public _jarray {};
class _jdoubleArray : public _jarray {};
class _jobjectArray : public _jarray {};
.....
3.Java调用本地代码(后面直接用native表示本地代码)的实现
Java代码中要能调用native的接口,必须将native库(windows中为XXX.dll/Linux中为XXX.so)加载到java环境中与java代码连接在一起,这样当java调用native的代码时才能保证找到正确的native方法。java中方法声明时加上native关键字表示这个方法调用的native库中的接口,例如private native long nativeInit()。因此如果想要在java中调用native的接口需要如下几个步骤:
那么这里有一个问题,就是在java中声明的native方法与native库中的C/C++接口的对应是如何实现的呢?
以frameworks/base/services/core/java/com/android/server/am/BatteryStatsService.java中Android原生的JNI方法实现为例看看如何实现Java到native接口的调用。
a.在java代码中加载native方法所在native库,确定我们的native库为libandroid_runtime.so
frameworks/base/core/java/com/android/internal/util/WithFramework.java
System.loadLibrary("android_runtime");
b.在BatteryStatsService.java中声明native方法
private static native int nativeWaitWakeup(int[] outIrqs, String[] outReasons);
c.java中native方法如何实现与native库中的接口对应
frameworks/base/services/core/jni/com_android_server_am_BatteryStatsService.cpp
1> 定义JNINativeMethod method_table,这里写上Java中native方法与native库中接口对应关系
static JNINativeMethod method_table[] = {
{ "nativeWaitWakeup", "([I[Ljava/lang/String;)I", (void*)nativeWaitWakeup },
};
要了解如上方法对应关系可以参考JNINativeMethod结构体的定义,如下:
typedef struct {
const char* name; #Java中的方法名
const char* signature; #方法签名,即函数的参数以及返回值
void* fnPtr; #函数指针,指向C/C++中的函数,这个名字可以和java中的方法名不一致
} JNINativeMethod
其中比较难以理解的是第二个参数,例如
"()V"
"(II)V"
"(Ljava/lang/String;Ljava/lang/String;)V"
实际上这些字符是与函数的参数类型一一对应的。
"()" 中的字符表示参数,后面的则代表返回值。例如"()V" 就表示void Func();
"(II)V" 表示 void Func(int, int);
具体的每一个字符的对应关系如下
字符 Java类型 C类型
V void void
Z jboolean boolean
I jint int
J jlong long
D jdouble double
F jfloat float
B jbyte byte
C jchar char
S jshort short
数组则以"["开始,用两个字符表示
[I jintArray int[]
[F jfloatArray float[]
[B jbyteArray byte[]
[C jcharArray char[]
[S jshortArray short[]
[D jdoubleArray double[]
[J jlongArray long[]
[Z jbooleanArray boolean[]
上面的都是基本类型。如果Java函数的参数是class,则以"L"开头,以";"结尾中间是用"/" 隔开的包及类名。而其对应的C函数名的参数则为jobject. 一个例外是String类,其对应的类为jstring
Ljava/lang/String; String jstring
Ljava/net/Socket; Socket jobject
如果JAVA函数位于一个嵌入类,则用$作为类名间的分隔符。
例如 "(Ljava/lang/String;Landroid/os/FileUtils$FileStatus;)Z"
2> 将java中的native方法进行注册,这里会指明对应的是哪个java类,如com/android/server/am/BatteryStatsService
int register_android_server_BatteryStatsService(JNIEnv *env)
{
return jniRegisterNativeMethods(env, "com/android/server/am/BatteryStatsService",
method_table, NELEM(method_table));
}
3> 在此cpp文件中实现C/C++中的nativeWaitWakeup接口
如上这样在现成的JNI架构里可以这样添加JNI方法,按照上面的方法做一个修改添加一笔JNI调用,如果要了解JNI从无到有的实现过程,
可以写一个JNI的Demo了解下从无到有的NDK开发过程。
4.native调用Java层的方法
Java中有静态和非静态成员以及成员方法,native如果想要访问java中的类或者对象的成员方法以及属性需要通过类似FindClass,GetStaticFieldID等专用与JNI的native方法来获取到java中的属性以及成员方法的jfiledID,jMethodID等,然后才能进行操作。至于这些native方法如何去找到你想要调用的java对象或者类的方法以及属性,需要传入能够确定需要调用的成员方法的信息,如方法的签名(参数类型,返回值类型等)
1) 查找需要调用或操作的属性以及成员方法的ID(如jFieldID,jMethodID)
2)通过GetStaticObjectField等方法获取属性值,通过Call<Type>Method等方法调用java的成员方法
3) 可以通过NewObject等方法在native中创建java中的对象
5.native中对java对象的引用分为局部引用和全局引用,局部引用方法调用完之后被引用的对象可以被GC回收掉,而全局引用则需要native显式告诉java这边可以回收了才会被GC回收,因此在native使用java中的全局引用时一定要在不使用该引用对象时释放掉,否则可能导致内存的泄漏。
1) 局部引用(NewLocalRef):只在上层Java调用本地代码的函数内有效,当本地方法返回时,局部引用自动回收。
2)全局引用(NewGlobalRef):只有显示通知VM时,全局引用才会被回收,否则一直有效,Java的gc不会释放该引用的对象。
3)局部引用只在创建它们的线程里有效,本地代码不能将局部引用在多线程间传递。一个线程想要调用另一个线程创建的局部引用是不被允许的。将一个局部引用保存到全局变量中,然后在其它线程中使用它,这是一种错误的编程
参考资料:
1.http://baike.baidu.com/view/598128.htm
2.https://msdn.microsoft.com/zh-cn/library/6yh4a9k1.aspx
3.http://blog.csdn.net/mr_raptor/article/details/30115113
4.http://blog.csdn.net/bigapple88/article/details/6756204
Java语言的执行环境是JVM(JAVA虚拟机),JVM其实是主机环境中的一个进程,每个JVM虚拟机进程在本地环境中都对应有一个JavaVM的结构体,用来记录当前JVM进程的相关环境数据。该结构体在创建Java虚拟机时被返回,在JNI中创建JVM的函数为JNI_CreateJavaVM。
在JNI的双向调用(Java<->C/C++)过程中,JNIEnv是一个很重要的变量,一般在调用nativeXXX方法或者java方法被native回调在JNI方法中第一个参数就会是JNIEnv对象,首先了解一下Java->C/C++的JNIEnv获取,其通过JNI方法的定义将JNIEnv参数加上,而C/C++->Java则一般通过JNIEnv* env = base::android::AttachCurrentThread();获取到JNIEnv对象。
那么JNIEnv是一个什么东西呢?JNIEnv是当前Java线程的执行环境,一个JVM进程对应一个JavaVM结构,而一个JVM进程可能会创建多个线程,每个线程会对应一个JNIEnv结构,该JNIEnv对象会保存在线程的本地存储(TLS,Thread Local Storage)中。因此每个线程的JNIEnv是不同,也不能共享使用的。
TLS科普一下,大家都知道同属于一个进程的多个线程是共享此进程的数据以及内存空间的,那么在一个线程中的修改所有其它线程都能够看得见,这样的优点是做到的快速的数据共享,而缺点是一个线程Game Over,那么整个进程就死掉了,而多个线程共享数据,需要昂贵的同步开销,同时也容易造成同步相关的Bug。那么通过TLS这种机制就可以为线程创建一些仅供线程内部函数访问的变量,其称为线程局部静态变量(static memory local to a thread)。
线程本地存储 (TLS) 是一种方法,给定的多线程进程中的每个线程可以使用这种方法分配用以存储线程特定的数据的位置。
TLS API([TlsAlloc]、[TlsGetValue]、[TlsSetValue] 和 [TlsFree])支持动态绑定(运行时)线程特定的数据。
这种概念在不同平台的实现方式不同,如果需要使用这种机制可以查阅对应平台TLS的实现方式(即TLS API)
JNIEnv结构是一个函数表,本地代码(即native的C/C++代码)通过JNIEnv的函数表来操作Java数据以及调用Java方法,即在本地代码中拿到了JNIEnv结构,就可以在本地代码中调用Java代码了。
其实JNI也就是Java世界与C/C++世界之间沟通的一种方式,这种方式需要依赖JavaVM和JNIEnv结构中定义的函数表,这些函数表负责将Java中的方法调用转换成对本地代码的调用,反之将native的调用转换成Java中的代码调用。
2.JNI的数据类型
既然要native<->java两种语言之间调用,那么必然会涉及到参数,两种语言的参数类型要如何互通,这个工作同样由JNI来完成。这里native的C/C++数据类型长度通常依赖于不同平台会不一致,而java不依赖平台通常同一个类型在不同平台所占空间长度一致,因此JNI机制定义了一些C/C++类型与java中的类型进行对应,取名与java中的类型对应,在java中类型名前加上j,例如java中的int,short对应jni中的类型为jint,jshort。具体类型的对应关系,查阅JDK中的jni.h和jni_md.h,ubuntu jdk路径/usr/lib/jvm/java-7-openjdk-amd64/include,例如:
1)基本类型
jni_md.h
typedef int jint;
#ifdef _LP64 /* 64-bit Solaris */
typedef long jlong;
#else
typedef long long jlong;
#endif
......
jni.h
typedef unsigned char jboolean;
typedef unsigned short jchar;
typedef short jshort;
typedef float jfloat;
typedef double jdouble
......
2)引用类型
class _jobject {};
class _jclass : public _jobject {};
class _jthrowable : public _jobject {};
class _jstring : public _jobject {};
class _jarray : public _jobject {};
class _jbooleanArray : public _jarray {};
class _jbyteArray : public _jarray {};
class _jcharArray : public _jarray {};
class _jshortArray : public _jarray {};
class _jintArray : public _jarray {};
class _jlongArray : public _jarray {};
class _jfloatArray : public _jarray {};
class _jdoubleArray : public _jarray {};
class _jobjectArray : public _jarray {};
.....
3.Java调用本地代码(后面直接用native表示本地代码)的实现
Java代码中要能调用native的接口,必须将native库(windows中为XXX.dll/Linux中为XXX.so)加载到java环境中与java代码连接在一起,这样当java调用native的代码时才能保证找到正确的native方法。java中方法声明时加上native关键字表示这个方法调用的native库中的接口,例如private native long nativeInit()。因此如果想要在java中调用native的接口需要如下几个步骤:
1) 编写Java代码,在Java代码中通过System.loadLibrary将native库代码加载到java环境中,一般将native库的加载写在静态块中,因为静态块仅在类加载的时候执行一次
2) 在java中声明native方法,例如private native long nativeInit();
相信大家有可能注意到Android系统中的native方法的声明和abstract的方法类似,没有具体的实现,按理说这个方法应该还要加上abstract关键字才对啊,这里应该可以查阅一下发现native和abstract两个关键字是冲突的,意思就是说在方法的声明中它两是站在一个位置上的,但是这个位置仅能站一个,它两有自己不同的功能,abstract将方法的具体实现移交给子类,native关键字将方法的具体实现移交给native库。这里换个方向想如果同时将方法声明为native和abstract,那么编译器就会confused了,不知道到底要谁来具体实现了,或者子类和native都实现了,但是调用的时候怎么调用也是一个问题,所以这两个关键字同一方法只能用一个。
3) 在java代码中像调用普通java方法一样调用2)中声明的native方法
那么这里有一个问题,就是在java中声明的native方法与native库中的C/C++接口的对应是如何实现的呢?
以frameworks/base/services/core/java/com/android/server/am/BatteryStatsService.java中Android原生的JNI方法实现为例看看如何实现Java到native接口的调用。
a.在java代码中加载native方法所在native库,确定我们的native库为libandroid_runtime.so
frameworks/base/core/java/com/android/internal/util/WithFramework.java
System.loadLibrary("android_runtime");
b.在BatteryStatsService.java中声明native方法
private static native int nativeWaitWakeup(int[] outIrqs, String[] outReasons);
c.java中native方法如何实现与native库中的接口对应
frameworks/base/services/core/jni/com_android_server_am_BatteryStatsService.cpp
1> 定义JNINativeMethod method_table,这里写上Java中native方法与native库中接口对应关系
static JNINativeMethod method_table[] = {
{ "nativeWaitWakeup", "([I[Ljava/lang/String;)I", (void*)nativeWaitWakeup },
};
要了解如上方法对应关系可以参考JNINativeMethod结构体的定义,如下:
typedef struct {
const char* name; #Java中的方法名
const char* signature; #方法签名,即函数的参数以及返回值
void* fnPtr; #函数指针,指向C/C++中的函数,这个名字可以和java中的方法名不一致
} JNINativeMethod
其中比较难以理解的是第二个参数,例如
"()V"
"(II)V"
"(Ljava/lang/String;Ljava/lang/String;)V"
实际上这些字符是与函数的参数类型一一对应的。
"()" 中的字符表示参数,后面的则代表返回值。例如"()V" 就表示void Func();
"(II)V" 表示 void Func(int, int);
具体的每一个字符的对应关系如下
字符 Java类型 C类型
V void void
Z jboolean boolean
I jint int
J jlong long
D jdouble double
F jfloat float
B jbyte byte
C jchar char
S jshort short
数组则以"["开始,用两个字符表示
[I jintArray int[]
[F jfloatArray float[]
[B jbyteArray byte[]
[C jcharArray char[]
[S jshortArray short[]
[D jdoubleArray double[]
[J jlongArray long[]
[Z jbooleanArray boolean[]
上面的都是基本类型。如果Java函数的参数是class,则以"L"开头,以";"结尾中间是用"/" 隔开的包及类名。而其对应的C函数名的参数则为jobject. 一个例外是String类,其对应的类为jstring
Ljava/lang/String; String jstring
Ljava/net/Socket; Socket jobject
如果JAVA函数位于一个嵌入类,则用$作为类名间的分隔符。
例如 "(Ljava/lang/String;Landroid/os/FileUtils$FileStatus;)Z"
2> 将java中的native方法进行注册,这里会指明对应的是哪个java类,如com/android/server/am/BatteryStatsService
int register_android_server_BatteryStatsService(JNIEnv *env)
{
return jniRegisterNativeMethods(env, "com/android/server/am/BatteryStatsService",
method_table, NELEM(method_table));
}
3> 在此cpp文件中实现C/C++中的nativeWaitWakeup接口
如上这样在现成的JNI架构里可以这样添加JNI方法,按照上面的方法做一个修改添加一笔JNI调用,如果要了解JNI从无到有的实现过程,
可以写一个JNI的Demo了解下从无到有的NDK开发过程。
4.native调用Java层的方法
Java中有静态和非静态成员以及成员方法,native如果想要访问java中的类或者对象的成员方法以及属性需要通过类似FindClass,GetStaticFieldID等专用与JNI的native方法来获取到java中的属性以及成员方法的jfiledID,jMethodID等,然后才能进行操作。至于这些native方法如何去找到你想要调用的java对象或者类的方法以及属性,需要传入能够确定需要调用的成员方法的信息,如方法的签名(参数类型,返回值类型等)
1) 查找需要调用或操作的属性以及成员方法的ID(如jFieldID,jMethodID)
2)通过GetStaticObjectField等方法获取属性值,通过Call<Type>Method等方法调用java的成员方法
3) 可以通过NewObject等方法在native中创建java中的对象
5.native中对java对象的引用分为局部引用和全局引用,局部引用方法调用完之后被引用的对象可以被GC回收掉,而全局引用则需要native显式告诉java这边可以回收了才会被GC回收,因此在native使用java中的全局引用时一定要在不使用该引用对象时释放掉,否则可能导致内存的泄漏。
1) 局部引用(NewLocalRef):只在上层Java调用本地代码的函数内有效,当本地方法返回时,局部引用自动回收。
2)全局引用(NewGlobalRef):只有显示通知VM时,全局引用才会被回收,否则一直有效,Java的gc不会释放该引用的对象。
3)局部引用只在创建它们的线程里有效,本地代码不能将局部引用在多线程间传递。一个线程想要调用另一个线程创建的局部引用是不被允许的。将一个局部引用保存到全局变量中,然后在其它线程中使用它,这是一种错误的编程
参考资料:
1.http://baike.baidu.com/view/598128.htm
2.https://msdn.microsoft.com/zh-cn/library/6yh4a9k1.aspx
3.http://blog.csdn.net/mr_raptor/article/details/30115113
4.http://blog.csdn.net/bigapple88/article/details/6756204