安卓进阶Android知识Android开发

深入理解“JNI”

2017-04-03  本文已影响367人  程序员丶星霖

深入理解“JNI”

一、JNI概述

JNI是Java Native Interface的缩写,中文译为“Java本地调用”。
JNI是一种技术,通过它可以做到:

JNI技术的推出有以下几个方面的意义:

在Android平台中,JNI就是一座将Native和Java连接起来的桥梁,将两种语言紧密地联系在了一起。


JNI示意图.png

二、MediaScanner

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

MediaScanner和其相关的JNI如下图所示:

MediaScannerJNI.png

从上图可知:

三、Java层的MediaScanner分析

MediaScanner的源码中与JNI有关部分的代码如下所示:

public  class  MediaScanner
{
    static{//static语句
        /*
        1、加载对应的JNI库,media_jni是JNI库的名字。
        在实际加载动态库的时候会将其扩展成libmedia_jni.so,
        在Windows平台上则会扩展为media_jni.dll
         */
         System.loadLibrary("media_jni");
         native_init();//调用native_init函数
    }

    //非native函数
    public  void  scanDirectories(String [] directories,String  volumeName){
        ......
    }

    //2、声明一个native函数。native为Java的关键字,表示它将由JNI层完成。
    private static native final void native_init();

    private native void processFile(String path,String mimeType,MediaScannerClient client);
}

代码中有两个要点:

  1. 加载JNI库;
  2. Java的native函数。

1.加载JNI库

如果Java要调用native函数,就必须通过一个位于JNI层的动态库(运行时加载的库)来实现。

在什么时候以及什么地方加载动态库呢?
原则上:在调用native函数前,任何时候、任何地方加载都是可以的。通常的做法是在类的static语句中加载,调用System.loadLibrary方法就可以了。System.loadLibrary函数的参数是动态库的名字,即media_jni。

2.Java的native

从代码中可知,native_init和processFile函数前都有Java的关键字native,它表示这两个函数将由JNI层来实现。

使用JNI的步骤:

  1. 加载对应的JNI库。
  2. 声明由关键字native修饰的函数。

四、JNI层MediaScanner的分析

MediaScanner的JNI层代码在android_media_MediaScanner.cpp中。

//1.这个函数是native_init的JNI层实现。
static void android_media_MediaScanner_native_init(JNIEnv *env)
{
    jclass clazz;
    clazz = env->FindClass("android/media/MediaScanner");
    ......
    fields.context = env->GetFieldID(clazz,"mNativeContext","I");
    ......
    return;
}
//2.这个函数是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);
    }
}

1.注册JNI函数

native_init函数对应的JNI函数是android_media_MediaScanner_native_init。native_init函数位于android.media包中,其全路径名是android.media.MediaScanner.native_init。而JNI函数的名字是android_media_MediaScanner_native_init。因为在Native语言中,符号"."有特殊的意义,所以JNI层需要把Java函数名称中的"."换成"_"。

JNI函数的注册,就是将Java层的native函数和JNI层对应的实现函数关联起来,有了这种关联,调用Java层的native函数的时候就可以顺利转到JNI层对应的函数执行了。

JNI函数的注册方式有如下两种:
1.静态方法:根据函数名来找对应的JNI函数,需要Java的工具程序javah参与。整体流程如下所示:

静态方法中native函数找到对应的JNI函数的过程:
当Java层调用native_init函数时,它会从对应的JNI库中寻找Java_android_media_MediaScanner_native_linit函数,如果没有,就会报错。如果找到,则会为这个native_init和Java_android_media_MediaScanner_native_linit建立一个关联关系,其实就是保存JNI层函数的函数指针。以后再调用native_init函数时,直接使用这个函数指针就可以了,这项工作是由虚拟机完成的。

静态方法是根据函数名来建立Java函数和JNI函数之间的关联关系的,它要求JNI层函数的名字必须遵循特定的格式。主要有以下几个弊端:

2.动态注册:在JNI技术中,用JNINativeMethod来记录Java native函数和JNI函数的一一对应关系。
动态注册只用完成下面两个函数即可:

