jni小结

Jni(java native interface)是一种技术,它让java调用其他语言的代码,比如C/C++的代码.SUN的官方网站上可以下载到相关的文档,看文档总是比较好的,给出链接先:

JVM TOOL DOC:

http://docs.oracle.com/javase/8/docs/platform/jvmti/jvmti.html

JNI DOC:

http://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/jniTOC.html

 

基本原理是将native的代码编译成动态库,java中加载此动态库,java使用jni调用动态库中的函数,动态库中的函数也可以通过jni调用到java中的函数.

那么要实现这样的功能,需要解决:

1.java中怎样加载动态库

2.Java调用动态库的函数和动态库调用java函数问题

3.参数传递和返回值问题

4.C/C++运行的线程环境问题

5.异常的抛出与捕获问题

6.垃圾回收问题

 

 

1.java中怎样加载动态库

Java中加载动态库是调用函数:

System.loadLibrary("jnitest");

这个函数会在项目目录下查找动态库libjnitest.so(linux系统)jnitest.dll(windows系统),开发android应用程序时,会在项目目录/libs下面查找.

加载时机是在调用库中的函数之前,通常的做法是在类的static域中加载,例如:

class JavaTestClass{

    static{

System.loadLibrary("jnitest");

}

    //other method...

};

 

2.Java调用动态库的函数和动态库调用java函数问题

在调用之前,需要完成java函数到动态库函数的一对一的映射才行.

2.1 动态库函数映射到java

通过这种映射之后,java可以调用到动态库的函数.JVM提供了两种映射方法,静态映射和动态映射.无论静态映射还是动态映射,java中函数声明都是一样的:

private native void jni_api();

2.1.1静态映射方法:

通常不用这种方法,因为它不好使,不过还是了解一下吧.在定义好java中的函数原型后,就可以使用工具javah通过类文件生成相应的C/C++头文件了.

javah -o output.h packetname.classname

packetname.classname文件中的native函数原型转换成C/C++中的函数原型,结果保存在output.h.例如:

javah -o jnitest.h com.test.JniTest

注意,执行的目录是在包的目录的上一层,不然可能找不到类文件.有了这个头文件,生活就变得容易了,C/C++代码只要包含这个头文件,然后实现函数就OK.

这个背后的机制其实比较简单,java类中调用native标志的函数时,JVM会在一个映射表中查找映射函数,如果没有函数就按照一定的规则变换此函数原型,然后跟据变换后的原型调用动态中的函数.个人觉得这个方法比较麻烦,主要是函数名比较丑...,简单略过吧,我们主要使用动态映射方法.

2.1.2动态映射方法

上面有提到JVM会查找一个映射表...,我们可以将java中的函数原型和动态库中的函数原型通通注册到那个表中,JVM不就可以顺利找到java的另一半了吗?JVM确实提供了这样的功能.准确地说,JVM提供了一些操作函数,同时JVM提供一个线程相关的结构体指针估且称为env,env包含了那组操作函数并且提供了包装输出了接口,env输出的接口称为jni api,我们就是通过这些jni apiJVM打交道的,当然有时候也需要使用JVM本身提供的函数.

jni.h中定义了JNI API,来看一下和方法注册相关的:

typedef struct {

    char *name; //函数在java中的名字

    char *signature;//函数参数和返回值签名

    void *fnPtr; //native代码中的函数指针

} JNINativeMethod;

//注册到映射表

jint RegisterNatives(jclass clazz, const JNINativeMethod *methods,jint nMethods);

jint UnregisterNatives(jclass clazz);//取消注册

clazzjava中声明的native函数所在的类的类型对像,注意不是类的实例,methods是映射结构体指针,nMethods是映射结构体的个数.通常的做法是先定义一个JNINativeMethod类型的数组并初始化,然后使用RegisterNatives一次性注册.

看起来非常简单,但是有个问题,jclass怎么来的呢,还有那个签名怎么回事?先来看jclass:

 jclass FindClass(const char *name);

参数namejava中类的名字,是全名,包含包的名字.

对于FindClassRegisterNatives等许多的jni函数,你会看到别人的代码都是这么用的:

C里面:  (*env)->FindClass(env, "java/lang/String")

C++:  env->FindClass("java/lang/String")

注意,所有的jni函数都是这么用的

签名是为了确定java中函数的参数和返回值,毕境上面只给出了name,怎么得到签名呢,一个是自己手动去写,这个需要熟悉类型到签名的转换,另一个就是使用工具javap,来看个例子:

javap -s com.test.JniTest

public void play(java.lang.String);

    Signature: (Ljava/lang/String;)V

很简单吧,不需要去记那么多规则,好吧,那么动态注册映射的时机呢,记得java函数中是在static中加载了动态库,那动态库可以在哪加载呢?

/* Defined by native libraries. */

JNIEXPORT jint JNICALL

