JNI层理解

https://www.gliffy.com/go/html5/launch?app=1b5094b0-6042-11e2-bcfd-0800200c9a66  画图工具


涉及的源码:

MediaScanner.java (base\media\java\android\media)             -------------------------    >               JAVA层

android_media_MediaScanner.cpp (base\media\jni)              --------------------------  >                 JNI层

android_media_MediaPlayer.cpp (base\media\jni)

AndroidRuntime.cpp (base\core\jni)


JNI概述:

JNI: Java Native interface的缩写.中文译为“Java本地调用”。通俗地说,它是一种技术,通过这种技术可以做到以下两点:

1. 可以使Java程序中的函数可以调用Native语言写的函数,Native 一般是C/C++编写的函数。

2. 反过来,Native程序中的函数也可以调用Java层的函数,也就是C/C++程序中可以调用Java的函数。

总的来说:就是Java和C/C++函数之间可以互相调用


在Android平台上,JNI就是一座将Native世界和Java世界间的间隔连接成了一座通途的桥。下图展示了Android平台中JNI所处的位置。



学习JNI实例: MediaScanner


将图2-2和图2-1结合来看,可以直到:

1. Java世界对应 MediaScanner  ,这个MediaScanner类有一些函数需要由Native层来实现

2. JNI层对应的是libmedia_jni.so.  media_jni是JNI库的名字"media"是Native层库的名字,也就是即libmedia库。 注意,JNI库的名字可以随便取,不过Android平台基本上都采用“lib模块名_jni.so”的命名方式

3. MediaScanner通过JNI库libmedia_jni.so和Native层的libmedia.so交互

从上面的分析可知:JNI层必须实现动态库的形式,这样Java虚拟机才能加载它并调用它的函数


Java层的MediaScanner分析:

MediaScanner是Android平台中多媒体系统的重要组成部分,它的功能是扫描媒体文件,得到诸如歌曲时长,歌曲作者等媒体信息,并将他们存入到媒体数据库中,供其他应用程序使用。

下面看下MediaScanner源码,这里提取出与JNI有关的部分:


上面代码两个比较重要的要点:

1. 一个是加载JNI库:System.loadLibrary("media_jni");  在类的static语句中加载

2. Java的Native函数 : private native void processDirectory(String path, MediaScannerClient client);


加载JNI库

在调用native函数前,在何时候,任何地方加载都可以。通行的做法是在类的static语句中加载,调用System.loadLibrary方法就可以了。我们以后就按这种方法编写代码即可。注意这个函数的参数是动态库的名字,即media_jni(模块名_jni)。 系统会自动根据不同的平台拓展为真是的动态库文件名。Linux为lib模块名_jni.so ,widows上拓展为模块名_jni.dll


JNI层MediaScanner的分析

JNI层的代码在android_media_MediaScanner.cpp中:



问题:Java层的native_init函数是如何对应JNI层的android_media_MediaScanner_native_init函数的?下面回答这个问题:

注册JNI函数

大家直到,native_init函数位于android.media包中(MediaScanner.java (framework\base\media\java\android\media))它的全路径名应该是android.media.MediaScanner.native_init,(android.media是文件夹的路径,MediaScanner是类名,native_init是类中的一个函数)而JNI层函数的名字是android_media_MediaScanner_native_init(JNI的名字就是根据上面的路径和名字有关的)。

因为在Native语言中,符号"."有着特殊的意义,所以JNI层需要把JAVA函数名称(包括包名)中的"."换成"_".也就是通过这种方式,native_init找到了自己JNI层的本家兄弟android.media.MediaScanner.native_init。

其实上面讨论的就是JNI函数的注册问题,“注册”之意就是将Java层的native函数和JNI层对应的实现函数关联起来。有了这种关联,调用Java层的native函数时,就能顺利转到JNI层对应的函数执行了。


对于JNI函数的注册方法,有两种:

1. 静态方法(不列举)

2. 动态方法

动态注册

既然Java native函数和JNI函数是一一对应的,那么是不是会有一个结构来保存这种关联关系呢?

答案是肯定的,十一个叫JNINativeMethod的机构体。



下面看看是JNINativeMethod是如何使用的:


从上面注册可以看到,最终是由AndroidRuntime类的方法registerNativeMethods进行注册的

下面看看AndroidRuntime::registerNativeMethods的实现:


jniRegisterNativeMethods是Android平台中为了方便JNI使用而提供的一个帮助函数,其代码如下所示:


所以,在自己的JNI层代码中使用这种方法,就可以完成动态注册了。这里还有一个很棘手的问题:这些动态注册的函数register_android_media_MediaScanner在什么时候和在什么地方被调用的呢?这里不卖关子,直接给出该问题的答案:

