JNI是Java Native Interface的缩写,java本地调用,JNI是一种技术,可以做到以下两点:
1.java程序中函数可以调用Native语言写的函数,Native一般指的是C/C++编写的函数
2.Native程序中的函数可以调用Java层的函数,也就是说,C/C++程序中可以调用java的函数
问题一: 在平台无关的java中,为什么要创建一个与Native相关的JNI技术,这不是破坏java的平台无关特性吗?
1.java的虚拟机是用Native语言写的,而虚拟机又运行在具体的平台上,所以,虚拟机本身无法做到与平台无关,然而,有了JNI技术后就可以对java层屏蔽不同操作系统平台(window和linux)之间的差异(例如,window上API使用openfile函数,linux上API使用open函数),这样就能实现java本身的平台无关特性。
2.在java出世之前,很多的东西都是拿Native写的,java也是为了避免重复造轮子,并且一些要求效率和速度的场合还是需要Native参与。
1.java世界对应的是MediaScanner,然而MediaScanner类中的一些函数需要Native层来实现。
2.JNI层对应的是libmedia_jni.so,media_jni是JNI库的名字,其中,下划线前的media是Native层库的名字,这里就是libmedia库,下划线jni表示他是一个JNI库。注意,JNI库名字可以随便取,但是Android平台上基本都采用 “lib模块名_jni.so” 的命名方式。
3.Native层对应的是libmedia.so,这个库完成了实际的功能。
4.MediaScanner将通过JNI库libmedia_jni.so和Native层的libmedia.so交互。
注意:
1.JNI层必须实现为动态库的形式,这样java虚拟机才能加载并调用它的函数。
2.MediaScanner是Android平台中多媒体系统的重要组成部分,他的功能是扫描媒体文件,得到歌曲时长,歌曲作者等媒体信息,并将他们存入媒体数据库中,提供其他应用程序使用。
加载JNI库
java如果要调用Native函数就必须在JNI层通过加载一个动态库实现,原则上,动态库的加载时间是在调用Native函数之前,但实际中通行的做法是在类的static语句中加载,调用System.loadLibrary方法,System.loadLibrary方法的参数是动态库的名字,media_jni,系统会自动根据不同的平台拓展成真实的动态库文件名。
总结:
JNI技术使用需要完成下面两项工作就可以使用JNI:
1.加载对应的JNI库
2.声明由关键字navtive修饰的函数
因为在Native语言中,符号“.”有着特殊的意义,所以JNI层需要把Java函数名称中的“.”替换成“_”,这样native_init找到了自己JNI层的本家兄弟android.media.MediaScanner.native_init
JNI函数的注册问题就是将java层的native函数JNI层对应的实现函数关联起来,有了这种关联,调用java层的native函数时,就能顺利转到JNI层对应的函数执行,JNI函数的注册方法有两种:
1.静态方法
根据函数名来找到对应JNI函数,它需要java的工具程序javah参与,先编写java代码,然后编译生成.class文件,使用java的工具程序叫做javah,如 java -o output packagename,这样它会深层次一个叫output.h的JNI层头文件,其中packagename.classname是java代码编译后的class文件,而在生成的output.h文件里,声明了对应的JNI层函数,只要实现里面的函数就行。
注意:
需要头文件
问题: 解释一下静态方法中native函数是如何找到对应的JNI函数的?
当java层调用native_init函数时,它会从对应的JNI库中寻找java_android_media_Media-Scanner_native_linit函数,如果没有就会报错,如果找到,则会为这个native_init和java_android_media_MediaScanner_native_linit建立一个关联关系,其实就是保存JNI层函数的指针,以后再调用native_init函数时,直接使用这个函数指针就可以了,当然这项工作是由虚拟机完成的。
本质上静态方法就是根据函数名来建立java函数和JNI函数之间的关联关系,而且它要求JNI层函数的名字必须遵循特定的格式,弊端:
1.需要编译所有声明了native函数的java类,每个所生成的class文件都得用javah生成一个头文件。
2.javah生成的JNI层函数名字特别长,书写不方便。
3.初次调用native函数时需要根据函数名字搜索对应的JNI层函数来建立关联关系,这样会影响运行效率。
解决方案:
java native函数是通过函数指针来和JNI层函数建立关联关系的,如果直接让native函数知道JNI层对应的函数指针,就可以了。
2.动态注册
既然java native函数和JNI函数是一一对应的,在JNI技术中,用来记录这种关系的是一个叫做JNINativeMethod的结构
MediaScanner JNI层对这个结构体的使用
AndroidRunTime类提供了一个registerNativeMethods函数来完成注册工作,实现如下:
这些动态注册函数在java层通过System.loadLibrary加载完JNI动态库后,紧接着会查找该库中的一个叫做JNI_OnLoad的函数,如果有就调用它,动态注册的工作就是在这里完成的。
java数据类型分为基本数据类型和引用数据类型,JNI层会区别对待两者。
JNIEvn是一个与线程相关的代表JNI环境的结构体,JNIEnv的内部结构:
JNIEnv实际上就是提供了一些JNI系统函数,通过这些函数可以调用Java的函数,操作jobject对象很多事情。
JNIEnv是一个与线程相关的变量,也就是说,线程A有一个JNIEnv,线程B有一个JNIEnv,由于线程相关,不能在线程B中使用线程A的JNIEnv结构体。JNIEnv都是native函数转换成为JNI层函数后由虚拟机传进来的,但是,但当后台线程收到一个网络消息,而又需要Native层函数主动回调Java层函数时,我们不能保存另一个线程的JNIEnv结构体,然后把它放到后台线程中来用,我们可以使用JNI_OnLoad函数中的第一个参数JavaVM:
无论进程由多少个线程,因为JavaVM独此一份,所以在任何地方都可以使用它。JavaVM和JNIEnv关系如下:
1.调用JavaVM的AttachCurrentThread函数,就可以得到这个线程的JNIEnv结构体,这样就可以在后台线程中回调Java函数了
2.在后台线程推出前,需要调用JavaVM的DetachCurrentThread函数来释放对应的资源。
java的引用类型除了少数几个外,最终在JNI层都会用jobject来表示对象的数据类型,如果想要操作jobject,本质上就是操作对象的成员变量和成员函数
1.jfieldID和jmethodID介绍
在JNI规则中,用jfieldID和jmethodID来表示java类的成员变量和成员函数,可以通过JNIEnv的下面两个函数得到:
jclass代表Java类,name表示成员函数或者成员变量的名字,sig为这个函数和变量的签名信息。
将scanFile和handleStringTag函数的jmethodID保存为MyMediaScannerClient的成员变量,,如果每次操作jobject前都去查询jmethodID或jfieldID,那么将会影响程序运行的效率,所以我们在初始化的时候可以取出这些ID保存起来,以后使用。
2.jfieldID和jmethodID使用
通过JNIEnv输出CallVoidMethod,再把jobject、jMethodID和对应的参数传进去,JNI层就能够调用Java对象的函数。
jfieldID操作jobject的成员变量:获得fieldID后,可调用Get< type >Field系列函数获取jobjet对应的成员函数的值,或者调用Set< type >Field系列函数来设置jobject对应的成员变量的值。
java中的string也是引用类型,不过由于它的使用频率比较高,所以在JNI规范中单独创建了一个jstring类型来表示java中的string类型,但是它并没有提供成员函数以便操作,而C++中的string类是有自己的成员函数的,所以操作jstring还是要依靠JNIEnv提供帮助。
动态注册的一段代码
这个y很长的字符串声明意思?
根据前面说的,这是java中对应函数的签名信息,由参数类型和返回值类型共同组成,为什么需要这个签名信息?
因为Java支持函数重载,因此,可以定义同名但是不同参数的函数,但是,仅仅通过函数名是没有办法找到具体函数的。为了解决,JNI技术中就将参数类型和返回值类型的组合作为一个函数的签名信息,有了签名信息和函数名,就能很顺利地找到Java中的函数。因为这个函数签名很长很麻烦,可以在具体编码的时定义一个字符串宏,这样改起来方便。
Java提供了一个叫javap的工具能帮助生成函数或变量的签名信息,用法如下:
javap -s -p xxx
Java中创建的对象最后是由垃圾回收器来回收和释放内存的,JNI一共提供了三种类型的引用。
1.Local Reference:本地引用,就是在JNI层函数中使用的非全局引用对象都是Local Reference,它包括函数调用时传入的jobject和JNI层函数中创建的jobject。Local Reference最大的特点就是,一旦JNI层函数返回,这些jobject就可能被垃圾回收。
2.Global Reference:全局引用,这种对象不主动释放,他永远不会被会垃圾回收。
3.Weak Global Reference:弱全局引用,一种特殊的Global Reference,在运行过程中可能会被垃圾回收,所以在使用它之前,需要调用JNIEnv的IsSameObject判断它是否被回收了。
因此,没有及时回收Local Reference或许是进程占用内存过多的一个原因。
JNI中的异常和C++、Java中的异常不太一样,如果调用JNIEnv的某些函数出问题了,会产生一个异常,但是这个异常不会中断本地函数的执行,直到JNI层返回到Java层后,虚拟机才会抛出这个异常,虽然在JNI层中产生的异常不会中断本地函数的运行,但一旦产生异常后,就只能做一些资源清理工作了,比如释放全局引用,如果这个时候调用JNIEnv的其他函数,会导致程序死掉。
JNI层函数可以在代码中截获和修改这些异常,JNIEnv提供了三个函数:
1.ExceptionOccured函数,用来判断是否发生异常
2.ExceptionClear函数,用来清理当前JNI层中发生的异常
3.ThrowNew函数,用来向Java层抛出异常。
1.JNI函数的注册方法
2.Java和JNI层数据类型的转换
3.JNIEnv和jstring的使用方法,以及JNI中的类型签名
4.垃圾回收在JNI层中的使用,以及异常处理方面的知识