JNI_OnLoad(JavaVM *vm, void *reserved);

JNIEXPORT void JNICALL

JNI_OnUnload(JavaVM *vm, void *reserved);

没错,就是他们,一个在加载后调用,一个在卸载后调用,动态库跟据自己的需要实现这两个函数.在加载动态库后,JVM会去查找动态库有没有提供JNI_OnLoad函数,如果没有JVM会抱怨一声然后正常加载,如果有就会调用它.参数vmJVM的指针,可以从中获得env,从而干许多事.不管干了什么,这个函数最后要返回JNI的版本值,比如:

#define JNI_VERSION_1_1 0x00010001

#define JNI_VERSION_1_2 0x00010002

#define JNI_VERSION_1_4 0x00010004

#define JNI_VERSION_1_6 0x00010006

不返回的话,也没什么问题,只是JVM会报个错,然后整个进程都不好了.

2.2 java中的函数映射到动态库中

通过这种映射之后,动态库可以调用到java的函数.这个没有什么静态映射的方法了,是通过查找到java函数的句柄,然后使用jni api来使这个函数执行.查找句柄的jni api:

jmethodID GetMethodID(jclass clazz, const char *name,const char *sig);

其中clazz是通过FindClass返回的,namejava中函数的名字,sigjava函数的签名.找到这个句柄之后,就可以调用jni api来执行这个函数了,env根据返回值的不同提供了许多函数,比如:

jobject CallObjectMethod(jobject obj, jmethodID methodID, ...);

jboolean CallBooleanMethod(jobject obj,jmethodID methodID, ...);

void CallVoidMethod(jobject obj, jmethodID methodID, ...);

...

除了这些之外,还因为参数提供方法不一样分成了VA版本:

void CallVoidMethodV(jobject obj, jmethodID methodID,va_list args);

void CallVoidMethodA(jobject obj, jmethodID methodID,const jvalue * args);

动态库除了可以访问java函数之外,还可以访问成员变量,同样是先取得句柄,然后调用jni api执行:

jfieldID GetFieldID(jclass clazz, const char *name,const char *sig);

jobject GetObjectField(jobject obj, jfieldID fieldID);

void SetObjectField(jobject obj, jfieldID fieldID, jobject val);

....

很多动态库保存全局结构体的方法是,java中定义一个int的成员变量,然后在native代码中申请到空间后,写入到此成员变量,这样当java调用其他的函数时,在函数中就可以通过env获取到全局结构体的指针了.

3.参数传递和返回值问题

看到上面的jnit,jobject是不是觉得头晕,这得来介绍下java中的类型和jni中的类型,以及C/C++的类型:

java

wide(byte)

jni

wide(byte)

C/C++

byte

1

jbyte

1

char

int

4

jint

4

int

long

8

jlong

8

long long

char

2

jchar

2

unsigned short

boolean

1

jboolean

1

unsigned char

short

2

jshort

2

short

float

4

jfloat

4

float

double

8

jdouble

8

double

这是基本类型的对应表,这是啥意思呢,其实就是java中一个类型,比如char到了jni api时会转换成jchar,jcharC/C++中其实是unsigned short类型.其中黄色标记的类型长度是操作系统依赖的,这里写的都是linux.

除了基本类型的转换之外,还有数组和对像的类型.

java

jni

C/C++

object

jobject

类的指针

java.lang.Class

jclass

java.lang.Throwable

jthrowable

java.lang.String

jstring

boolean[]

jbooleanArray

byte[]

jbyteArray

char[]

jcharArray

short[]

jshortArray

int[]

jintArray

long[]

jlongArray

float[]

jfloatArray

double[]

jdoubleArray

object[]

jobjectArray

对于这种非基本类型的类型,C/C++中访问的时候需要依靠env的帮助,跟据类型不同,这些jni api也不一样:

// 创建一个jbyteArray

jbyteArray NewByteArray(jsize len)

//从jbyteArray获取jbyte数组指针

jbyte * GetByteArrayElements(jbyteArray array, jboolean *isCopy);

//释放数组空间

void ReleaseByteArrayElements(jbyteArray array,jbyte *elems,jint mode);

......

参数isCopy用来指示在创建jbyte数组的时候是否进行了复制,如果进行了复制,*isCopy=JNI_TRUE,如果没有进行复制返回的是jbyteArray本身的数据地址那么*isCopy=JNI_FALSE.可以传入0作为参数,忽略其返回值.参数mode指示这个函数的操作方式:

Primitive Array Release Modes

mode 

actions 

copy back the content and free the elems buffer 

JNI_COMMIT 

copy back the content but do not free the elems buffer 

JNI_ABORT 

free the buffer without copying back the possible changes

 

4.C/C++运行的线程环境问题