/*
    env指向一个JNIEnv结构体。classname为对应的Java类名,由于JNINativeMethod中使用的函数名并非全路径名,所以要指明是哪个类。
 */
jclass clazz = (*env)->FindClass(env,className);
//调用JNIEnv的RegisterNatives函数,注册关联关系。
(*env)->RegisterNative(env,clazz,gMethods,numMethods);

在什么时候和什么地方调用这些动态注册的函数?
当Java层通过System.loadLibrary加载完JNI动态库后,紧接着会查找该库中一个叫JNI_OnLoad的函数。如果有,就调用它,而动态注册的工作就是在这里完成的。

2.数据类型转换

Java数据类型分为基本数据类型和引用数据类型两种,JNI层也是区别对待这两种数据类型的。

1、基本数据类型的转换
基本数据类型的转换关系如下表所示:

Java Native类型 符号属性 字长
boolean jboolean 无符号 8位
byte jbyte 无符号 8位
char jchar 无符号 16位
short jshort 有符号 16位
int jint 有符号 32位
long jlong 有符号 64位
float jfloat 有符号 32位
double jdouble 有符号 64位

2、引用数据类型的转换
引用数据类型的转换关系如下所示:

Java引用类型 Native类型
All objects jobject
java.lang.Class实例 jclass
java.lang.String实例 jstring
Object[] jobjectArray
boolean[] jobjectArray
byte[] jbyteArray
java.lang.Throwable实例 jthrowable
char[] jcharArray
short[] jshortArray
int[] jintArray
long[] jlongArray
float[] floatArray
double[] jdoubleArray

3.JNIEnv介绍

JNIEnv是一个与线程相关的代表JNI环境的结构体,其内部结构如下所示:

JNIEnv内部结构简图.png

如上图所示,JNIEnv实际上就是提供了一些JNI系统函数。通过它们可以做到:

如果线程A中有一个JNIEnv,线程B有一个JNIEnv。由于线程相关,所以不可以在线程B中使用线程A的JNIEnv结构体。

JavaVM和JNIEnv有什么关系?

4.JNIEnv操作jobject

一个Java对象是由什么组成的?Java对象是由成员变量和成员函数。
操作jobject的本质就应当是操作对象的成员变量和成员函数。

1、jfieldID和jethodID介绍
在JNI规则中,用jfieldID和jmethodID来表示Java类的成员变量和成员函数,可以通过JNIEnv的下面两个函数得到:

上面的参数中:

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

2、使用jfieldID和jmethodID
通过JNIEnv输出CallVoidMethod,再把jobject、jMethodID和对应的参数传进去,JNI层就能够调用Java对象的函数。

实际上JNIEnv输出了一系列类似CallVoidMethod的函数,形式为NativeType Call<type> Method(JNIEnv *env , jobject obj , jmethodID methodID , ...)。其中type对应Java函数返回值类型。

如下所示是常用的Get/Set函数:

Get函数 Set函数
GetObjectField() SetObjectField()
GetBooleanField() SetBooleanField()
GetByteField() SetByteField()
GetCharField() SetCharField()
GetShortField() SetShortField()
GetIntField() SetIntField()
GetLongField() SetLongField()
GetFloatField() SetFloatField()
GetDoubleField() SetDoubleField()

5.jstring介绍

JNI中的jstring类型表示Java中的String类型。下面是几个关于jstring的函数:

6.JNI类型签名

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

JNI规范定义的函数签名信息的格式是:
(参数1类型标示参数2类型标示...参数n类型标示)返回值类型标示

常见的类型标示如下表:

类型标示 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  xxx

参数说明如下:

7.垃圾回收

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判断它是否被回收了。

8.JNI中的异常处理

在JNI层中产生的异常不会中断本地函数的运行,但是一旦产生异常后,就只能做一些资源清理工作了(例如释放全局引用,或者ReleaseStringChars)。如果这时调用除前面所说的函数之外的其他JNIEnv函数,则会导致程序死掉。

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

好了,以上都是学习《深入理解Android:卷1》的笔记,分享给大家。

二维码.jpg
上一篇下一篇

猜你喜欢

热点阅读