现在的位置: 首页 > 综合 > 正文

JNI调用机制

2013年10月12日 ⁄ 综合 ⁄ 共 3568字 ⁄ 字号 评论关闭

Java Native Interface(JNI)是java本地接口,所谓的本地(native)一般是指c/c++语言。当使用java进行程序设计的时候,一般主要有三种情况需要c的协助。

1)调用驱动。由于操作系统提供的驱动接口一般都是c接口,java语言本身不具备操作这些驱动的能力

2)对于某些大量数据处理的模块,java效率可能远低于c,因此,程序员希望使用c完成

3)对于某些功能模块,可能java和c的效率差不多,但是这些模块已经有现成的c代码,程序员不想用java重写,而是复用已有的c代码。

这就是java提出jni概念的原因。无论出于什么原因,从程序的角度来看,jni接口主要包含两种情况。第一种是java中调用c,第二种是c中访问java,只要解决了这两个问题,那么就可以任意进行java和c的应用组合。framework中大量使用JNI完成本地接口的实现,因此,理解JNI对于阅读Framework代码有很大帮助。

1.java中访问c

java中可以定义某个函数为native类型,对于native函数,只要声明即可,因为该函数的实现是native的,既有相应的c去实现。java编译器遇到native函数时,不会关心函数的具体实现,因此编译不会出错误。

程序运行时,在调用native方法之前,程序员必须把c生成的动态库装载进来,否则程序会因为找不到相应的native方法而出错。

当调用native函数时,java会自动产生一个对应的c中的函数名称,因为java中声明的函数名称和c中实现的函数名称是不同的。其关系为,后者等于包名加前者的名称,并且中间以下划线分割,比如Framework,AssetManager类声明以下方法

private native final void init() ;

该方法对应的c函数是

static void android_content_AssetManager_init(JNIEnv* env,jobject clazz)

这种映射关系并不是java编译器内涵的,程序员完全可以改变,但这是一种编程规范。事实上当java调用native时,编译器会向native引擎传递调用者的包名,以及函数名称,含有参数类型,仅此而已,当然这也足以,native引擎根据这些信息决定应该具体调用哪个本地函数。native引擎中AndroidRuntime类提供了一个registerNativeMethods()方法,可以通过该函数来定义java native函数和c函数名称的映射关系。

在产生c函数中,会包含至少两个参数。前者是JNIEnv对象,该对象是Java VM所运行的环境,相当于JVM管家,通过它可以访问JVM内部的各种对象;第二个参数是jobject,是调用该函数的对象,比如上面的指的是AssetManager对象。

如果native声明的函数本身有参数,那么这些参数会依次放在以上两个参数后面,比如以下方法

private native final Sting[] getArrayStringResource(int arrayRes);

其对应的c函数为:

static jobjectArray android_content_AssetManager_getArrayStringResource(JNIEnv* env,jobject clazz,jint arrayResId);

在以上对应关系中我们发现,native所使用的类型跟java不一样,这些具体的定义位于jni.h中

该文件所在路径为:

dalvik/libnativehelper/include/nativehelper/jni.h

external/webkit/webkit/android/javaVM/jni.h

ndk/build/platforms/android-x/arch-xxx/usr/include/jni.h

不同的平台会有不同的定义,因为java是跨平台的,而各自平台的cpu数据宽度不同,所以又各自的定义。

以上介绍了java和c函数的名称转换,如何具体操作呢?

依照转换关系,手工编写相应的c代码,然后编译成动态库,并在java代码执行的时候加载该库。为了辅助,java提供了javah工具,该工具可以从一个java文件生成相应的c头文件剩下的就是根据这些头文件在实现具体内部代码

比如我们的Foo.java

package com.haiii.android.client;

public class Foo{

native void foo1();

native int foo2(int a,String b);

使用javah命令

javah -d ~/Desktop -jni com.haiii.android.client.Foo

-d 是指定输出路径,-jni表示生成jni头文件,后面的是class文件所在类。

生成默认文件名称为com_haiii_andorid_client_Foo.h。

编译c代码,产生动态库以后,在代码调用native函数前,使用System.loadLibrary('lib_name')函数加载该库。

java代码不能直接访问c中的变量,c中的变量对java来讲都是私有的。如果想访问某个变量,那么就让c提供一个get/set类型的方法,以达到简介访问的目的。

2.c访问java

c为什么访问java,如果c中需要使用java的某个变量而进行相应的处理,或者c中想调用java中函数完成某个操作,那么c就要访问java。

由于java中的函数在native引擎中没有直接的函数指针,java函数只能由java引擎去执行,而不是c。所以c访问java不能通过函数指针,而只能通过通用的函数接口,正如java调用c一样。java把类名、参数类型、传递给native引擎,然后由native引擎处理c函数,同理c调用java是,也需要把想要访问的类名、参数名称、参数传递给java引擎。其步骤如下:

1)获取Java对象的类

cls=env->GetObjectClass(jobject);

其中env为java调用c函数时的第一个参数,这意味着c调用java函数只能在java调用c函数中进行,否则无法获取env变量。换句话说,对于c来讲,就是“你不惹我,我不惹你”。jobject为第二个参数。class的类型是jclass。

2)获取java函数的id值

jmethodId mid=env->GetMethodId(cls,"method_name","([Ljava/lang/String;)V");

该方法第二个参数为java中的函数名称,第三个参数代表了java函数的参数和返回值,参数在括弧之中,返回值在括弧之外。参数【Ljava/lang/String代表了String类型的参数,由于String本身是一个类,而不是java的原子类型,所以前面加了包的名称,并用斜线分割,最前面还要用一个中括弧进行标示,后面还要用分号隔离。返回值V代表void。

java和native数据类型对应

java类型             native类型

boolean         Z

byte         B

char C

double D

float F

int I

long L

Object 'L'+'packageName'+';'

short S

java提供了一个javap工具,查看java函数的输入,返回参数,用法如下

javap -s Foo

-s含义是签名,上面所说的参数也叫函数的签名,因为java允许函数重载,所以不同的参数、返回值代表着不同的函数,或者说是不同函数签名。

3)找到函数后,就可以调用该函数了

env->CallXXXMethod(jobject,mid,ret);

其中XXX代表了函数的返回值类型,具体包括Void、Object、Boolean、Byte、Char、Short、Int、Long、Float、Double。第二个参数mid即为第二步获取的函数id。通过以上三步,实现了c中调用java函数。

c中如何访问java中的变量呢?

实现步骤如下

1)与前面相同

class=env->getObjectClass(jobject);

2)获取变量ID值

jfieldId fid=env->getFiledId(cls,"filed_name","I")

参数filed_name为java变量名称,第三个参数是变量的类型,其格式与上面相同。

3)获取变量值

value=env->getXXXField(env,jobject,fid)

该函数与上面的显著不同,其中第一、第二参数为原装java访问c函数的前两个参数,该方法以返回值的方式获取变量值,而不是通过参数引用。

转自《android内核剖析》

抱歉!评论已关闭.