标准Java的JNI
官方文档:http://docs.oracle.com/javase/1.5.0/docs/guide/jni/spec/jniTOC.html
JAVA通过JNI调用本地方法,而本地方法是以库文件的形式存放的(在Windows上是dll文件形式,在Linux上是so文件形式)。
1. javac TestNative.java
生成TestNative.class
public class TestJNI {
private native int sum(int x,int y); // native的关键字,说明是一个用native代码实现的函数,需要用JNI调用Native代码
public static void main(String[] args) {
TestJNI testJniObject = new TestJNI();
int result = testJniObject.sum(10,10);
System.out.println("result = "+result);
}
static {
System.loadLibrary("testjni"); // 库的扩展名字可以不用写出来,究竟是DLL还是SO,由系统自己判断。
}
}
2. javah -classpath . TestNative
生成TestNative.h,函数名称的命名规则是: Java_类名_函数名
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class TestJNI */
#ifndef _Included_TestJNI
#define _Included_TestJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: TestJNI
* Method: sum
* Signature: (II)I
*/
JNIEXPORT jint JNICALL Java_TestJNI_sum
(JNIEnv *, jobject, jint, jint);
#ifdef __cplusplus
}
#endif
#endif
3. 新建一个TestNative.c实现文件如下:
#include<jni.h>; // 注意必须要包含jni.h头文件,该文件中定义了JNI用到的各种类型,宏定义等
JNIEXPORT jint JNICALL Java_TestJNI_sum
(JNIEnv *ev, jobject obj, jint x, jint y) // JNIEnv可以用来在java和C/C++基础数据类型的转换
{
return x+y;
}
env代表java虚拟机环境,Java传过来的参数需要调用JVM提供的接口来转换成C类型的。
obj代表调用的对象,相当于c++的this。当c函数需要改变调用对象的成员变量时,可以通过操作这个对象来完成。
如果是Windows,可以使用VC建立Win32 Dynamic-Link Library工程,或者使用VC的编译器cl:
cl -I%java_home%\include
-I%java_home%\include\win32
-LD -Fe testjni.dll TestJNI.c
-LD:创建一个动态连接库
-Fe:设置最终可执行文件的存放路径及(或)文件名
如果是Linux,可以使用gcc:
gcc -I/usr/lib/jvm/java-6-sun/include/linux/
-I/usr/lib/jvm/java-6-sun/include/
-fPIC -shared -o libtestjni.so TestJNI.c
-fPIC:表示编译为位置独立的代码,不用此选项的话编译后的代码是位置相关的所以动态载入时是通过代码拷贝的方式来满足不同进程的需要,而不能达到真正代码段共享的目的。
值得注意的是在TestJNI.java中,我们LoadLibrary方法加载的是“testjni”,可我们在Linux中生成的动态库却是libtestjni.so。这是Linux的链接规定的,一个库的必须是:lib+库名+.so。链接的时候只需要提供库名就可以了。
4. 把动态链接库拷贝到TestJNI.class所在的目录中,并运行TestJNI。
JNI的参数类型转换
对于字符串型参数,因为在本地代码中不能直接读取 Java 字符串,而必须将其转换为 C /C++ 字符串或 Unicode。以下是三个我们经常会用到的字符串类型处理的函数:
const char* GetStringUTFChars ( jstring string, jboolean* isCopy )
返回指向字符串 UTF 编码的指针,如果不能创建这个字符数组,返回 null。这个指针在调用 ReleaseStringUTFChar() 函数之前一直有效。
参数:
string Java 字符串对象
isCopy 如果进行拷贝,指向以 JNI_TRUE 填充的 jboolean,否则指向以 JNI_FALSE 填充的 jboolean。
void ReleaseStringUTFChars ( jstring str, const char* chars )
通知虚拟机本地代码不再需要通过 chars 访问 Java 字符串。
参数:
string Java 字符串对象
chars 由 GetStringChars 返回的指针
jstring NewStringUTF ( const char *utf )
返回一个新的 Java 字符串并将 utf 内容拷贝入新串,如果不能创建字符串对象,返回 null。通常在返回值类型为 string 型时用到。
对于数值型参数,在 C/C++ 中可直接使用,其字节宽度如下所示:
Java | C/C++ | 字节数 |
boolean | jboolean | 1 |
byte | jbyte | 1 |
char | jchar | 2 |
short | jshort | 2 |
int | jint | 4 |
long | jlong | 8 |
float | jfloat | 4 |
double | jdouble | 8 |
对于数组型参数,
Java | C/C++ |
boolean[ ] | JbooleanArray |
byte[ ] | JbyteArray |
char[ ] | JcharArray |
short[ ] | JshortArray |
int[ ] | JintArray |
long[ ] | JlongArray |
float[ ] | JfloatArray |
double[ ] | JdoubleArray |
对于上述类型数组,有一组函数与其对应:
xxx * GetXxxArrayElements ( xxxArray array, jboolean *isCopy )
产生一个指向 Java 数组元素的 C 指针。不再需要时,需将此指针传给 ReleaseXxxArrayElemes。
参数:
array 数组对象
isCopy 如果进行拷贝,指向以 JNI_TRUE 填充的 jboolean,否则指向以 JNI_FALSE 填充的 jboolean。
void ReleaseXxxArrayElements ( xxxArray array, xxx *elems, jint mode )
通知虚拟机不再需要从 GetXxxArrayElements 得到的指针。
参数:
array 数组对象
elems 不再需要的指向数组元素的指针
mode:
0 = 在更新数组元素后释放 elems 缓冲器
JNI_COMMIT = 在更新数组元素后不释放 elems 缓冲器
JNI_ABORT = 不更新数组元素释放 elems 缓冲器
xxxArray NewXxxArray ( jsize len )
产生一个新的数组,通常在返回值类型为数组型时用到。
Android中的JNI
同标准Java中的JNI一样,也是通过native关键字来声明JNI调用,并且需要通过静态代码来load JNI的库。
package com.edison;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
public class TestJniActivity extends Activity {
private static final String TAG = "TestJniActivity";
static {
System.loadLibrary("testjni");
}
private native String testJNI();
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
Log.d(TAG, "JNI call: " + testJNI());
}
}
如果你是在Eclipse中开发apk的话,可以在打开终端进入bin\classes目录,然后执行:
javah com.edison.TestJniActivity
将会得到,一个头文件com_edison_TestJniActivity.h:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_edison_TestJniActivity */
#ifndef _Included_com_edison_TestJniActivity
#define _Included_com_edison_TestJniActivity
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_edison_TestJniActivity
* Method: testJNI
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_edison_TestJniActivity_testJNI
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
Android JNI实现中为C/C++提供了两套不同的API:
C实现:我们创建com_edison_TestJniActivity.c文件,内容如下:
#include <jni.h>
#include <utils/Log.h>
/* Native interface, it will be call in java code */
JNIEXPORT jstring JNICALL Java_com_edison_TestJniActivity_testJNI(JNIEnv *env, jobject obj)
{
return (*env)->NewStringUTF(env, "Hello World!");
}
/* This function will be call when the library first be load.
* You can do some init in the libray. return which version jni it support.
*/
jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
void *venv;
LOGI("JNI_OnLoad!");
if ((*vm)->GetEnv(vm, (void**)&venv, JNI_VERSION_1_4) != JNI_OK) {
LOGE("ERROR: GetEnv failed");
return -1;
}
return JNI_VERSION_1_4;
}
C++实现:我们创建com_edison_TestJniActivity.cpp文件,内容如下:
#include <jni.h>
#include <utils/Log.h>
JNIEXPORT jstring JNICALL Java_com_edison_TestJniActivity_testJNI(JNIEnv *env, jobject obj)
{
return env->NewStringUTF("Hello World!");
}
static const char *classPathName = "com/edison/TestJniActivity";
static JNINativeMethod methods[] = {
{"testJNI", "()Ljava/lang/String;", (void*)Java_com_edison_TestJniActivity_testJNI },
};
/*
* Register several native methods for one class.
*/
static int registerNativeMethods(JNIEnv* env, const char* className,
JNINativeMethod* gMethods, int numMethods)
{
jclass clazz;
clazz = env->FindClass(className);
if (clazz == NULL) {
LOGE("Native registration unable to find class '%s'", className);
return JNI_FALSE;
}
if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) {
LOGE("RegisterNatives failed for '%s'", className);
return JNI_FALSE;
}
return JNI_TRUE;
}
/*
* Register native methods for all classes we know about.
*
* returns JNI_TRUE on success.
*/
static int registerNatives(JNIEnv* env)
{
if (!registerNativeMethods(env,
classPathName,
methods,
sizeof(methods) / sizeof(methods[0]))) {
return JNI_FALSE;
}
return JNI_TRUE;
}
typedef union {
JNIEnv* env;
void* venv;
} UnionJNIEnvToVoid;
/* This function will be call when the library first be loaded */
jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
UnionJNIEnvToVoid uenv;
JNIEnv* env = NULL;
LOGI("JNI_OnLoad!");
if (vm->GetEnv((void**)&uenv.venv, JNI_VERSION_1_4) != JNI_OK) {
LOGE("ERROR: GetEnv failed");
return -1;
}
env = uenv.env;;
if (registerNatives(env) != JNI_TRUE) {
LOGE("ERROR: registerNatives failed");
return -1;
}
return JNI_VERSION_1_4;
}
前面已经提到Android系统JNI为C和C++提供了两套不同的API。请仔细对比NewStringUTF( ),GetEnv( )函数,就会发现JNI API的不同。
JNI_OnLoad( )函数JNI规范定义的,当共享库第一次被加载的时候会被回调,这个函数里面可以进行一些初始化工作,比如注册函数映射表,缓存一些变量等,最后返回当前环境所支持的JNI环境。
Android中JNI的函数映射表
Android JNI允许提供一个函数映射表,注册给Jave虚拟机,这样Java虚拟机就可以用函数映射表来调用相应的函数,就可以不必通过函数名来查找需要调用的函数了。
这样你的函数名也可以随便定义了。但是Android系统中,还是推荐用JNI标准的函数名定义的。
Android JNI不能通过标准JNI函数名找到C++实现的共享库中的函数,但是C实现的共享库没有这个问题?
函数映射表描述了函数的参数和返回值,这个数组的类型是JNINativeMethod,定义如下:
typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;
第一个变量name是Java中函数的名字。
第二个变量signature,用字符串是描述了函数的参数和返回值。
第三个变量fnPtr是函数指针,指向C函数。
其中比较难以理解的是第二个参数,例如:
"()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”
Navite访问Java对象
在JNI编程中,Native代码不能对Java虚拟机中对象的内存分布有任何假设。因为Java虚拟机可以根据自己的策略定义自己对象的内存布局,所以JNI规范有如下要求:
1、如果要在Native代码中生成Java对象,则必须调用Java虚拟机的JNI接口来生成;
2、对Java对象的操作,也需要调用Java虚拟机的JNI接口来进行;
3、在Native代码中,通过引用来访问Java对象,对象引用由Java虚拟机的JNI接口返回,比如:NewStringUTF( )函数,用utf8的字符串创建一个Java字符串对象,在Native代码中以jstring类型的引用来访问它。
JNI规范中定义了三种引用——局部引用(Local reference),全局引用(Global reference),弱全局引用(Weak global reference)。
1、局部引用是Native代码中最常用的引用。大部分局部引用都是通过JNI API返回来创建,也可以通过调用NewLocalRef来创建。另外强烈建议Native函数返回值为局部引用。局部引用只在当前调用上下文中有效,所以局部引用不能用Native代码中的静态变量和全局变量来保存。另外时刻要记着Java虚拟机局部引用的个数是有限的,编程的时候强烈建议调用EnsureLocalCapacity和PushLocalFrame来确保Native代码能够获得足够的局部引用数量。
2、全局变量必须要通过NewGlobalRef创建,通过DeleteGlobalRef删除。主要用来缓存Field ID和Method ID。全局引用可以在多线程之间共享其指向的对象。在C语言中以静态变量和全局变量来保存。
3、全局引用和局部引用可以阻止Java虚拟机回收其指向的对象。
4、弱全局引用必须要通过NewWeakGlobalRef创建,通过DeleteWeakGlobalRef销毁。可以在多线程之间共享其指向的对象。在C语言中通过静态变量和全局变量来保持弱全局引用。弱全局引用指向的对象随时都可能会被Java虚拟机回收,所以使用的时候需要时刻注意检查其有效性。弱全局引用经常用来缓存jclass对象。
5、全局引用和弱全局引用可以在多线程中共享其指向对象,但是在多线程编程中需要注意多线程同步。强烈建议在JNI_OnLoad初始化全局引用和弱全局引用,然后在多线程中进行读全局引用和弱全局引用,这样不需要对全局引用和弱全局引用同步(只有读操作不会出现不一致情况)。