• Android JNI浅谈


     

    JNI(Java Native Interface)Java本地接口,它的存在是为了:

    1. java程序中的函数可以调用Native语言编写的函数,一般是C/C++
    2. 本地函数(C/C++编写的函数)可以调用Java层的函数。

    也就是说JNI提供了底层语言与上层java之间交互的通道。

     

    那么JNI是如何实现这一点的呢?

           首先,既然java中可以调用Native的函数,那么就说明,在Java的世界和Native的世界中,包含相互对应的方法与函数,那么它们之间是如何对应的呢?先看下图:


       
    以MediaScanner为例,在Java中对应的类是MediaScanner,Native层与之对应的是libmedia.so,这个库文件完成了实际的功能。JNI作为连接对应的是libmedia_jni.so。media_jni是JNI库的名字,其中下划线前的media是Native层库的名字,就是这里的libmedia库,下划线之后的jni表示它是一个JNI库。

    注:JNI库的名字是可以随便取的,但是android中基本都在用“lib模块名_jni.so”格式命名。

    由此可知,在JNI层必须用动态库的形式实现,这样虚拟机才能加载它并调用它的函数。

    当然,并不是通过这种命名规则JNI就知道如何来对应我们的Java层和Native层的方法与函数,这需要注册JNI函数,再如何注册之前,先看下在java层如何调用JNI。

    还是以MediaScanner为例:

     

    public class MediaScanner
    {
        static {
                System.loadLibrary("media_jni");
                native_init();
        }

    在static语句中,首先通过System.loadLibrary()来加载JNI库,其中的media_jni是JNI库的名字,在实际加载动态库的时候,会将其扩展成libmedia_jni.so。在window平台上则扩展为meida_jni.dll。然后调用native_init函数。

     

    private static native final void native_init();
    private native void processFile(String path, String mimeType, MediaScannerClient client);

     

    这里使用native关键字声明了函数,标明这些函数,由JNI层来完成。

           由以上信息可以看出,在Java层调用JNI层函数只有两个步骤:加载JNI库和声明Java中的native函数。

           那么在JNI层是如何关联java方法与JNI层中的实现的呢?首先看看JNI层是如何实现我们在Java层用Native关键字声明的方法的:

     

    我们看到在java中的方法processFile(),对应在JNI层是android_media_MediaScanner_processFile。那么系统是如何做到让这两者匹配的呢?这就是我们之前说的java注册。注册之后,Java层的native函数就和JNI层的实现函数关联起来了,有了这种关联,当调用Java层native函数是,就能顺利的转移到JNI层来实现了。注册JNI函数有两种方式:静态注册动态注册

    静态注册,这种方式就是根据函数名来找对应JNI函数,需要Java的工具程序javah参与,流程如下:

    1.先编写java代码,然后编译生成.class文件。

    2.使用javah,如javah –o output packagename.classname,这样就会生成一个output.h的JNI层头文件,在这个文件里,对应了JNI层的函数,只要实现里面的函数即可。

    这个头文件一般都会使用packagename_class.h的样式,例如MediaScanner对应的JNI层头文件为android_media_MediaScanner.h。内容如下:

     

    总结下:静态注册其实比较简单,当java层调用processFile函数时,它会从对应的JNI库中寻找java_android_media_MediaScanner_processFile函数,如果没有则报错,如果有,则为两者建立一个对应关系,其实也就是保存一个JNI层函数的指针,当以后再次调用processFile函数时,就会直接使用这个指针。

    但是,这种方式也有一定的弊端:

    1.需要声明所有声明了native关键字函数的java类,每个生成的class文件都的用javah生成头文件。

    2.javah生成的JNI层函数名特别长,不方便书写,使用。

    3.初次使用时,要根据名字搜索对应JNI层函数建立关系,这样影响运行效率。

    动态注册,既然我们说Java层native函数与JNI层函数有一一对应的关系,那么可否利用一种结构来保存这种对应关系呢?动态注册也就由此出发,利用一个叫JNINativeMethod结构来保存这种一一对应关系,定义如下:

     

    typedf struct{
    
    const char* name;          //java中native函数名,processFile
    
    const char* signature;      //java函数的签名信息,字符串表示参数类型与返回值类型
    
    void* fnPtr;           //JNI层对应函数的函数指针,类型为void*
    
    }JNINativeMethod;

     

    那么如何使用这种结构体呢?我们看JNI层是如何做的:

    static JNINativeMethod gMethods[]={
    
        …… 
    
    {
    
              "processFile"//Java中native的函数名
    
              //porcessFile的签名信息
    
    "(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V”,
    
    //JNI层对应的函数指针
    
              (void *)android_media_mediaScanner_processFile   
    
    },
    
    ……
    
    };
    
     
    
    ……
    
    int register_android_media_MediaScanner(JNIEnv *env){
    
           //调用AndroidRuntime的registerNativeMethods函数,第二个参数标明是Java中的哪个表
    
           return AndroidRuntime::registerNativeMethods(env,
    
                 "android/media/Media/MediaScanner",gMethods,NELEM(gMethods)
    
    }
    
    AndroidRuntime类提供了一个registerNativeMethods()方法来完成注册,在registerNativeMethods中:
    
    int AndroidRuntime::registerNativeMethods(JNIEnv* env,
    
    const char* className,const JNINativeMethod* gMethods,int numMethods){
    
           //调用jniRegisterNativeMethods完成注册
    
    return jniRegisterNativeMethods(env,
    
    classname,gMethods,numMethods);
    
    }
    
    其中jniRegisterNativeMethods是Android平台为方便使用JNI提供的帮助函数,代码为:
    
    int jniRegisterNativeMethods(JNIEnv* env,
    
    const char* className,const JNINativeMethod* gMethods,int numMethods)
    
    {
    
           jclass clazz;
    
           clazz=(*env)->FindClass(env,className);
    
           ……
    
        if((*env)->RegisterNatives(env,clazz,gMethods,numMethods)<0){
    
              return -1;
    
    }
    
    return 0;
    
    }
    
     

    总结如下:其实动态注册只做了两步:

    1.使用env指向一个JNIEnv结构体,className对应Java类名,由于JNINativeMethod中使用的函数名不是全路径,因此要指明是哪个类。

    2.调用JNIEnv的RegisterNatives函数完成注册。

     

    那么JNI中的注册信息时在什么时候和什么地方被调用的呢?

    Java层调用System.loadLibrary加载JNI动态库之后,紧接着会查找该库中一个JNI_OnLoad的函数。如果有,则调用它,动态注册就是在这里完成的。

    在静态注册中对改函数没有要求,但是动态注册则必须有。

     

    前面总结了JNI是如何和Java层建立关联,如何在程序中注册的,下面主要说下JNI的类型以及签名。

    JNI的数据类型

    在Java中的函数类型有基本数据类型和引用数据类型。那么这些类型在JNI层中会变成什么呢?下表是JNI层数据转换:

    引用数据类型转换:

    Java引用类型

    Native类型

    Java引用类型

    Native类型

    All objects

    jobjects

    char[]

    jcharArray

    Java.long.Class

    jclass

    short[]

    jshortArray

    Java.lang.String

    Jstring

    int[]

    jintArray

    Object[]

    jobjectArray

    long[]

    jlongArray

    boolean[]

    jbooleanArray

    float[]

    floatArray

    byte[]

    jbyteArray

    double[]

    jdoubleArray

    java.lang.Throwable

    jthrowable

     

     

    基本数据类型

    Java

    boolean

    byte

    char

    short

    int

    long

    float

    double

    Native

    jboolean

    jbyte

    jchar

    jshort

    jint

    jlong

    jfloat

    jdouble

     

    由以上两表,我们可以看出来除了基本数据类型,基本类型数组,Class,String,以及Throwable之外的所有Java对象的数据类型,在JNI中都用jobject来表示,这就好比Native层void*一样。

    JNI类型签名

    我们之前在动态注册中有一段代码:

    static JNINativeMethod gMethods[]={
    
        …… 
    
    {
    
              "processFile"//Java中native的函数名
    
              //porcessFile的签名信息
    
    "(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V”,
    
    //JNI层对应的函数指针
    
              (void *)android_media_mediaScanner_processFile   
    
    },
    
    ……
    
    };
    
     

    中间的这一段代码的含义是什么呢:

    "(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V”,

    这段代码表示java中对应函数的签名信息,包含了参数类型和返回值类型。JNI中这么做是由于,在java中支持函数的重载,也就是函数命可以相同,但是函数的参数个数及类型不同,因此仅仅通过函数名是不能找到具体函数的,因此JNI中就将参数类型和返回值类型共同组成一个签名信息,有了签名信息和函数名,就能够找到java中的函数了。

             那么签名的格式规定为:

    (参数1类型标示;参数2类型标示……;参数n类型标示)返回类型标示

    因此 void processFile(String path,String mimeType,MediaScannerClient client)对应的JNI函数签名就为:

    (Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V

    当参数的类型为引用类型是,格式为“L包名”,包名中的“.”换成“/”即可。

    因此上面的Ljava/long/String就标示java.long.String类型(String类型)最右边的V表示返回值类型,void类型的对应标示为V.常见的标示见下表:

    类型标示

    Java类型

    类型标示

    Java类型

    Z

    boolean

    F

    float

    B

    byte

    D

    double

    C

    char

    L/java/langaugeString

    String

    S

    short

    [I

    int []

    I

    int

    [L/java/lang/object

    Object[]

    J

    long

     

    虽然签名比较麻烦,但是Java中有提供一个叫Javap的工具能帮助生成签名信息,格式为:javap –s –p ****;其中****味编译后的class文件,s标示输出内部数据类型的签名信息,p标示打印出所有public类型的函数和成员签名信息

  • 相关阅读:
    数据库回滚解决删除数据库出错
    教大家支付宝抢红包
    MySql 连接字符串
    搭建Git服务器
    队列的顺序存储结构
    栈的应用---递归
    栈的链式存储结构及应用(C、Java代码)
    栈的顺序存储结构及应用(C、Java代码)
    静态链表、循环链表、双向链表(C代码实现)
    线性表的链式存储结构(Java代码实现)
  • 原文地址:https://www.cnblogs.com/qgli/p/3117299.html
Copyright © 2020-2023  润新知