Java Native Interface Java本地调用,
Java程序中的函数和native语言写的函数(C或C++)可以实现相互调用。
使用JNI技术的原因
1 Java诞生之前,很多软件都是用native语言写的,在Java中只要使用JNI技术调用他们就可以。
2 Java是一次编译,到处运行,原因是Java虚拟机在不同的平台有不同的虚拟机,虚拟机屏蔽了底层硬件差异,Java虚拟机是使用native语言写的,而虚拟机运行在具体平台上,所以虚拟机无法做到与平台无关,有了JNI就可以屏蔽不同操作平台之间的差异了。
3 扩展Java虚拟机的功能,能通过JNI操控硬件
4 class文件安全性低,c编译完就直接是机器码
学习JNI的实例-MediaScanner
MediaScanner的功能是扫描多媒体文件,的到歌曲时长和作者等信息,然后将他们存到媒体数据库中供其他应用程序使用
Java层的MediaScanner分析:
Java层面对应的是MediaScanner,在JNI层面对应的是libmedia_jni.so,native层对应的是libmedia.so,JNI层必须实现为动态库的形式,这样Java虚拟机才能加载他并调用他的函数。
public class MediaScanner
{
static {
//1 加载JNI对应的库 media_jni是名字加载的时候会拓展成libmedia_jni.so windows平台会拓展成media_jni.dll
System.loadLibrary("media_jni");
native_init();
}
…
//2 声明一个native函数 native是Java的关键字 表示由JNI层完成
private native void processFile(String path, String mimeType, MediaScannerClient client);
…}
JNI层的MediaScanner分析
//native_init的JNI层体现
static void android_media_MediaScanner_native_init (JNIEnv *env)
{
jclass clazz;
clazz = env->FindClass("android/media/MediaScanner");
if (clazz == NULL) {
jniThrowException(env, "java/lang/RuntimeException", "Can't find android/media/MediaScanner");
return;
}
fields.context = env->GetFieldID(clazz, "mNativeContext", "I");
if (fields.context == NULL) {
jniThrowException(env, "java/lang/RuntimeException", "Can't find MediaScanner.mNativeContext");
return;
}
}
//processFile的JNI层体现
static void android_media_MediaScanner_processFile(JNIEnv *env, jobject thiz, jstring path, jstring mimeType, jobject client)
{
MediaScanner *mp = (MediaScanner *)env->GetIntField(thiz, fields.context);
……
const char *pathStr = env->GetStringUTFChars(path, NULL);
……
if (mimeType) {
env->ReleaseStringUTFChars(mimeType, mimeTypeStr);
}
}
Native_init函数位于frameworks/base/media/java 中的android.media.MediaScanner里,Native语言中“.”有着特殊的含义,所以采用”_”, android_media_MediaScanner正好对应Java下的完整包名,所以Java层的native_init找到了android_media_MediaScanner_native_init
JNI的注册:
“注册“就是将Java层的native函数与JNI层对应的函数关联起来,关联起来就能顺利的在Java层调用native函数时顺利转到JNI层对应的函数执行了。
静态注册
根据函数名建立起来Java函数和JNI函数的关联关系。
当Java层调用native_init函数的时候,就会从对应的JNI库中寻找java_android_media_MediaScanner_native_linit函数,如果没有就会报错,有就会为native_init和java_android_media_MediaScanner_native_linit函数建立一个关联的关系,其实就是保存JNI层函数的函数指针,以后再调用native_init方法时就直接使用这个指针就可以了,这项工作是虚拟机来完成的。
注:如果Java函数名中有“_”,转成JNI就变成了“_l”,Java中每有一个native的函数,就有对应的包名_类名格式的JNI函数名。
例如:
Java层:
private native void processFile(String path, String mimeType, MediaScannerClient client);
JNI层:
static void android_media_MediaScanner_processFile(JNIEnv *env, jobject thiz, jstring path, jstring mimeType, jobject client){ }
java代码生成.class文件后执行:
javac -h -output packagename.classname,会生成一个packagename_class.h的文件
MediaScanner对应的JNI层头文件就是android_media_MediaScanner.h
代码中:native_init变成了:
JNIEXPORT void JNICALL java_android_media_MediaScanner_native_linit,
processFile函数变成了:
JNIEXPORT void JNICALL java_android_media_MediaScanner_processFile
缺点:
要编译所有声明了native函数的类,每个class文件都要使用javac -h生成一个.h的头文件
Javac -h生成的JNI函数名字比较长书写起来不方便
初次调用native函数要根据名字建立联系,花费的时间比较长。
动态注册
有一种结构来保存native函数和JNI 函数的对应关系 就是---JNINativeMethod,他的结构:
typedef struct{
//Java中native函数的名字 不用携带包名的路径 例如“native_init”
const char* name;
//java函数的签名信息 用字符串表示 是参数类型和返回值类型的组合
const char* signature;
//
void* fnPtr; //JNI层对应的函数指针 是void 类型
}
这是android_media_MediaScanner.cpp函数里,定义了一个javaNative函数的数组,里面就是Java中的native函数,以其中一个processfile函数举例子,遵循 JNINativeMethod结构:
{
//函数名
"processFile",
//函数签名
"(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
//JNI层对应的函数指针
(void*)android_media_MediaScanner_processFile
}
AndroidRuntime方法里提供了注册方法,调用registerNativeMethods方法,如下是AndroidRunTime.cpp的方法:
int AndroidRuntime::registerNativeMethods(JNIEnv* env,
const char* className, const JNINativeMethod* gMethods, int numMethods)
{
return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}
jniRegisterNativeMethods的方法是在JNIHelp.c里。
实际是调用JNIEnv的RegisterNatives方法,实现关联关系,由于JavaNative的函数不是全路径名,所以要传入classname指定类名
动态注册是在System.loadLibrary时就会加载JNI库,到JNI库中寻找JNI_OnLoad函数,如果有,动态注册的工作就是在这里完成的。
总结,env只想指向一个结构体,classname是对应的Java类名,调用JNIEnv的RegisterNatives函数注册关联关系。
数据类型:
在Java中调用native函数传递的参数是Java类型,到JNI中就变成了JNI的数据类型,对应关系如下:
基本数据类型:
boolean -> jboolean byte -> jbyte 同理… double -> jdouble
引用数据类型:
class -> jclass Throwable -> jthrowable Object -> jobject
除了Java中基本数据类型的数组、Class、String和Throwable外,其余所有Java对象的数据类型在JNI中都用jobject表示。例如processFile函数里的参数,JNI层后三个参数对应了Java里的三个参数。
->private native void processFile(String path, String mimeType, MediaScannerClient client);
->static void android_media_MediaScanner_processFile(JNIEnv *env, jobject thiz, jstring path, jstring mimeType, jobject client)
但是JNI层却有五个参数,第一个参数JEnv和第二个参数jobject,jobject则是表示的是类对象,表示的是在哪个类对象里调用此函数,静态方法就用jclass表示,非静态方法则是jobject,JNIEnv呢?
JNIEnv
JNIEnv是一个与线程相关的代表JNI环境的结构体,实际的作用就是提供了一些JNI系统函数,可以调用Java函数,操作jobject.
线程相关就是说线程1有一个JNIEnv,线程2也有一个JNIEnv,在线程1中不能使用2的结构体,JNIEnv是在native函数转换为JNI层函数的时候由Java虚拟机传进来的,调用传入JNIEnv就不会出错,但是当后台线程收到一个网络消息,又需要从native层函数回调给Java层的时候,JNIEnv从何而来呢?
前面提到的JNI_OnLoad函数中,第一个参数是虚拟机在JNI层的代表,虚拟机只有一个
Jinit JNI_OnLoad(JavaVm* vm , void* reserved){ }
调用JavaVM的AttachCurrentThread函数就可以得到这个线程的JNIEnv结构体,这样就可以在后台线程中回调Java函数了,后台线程退出前,调用JavaVM的DetachCurrentThread函数来释放对应的资源。
通过JNIEnv操作jobject
Java的引用类型大多数都是jobject,Java对象又是由成员变量和成员函数组成,所以操作jobject的本质就是操作这些成员变量和成员函数。
在JNI中,用jfieldID和jmethodID表示Java类的成员变量和成员函数,可以通过JNIEnv的两个函数得到:
//jclass代表Java类,name代表成员函数或成员变量的名字,sig表示这个函数和变量的签名信息。
jfieldID GetFieldID(jclass clazz,const char*name, const char *sig);
jmethodID GetMethodID(jclass clazz, const char*name,const char *sig);
//1 先找到mediaScannerClient类在JNI层对应的jclass实例
Jclass mediaScannerClientInterface= env->FindClass("android/media/MediaScannerClient");
//2 取出类mediaScannerClient的函数的methodID
mScanFileMethodID = env->GetMethodID(mediaScannerClientInterface, "scanFile",
"(Ljava/lang/String;JJ)V");
将对应的函数的jmethodID保存起来,保存为MyMediaScannerClient的成员变量,因为每次操作jobject前都要去查询jmethodID或jfieldID,将会影响程序的运行效率,所以在初始化的时候就会取出这些ID保存起来以后使用。继续看,
virtual bool scanFile(const char* path, long long lastModified, long long fileSize)
{
jstring pathStr;
if ((pathStr = mEnv->NewStringUTF(path)) == NULL) return false;
// CallVoidMethod 第一个参数是MyMediaScannerClient的类对象 第二个参数是
//ScanFile函数的jmethodID,后面的三个参数则是scanFile的参数
mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified, fileSize);
mEnv->DeleteLocalRef(pathStr);
return (!mEnv->ExceptionCheck());
}
通过JNIEnv输出CallVoidMethod方法,把jobject和jMethodID以及对应的参数传进去,JNI层就能调用Java对象函数了。
jstring
NewString:可以从Native字符串中得到一个jstring对象。
NewStringUTF:可以根据Native的一个UTF-8,字符串得到一个jstring对象。
上面两个函数将本地字符串转成了Java的String对象,JNIEnv还提供了GetStringChars函数和GetStringUTFChars函数,他们可以将Java String字符串转换成本地字符串,前者得到的是unicode字符串,后者得到的是UTF-8字符串。调用完上面的几个函数都要调用ReleaseStringChars和ReleaseStringUTFChars来释放资源,否则会造成JVM泄露。
JNI签名
动态注册JNI函数时,会有一个结构体,里面包含签名信息,这是因为:
Java支持函数的重载,可以定义同名但不同参数的函数,但仅仅根据函数名是没有办法找到具体函数的,为了解决这一问题,JNI技术中就将参数类型和返回值类型的组合作为了一个函数的签名信息,以便于顺利精确的找到对应的Java函数。
ProcessFile函数为例:
{"processFile", "(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
(void *)android_media_MediaScanner_processFile
}
括号内是参数类型的标识,最右边是返回值类型的标识,void对应的是v,当参数的类型是引用类型时,格式是“L包名;”,其中的.换成/,函数的签名稍微写错一个标点符号就会导致注册失败,常见的标识类型
如果是Java数组,标识符中会有一个“[”,引用类型(除基本类型的数组外)标识符后都会有一个“,”
Javap的函数或者变量能帮助生成函数或者变量的签名信息,javap -s -p xxx xxx是编译后生成的class文件,s表示输出内部数据类型的签名信息,p表示打印所有的函数和成员的签名信息。
垃圾回收
在Java中创建对象时由垃圾回收器进行回收和释放内存,对JNI有什么影响呢?
在JNI层使用save_thiz = thiz;这样的语句是不会增加引用计数的,就很有可能被回收,所以在JNI中有三种类型的引用:
Local Reference:本地引用。在JNI层函数中使用的非全局引用对象都是Local Reference。它包括函数调用时传入的jobject和在JNI层函数中创建的jobject。LocalReference最大的特点就是,一旦JNI层函数返回,这些jobject就可能被垃圾回收。
如果不调用DeleteLocalRef,pathStr将在函数返回后被回收;如果调用DeleteLocalRef的话,pathStr会立即被回收。
Global Reference:(env->NewGlobalRef(client)) )全局引用,这种对象如不主动释放,就永远不会被垃圾回收,调用DeleteGlobalRef释放这个全局引用。
Weak Global Reference:弱全局引用,一种特殊的GlobalReference,在运行过程中可能会被垃圾回收。所以在程序中使用它之前,需要调用JNIEnv的IsSameObject判断它是不是被回收了。
使用最多的是Local Reference和Global Reference,及时回收Local Reference,避免占用过多内存。
异常处理
调用JNIEnv的某些函数出错了产生异常,那么也不会中断本地的函数执行,直到从JNI层返回到Java层后,虚拟机才会抛出这个异常。
ExceptionOccured:用来判断是否发生异常
ExceptionClear:清理当前JNI层发生的异常
ThrowNew:向Java层抛出异常