当Java层通过System.loadLibrary加载完JNI动态库后,紧接着就会查找该库中一个叫JNI_OnLoad的函数。如果有,就调用它,而动态注册的工作就是在这里完成的。(目前没有找到相关代码进行证明

所以如果使用动态注册的方法,就必须实现JNI_Onload函数,只有在这个函数中才有机会完成动态注册的工作.静态注册的方法则没有这个要求,但建议大家也实现这个JNI_Onload函数,因为有一些初始化工作是可以在这里做的。

那么libmedia_jni.so的JNI_Onload函数是在哪里实现的呢?搜索代码,该函数定义在了android_media_MediaPlayer.cpp (base\media\jni)。也是JNI层实现的,内容如下:


下面总结一下:


二、JNI的数据转换问题:

1.基本数据类型转换:

2.引用数据类型的转换:


由上表可知:除了Java中基本数据类型的数组,Class,String和Throwable外,其余所有Java对象的数据类型在JNI中都用Jobject表示。看看processFile函数:


从上面代码可以发现:

java的String类型在JNI层对应为jstring类型

Java的MediaScannerClient类型为JNI层对应为jobject

如果对象类型都用jobject表示,就好比是Native层的void*类型一样,对“码农”来说,它们是完全透明的。既然是透明的,那该如何使用和操作他们呢?在回答这个问题之前,再来仔细看看上面的android_media_MediaScanner_processFile函数。代码如下:


上面的代码,引出了下面几个小姐的主角JNIEnv


JNIEnv的介绍:


JNIEnv是一个与线程相关的,代表JNI环境的结构体。从上图可以看到,JNIEnv实际上提供了一些JNI的系统函数,通过这些函数可以做到:

1.调用Java的函数

2.操作jobject对象等很多事情。

后面的小节中将具体介绍如何使用JNIEnv中的函数。这里,先介绍一个关于JNIEnv的重要知识点。

上面提到说JNIEnv是一个与线程相关的变量,也就是说,线程A有一个JNIEnv,线程B有一个JNIEnv。由于线程相关,所以不能在线程B中使用线程A的JNIEnv结构体。读者可能会问,JNIEnv不都是native函数转换成JNI层函数后由虚拟机传进来的吗?使用传进来的这个JNIEnv总不会错吧?是的,在这种情况下使用当然不会出错。只是当后台线程收到一个网络消息,又不需要由Native层函数主动回调Java层函数时,JNIEnv从何而来呢?根据前面的介绍可知,我们不能保存另外一个线程的JNIEnv结构体,然后把它放到后台线程来用。这该如何是好?

还记得前面介绍的那个JNI_Onload函数吗?它的第一个参数是JavaVM,它是虚拟机在JNI层的代表,代码如下所示:


正如上面代码所说,不论进程中有多少线程,JavaVM却独此一份,所以在任何地方都可以使用它。那么,JavaVM和JNIEnv又有什么关系呢?答案如下:

1. 调用JavaVM的AttachCurrentThread函数中,就可以获取当前这个线程的JNIEnv结构体。这样就可以在后台线程中回调Java函数了。

2. 另外,在后台线程退出前,需要调用JavaVM的DetachCurrentThread函数来释放对应的资源


通过JNIEnv操作jobject:

前面提到过一个问题,即Java的引用类型除了少数几个外,最终在JNI层都会用jobject来表示对象的数据类型,那么该如何操作这个jobject呢?

从另外一个角度解释这个问题:

一个java对象是由什么组成的?当然是它的成员变量和成员函数了。那么,操作jobject的本质就应当是操作这些对象的成员变量和成员函数。所以应先来看与成员变量及成员函数有关的内容。

1.jfieldID和jmethodID介绍:

在JNI规则中,用jfieldIDjmethodID来表示java类的成员变量和成员函数。

JfieldID:Java类的成员变量

jmethodID:Java类的成员函数。

可以通过JNIEnv的下面两个方法函数得到:


其中

jclass代表Java类

name表示成员函数或成员变量的名字

sig为这个函数和变量的签名信息。

查看MS是如何使用它的:



上面的scanFile,handleStringTag,setMimeType函数的jmethodID保存为MyMediaScannerClient的成员变量。 为什么这里要把他们保存起来

这个涉及到一个关于程序运行效率的知识点:如果每次操作jobject前都去查询jmethodID或jfieldID,那么将会影响程序运行的效率,所以我们在初始化的时候可以取出这些ID并保存起来以供后续使用。

使用JfieldID和jmethod

取出jmethod后,如何使用?


明白了,通过JNIEnv输出CallVoidMethod,再把jobject,jMethodID和对应的参数传进去,JNI层就能够调用Java对象的函数了!(JNI层回调Java层函数


实际上,JNIEnv有一系列类似CallVoidMethod的函数,形式如下:


其中type对应java函数的返回之类型,例如:CallIntMethod等

上面是针对非static函数的,如果向调用Java中的static函数,则使用CallStatic<Type>Method系列函数。(以上都是JNI操作Java层对象的方法函数


现在我们了解如何通过JNIEnv操作jobject的成员函数,那么如何通过jfieldID操作jobject的成员变量呢?这里直接给出整体的解决方案。如下所示:



jstring介绍:

Java中的String也是引用类型,不过由于它的使用频率较高,所以在JNI规范中单独创建了一个jstring类型来表示Java的String类型。虽然jstring是一个独立的数据类型,但是它并没有提供成员函数以便操作。

而C++中的string'类是有自己的成员函数的。那么该怎么操作jstring呢?还需要依靠JNIEnv提供的帮助。这里看几个有关的jstring的函数:

本地字符串转换成Java的String对象

1.   JNIEnv->NewString : 从Native的字符串得到一个jstring对象。其实,可以把一个jstring对象看成是Java中String对象在JNI层的代表,也就是说jstring就是一个Java String。

2.  JNIEnv->NewStringUTF : 根据Native的一个UTF-8字符层得到一个jstring对象。这个在工作中用的最多

将Java String对象转换为本地字符串

3.  GetStingChars 和 GetStingUTFChars

注意:

在调用了上面几个函数,在做完相关的工作后,都需要调用ReleaseStringChars函数或ReleaseStringUTFChars函数来对应地释放资源,否则会导致JVM内存泄漏。这一点和jstring的内部实现有关。必须注意

下面来看看processFile是怎么做的?



JNI类型签名介绍:

先看下动态注册中的一段代码:


签名的目的:

因为Java支持函数重载,也就是说,可以定义同名但不同参数的函数。但仅仅根据函数名是没有办法找到具体函数的。为了解决这个问题,JNI技术中就将参数类型和返回值类型的组合作为了一个函数的简明信息,有了签名信息和函数名,就能很顺利地找到Java中的函数了。

签名的格式如下:


以processFile为例:


L:表示的是传进的参数是引用类型。



函数签名不仅看起来麻烦,写起来更麻烦,稍微写错一个标点就会导致注册失败。所以,在具体编码时,我们可以定义字符串宏,这样改起来也方便。


上面列出了一些常用的类型标识。注意:如果Java类型是数组,则标识中会有一个“【” ,另外,引用类型(除基本类型的数组外)的标识最后都有一个“;”

只有引用类型后才有“;”

下面是函数签名的小例子:


也可以使用:javap -s -p xxx 来获取签名信息

xxx: 是编译后的class文件

s: 输出内部数据类型的签名信息

p: 打印所有函数和成员的签名信息。默认智慧打印public成员和函数的签名信息。


垃圾回收

我们知道,Java中创建的对象最后是由垃圾回收器来回收和释放内存的,可它对JNI有什么影响呢?下面看一个例子:



上面的做法肯定会有问题,因为和save_thiz对应的Java层中的MediaScanner很有可能已经被垃圾回收了,也就是说save_thiz保存的这个jobject可能是一个野指针,如果使用它,后果会很严重。

可能有人要问,对一个引用类型执行赋值操作,他的引用计数不会增加吗?而垃圾回收机制知会保证哪些没有被引用的对象才会被清理。问得对,但如果在JNI层使用下面这样的语句,是不会增加引用计数的

save_thiz = thiz; //这种赋值不会增加jobject的引用计数

引用计数没有增加,thiz就有可能被回收,那该怎么办?

不必担心,JNI规范已经很好地解决了这个问题,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和Global Reference。下面看一个实例:


每当JNI层想要保存Java层中的某个对象时,就可以使用Global Reference,使用完后记住释放它就可以了。


下面看看LocalReference的一个问题,还是先看实例:


所以,没有及时回收Local Reference或许是进程占用内存过多的一个原因。请务必注意这一点。。。


JNI中的异常处理

JNI中也有异常,不过它和C++,Java的异常不太一样。如果调用JNIEnv的某些函数出错了,则会产生一个异常,但这个异常不会中断本地函数的执行,知道从JNI层返回到Java层后,虚拟机才会抛出这个异常,然后再JNi层中产生的异常不会中断本地函数的运行,但是一旦产生异常后,就只能做一些资源清理工作了。(例如释放全局引用,或者ReleaseStringChars)。如果这时调用除上面所说函数(ReleaseStringChars)之外的其他JNIEnv函数,就会导致程序死掉

下面看一个和异常处理有关的例子,代码如下所示:


JNI层函数可以在代码中截获和修改这些异常,JNIEnv提供了三个函数给予帮助:

1. ExceptionOccured函数,用来判断是否发生异常。

2. ExceptionClerar函数,用来清理当前JNI层中发生的异常

3. ThrowNew函数,用来向Java层抛出异常。

异常处理是JNI层代码必须关注的事情,在编写代码的时候一定要小心对待。




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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值