java中可以有许多线程,当在这些线程中调用到动态库的函数时,其环境信息也会传递下来,JVM提供了一个结构体env来保存这些信息供动态库使用.怎么获得这个结构体呢,普通的被调用函数不用说了,参数中就有env结构体指针.要自己获取env结构体的地方有两个,一个是在JNI_Onload,一个是在动态库自己创建的线程中.

JNI_Onload,可以通过JVM的接口获取:

jint GetEnv(JavaVM *vm, void **env, jint version);

在自已创建的函数中:

jint AttachCurrentThread(JavaVM *vm, void **p_env, void *thr_args);

jint DetachCurrentThread(JavaVM *vm);

或许有人会比较奇怪,为什么在自创线程中不能调用GetEnv得到env,其实是因为所有的线程都必须先AttachCurrentThread之后才能调用GetEnv,java中创建的线程都调用了AttachCurrentThread,这个应该是在API中调用好了的.

java 创建一个JVM进程,JVM创建若干线程,所以JVM和线程无关,所有线程中共享一个JVM,我们可以保存起来使用,env则和线程有关,必须AttachCurrentThread之后才能使用,用完后也必须释放掉.

5.异常的抛出与捕获问题

C/C++可以抛出异常给java代码:

jint Throw(jthrowable obj);

jint ThrowNew(jclass clazz,const char *message);//clazz应该是FindClass("java/lang/Throwable")返回的,message是给异常携带的错误信息.

C/C++可以捕获java抛出的异常:

jthrowable ExceptionOccurred(JNIEnv *env);//如果没有异常返回NULL,否则返回异常.

void ExceptionDescribe(JNIEnv *env);//打印当前异常信息

void ExceptionClear(JNIEnv *env);//清除当前异常信息

 

6.垃圾回收问题

JVM会不定时回收"垃圾",所谓的"垃圾"是指引用为0的对像,java中每次赋值后都会增加左值的引用减少右值的引用,但是在C/C++中不会,所以这会引起问题,试想,java将一个对像传递给了native层代码,然后这个对像在java中引用减少为0,那么它就会当成垃圾回收掉,这么可怕的事情怎么避免呢,当然是在native代码中也增加一次引用了.

jni中一共提供了三种引用类型,全局引用GlobalRef,本地引用LocalRef,弱全局引用WeadGlobalRef.

全局引用只能手动释放,不释放其对像会永久存在,除非进程被清理,它的创建与释放:

jobject NewGlobalRef(jobject obj);

void DeleteGlobalRef(jobject globalRef);

本地引用包括传为参数传进来的jobject和在函数中定义的jobject,本地引用会在函数返回时自动释放,也可以自己手动释放:

jobject NewLocalRef(JNIEnv *env, jobject ref);

void DeleteLocalRef(jobject localRef);

弱全局引用指向的对像可能被垃圾回收:

jweak NewWeakGlobalRef(JNIEnv *env, jobject obj);

void DeleteWeakGlobalRef(JNIEnv *env, jweak obj);

IsSameObject(jweak,NULL);//判断弱全局引用指向的对像是否被回收

 

注意一点,垃圾回收的时间没法确定,如果想快速释放一个对像,还是需要调用java层进行主动释放空间.

 

7.一个简单的例子:

com.test.JniTest.java:

package com.test;

public class JniTest {

static{

System.loadLibrary("jnitest");

}

public native static void jni_api();

public static void main(String args[]){

try{

jni_api();

}catch(Throwable e){

System.out.println("------------------");

System.out.println(e.getMessage());

}

}

}


jni/jnitest.h(使用命令javah -o jnitest.h com.test.JniTest生成

/* DO NOT EDIT THIS FILE - it is machine generated */

#include <jni.h>

/* Header for class com_test_JniTest */

 

#ifndef _Included_com_test_JniTest

#define _Included_com_test_JniTest

#ifdef __cplusplus

extern "C" {

#endif

/*

 * Class:     com_test_JniTest

 * Method:    jni_api

 * Signature: ()V

 */

JNIEXPORT void JNICALL Java_com_test_JniTest_jni_1api

  (JNIEnv *, jobject);

 

#ifdef __cplusplus

}

#endif

#endif


jni/jnitest.cpp:

#include <jni.h>

#include "jnitest.h"

#include <stdio.h>

 

JNIEXPORT void JNICALL Java_com_test_JniTest_jni_1api

  (JNIEnv *env, jobject thiz){

    printf("----jni test---\n");

    jclass c=env->FindClass("java/lang/Throwable");

    printf("----find class 0x%x---\n",c);

    if(c)

        env->ThrowNew(c,"hello,this is exception");

    else

        printf("can not find class!\n");

}

 

编译生成动态库:

gcc -shared -o ../libjnitest.so jnitest.cpp -I/usr/local/jdk1.7.0_45/include -I/usr/local/jdk1.7.0_45/include/linux

测试运行:

------------------

hello,this is exception

----jni test---

----find class 0x8f0821a8---

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值