android系统核心机制 基础(08)JNI 基础

2021年11月20日 阅读数:4
这篇文章主要向大家介绍android系统核心机制 基础(08)JNI 基础,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

该系列文章总纲连接:android 系统核心机制基础 系列文章目录java


本章关键点总结 & 说明:

android系统核心机制 基础(08)JNI 基础_android

这里导图主要关注➕左上角 JNI基础部分便可。主要从注册、数据类型、签名、垃圾回收、异常处理几个角度解读了下JNI基础,由于这张图有点大,所以JNI部分单独截了个图,以下所示:android

android系统核心机制 基础(08)JNI 基础_头文件_02

这样就清晰多了。接下来我么继续解读JNI,JNI是Java Native Interface的缩写,经过该计数可实现java方法与Native层方法相互调用。这里以MediaScanner为例分析下JNI。首先看下图:数据库

android系统核心机制 基础(08)JNI 基础_成员变量_03

说明:数组

  1. Java层的MediaScanner类有一些函数是须要由Native层实现
  2. JNI层对应的是libmedia_jni.so
  3. Native层对应的是libmedia.so,该库完成实际功能
  4. MediaScanner Java层将经过libmedia_jni.so和Native的libmedia.so交互

MediaScanner功能:扫描媒体文件,获得媒体信息,存入到媒体数据库,供其余应用程序使用ide

首先从java层入手,构造器代码以下:函数

public class MediaScanner
{
    static {
        //加载JNI库
        System.loadLibrary("media_jni");
        //native方法
        native_init();
    }
    //native关键字:由native层完成,java层仅声明
    private static native final void native_init();
    //...
}

由此总结下,JNI技术对于Java层来讲,只要完成下面两项工做便可使用JNI,分别是:工具

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

继续分析 JNI层对应的native_init代码,android_media_MediaScanner.cpp中实现以下:学习

static void android_media_MediaScanner_native_init(JNIEnv *env)
{
    ALOGV("native_init");
    jclass clazz = env->FindClass(kClassMediaScanner);
    if (clazz == NULL) {
        return;
    }

    fields.context = env->GetFieldID(clazz, "mNativeContext", "J");
    if (fields.context == NULL) {
        return;
    }
}

这里关注下,如何知道Java层的native_init函数对应的是JNI层的android_media_MediaScanner_native_init?必定是哪里作了关联,其实是由于注册了JNI函数,接下来对注册JNI进行解读spa

1 JNI注册解读线程

native_init函数位于android.media这个包中,它的全路径名应该是android.media.MediaScanner.native_init,而JNI层函数的名字是android_media_MediaScanner_native_init。由于在Native语言中,符号“.”有着特殊的意义,因此JNI层须要把“.”换成“_”。也就是经过这种方式,native_init找到了本身JNI层的本家兄弟android.media.MediaScanner.native_init

“注册”之意就是将Java层的native函数和JNI层对应的实现函数关联起来,而JNI函数的注册实际上有两种方法:

@1 静态注册

编写Java代码,编译生成.class文件,使用Java的工具程序javah,如:javah –o output packagename.classname

这样它会生成一个叫output.h的JNI层头文件。其中packagename.classname是Java代码编译后的class文件,而在生成的output.h文件里,声明了对应的JNI层函数,只要实现里面的函数便可。这个头文件的名字通常都会使用packagename_class.h的样式,例如MediaScanner对应的JNI层头文件就是android_media_MediaScanner.h。这种方式生成的头文件以下:

#include <jni.h>  //必须包含这个头文件,不然编译通不过
#ifndef _Included_android_media_MediaScanner
#define _Included_android_media_MediaScanner
#ifdef __cplusplus
extern "C" {
#endif
//...
//native_init对应的JNI函数
JNIEXPORT void JNICALLJava_android_media_MediaScanner_native_1init(JNIEnv*, jclass); 
#ifdef __cplusplus
}
#endif
#endif

从上面代码中能够发现,native_init的JNI层函数被声明成:

//Java层函数名中若是有一个”_”的话,转换成JNI后就变成了”_l”
JNIEXPORT void JNICALLJava_android_media_MediaScanner_native_1init

静态注册的过程总结:Java层调用native_init函数时,会从对应JNI库Java_android_media_MediaScanner_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用来记录这种关系,定义以下:

typedef struct {
    //Java中native函数的名字,不用携带包的路径。例如“native_init“。
	constchar* name;    
	//Java函数的签名信息,用字符串表示,是参数类型和返回值类型的组合。
    const char* signature;
    void*       fnPtr;  //JNI层对应函数的函数指针,注意它是void*类型。
} JNINativeMethod;

看MediaScanner JNI层如何使用该结构体,对应结构体代码以下:

static JNINativeMethod gMethods[] = {
    //...
    {
        "native_init",
        "()V",
        (void *)android_media_MediaScanner_native_init
    },
    //...
    {
        "native_finalize",
        "()V",
        (void *)android_media_MediaScanner_native_finalize
    },
};

注册使用的方法以下:

int register_android_media_MediaScanner(JNIEnv *env)
{
    return AndroidRuntime::registerNativeMethods(env,
                kClassMediaScanner, gMethods, NELEM(gMethods));
}

AndroidRunTime类提供了一个registerNativeMethods函数完成注册工做,代码以下:

int AndroidRuntime::registerNativeMethods(JNIEnv* env,
    const char* className, const JNINativeMethod* gMethods, int numMethods)
{
    return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}

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

int jniRegisterNativeMethods(JNIEnv* env, constchar* className,constJNINativeMethod* gMethods, int numMethods)
{
    jclassclazz;
    //关键点1,寻找全路径,查找类名
    clazz= (*env)->FindClass(env, className);	
    //...
	//关键点2,实际调用JNIEnv的RegisterNatives函数完成注册
    if((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0) {
       return -1;
    }
    return0;
}

这里关键点有2个,如上代码所示,在本身的JNI层代码中使用这种方法,就能够完成动态注册。

动态注册的函数如何被调用?当Java层经过System.loadLibrary加载完JNI动态库后,接着会查找该库中一个叫JNI_OnLoad的函数,若是有就调用它,而动态注册的工做就是在这里完成的。因此,若是想使用动态注册方法,就必需要实现JNI_OnLoad函数,只有在这个函数中,才有机会完成动态注册的工做。静态注册则没有这个要求。那么会过来,libmedia_jni.so的JNI_OnLoad函数是如何实现呢,代码以下:

 //该函数的第一个参数类型为JavaVM,这但是虚拟机在JNI层的表明喔,每一个Java进程只有一个这样的JavaVM
jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
{
    JNIEnv* env = NULL;
    jint result = -1;

    if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
        ALOGE("ERROR: GetEnv failed\n");
        goto bail;
    }
    assert(env != NULL);
    //动态各类注册函数...
    if (register_android_media_MediaCodecList(env) < 0) {
        ALOGE("ERROR: MediaCodec native registration failed");
        goto bail;
    }
    //动态各类注册函数...

    /* success -- return valid version number */
    result = JNI_VERSION_1_4;

bail:
    return result;
}

注意:JNI层代码中通常要包含jni.h这个头文件。Android源码中提供了一个帮助头文件JNIHelp.h,它内部其实就包含了jni.h,因此咱们在本身的代码中直接包含这个JNIHelp.h便可。

2 数据类型转换

在Java中调用native函数传递的参数是Java数据类型,分为基本数据类型和引用数据类型两种,JNI层也是区别对待这两者的。

2.1 基本数据类型转换

android系统核心机制 基础(08)JNI 基础_动态注册_04

注意:转换成Native类型后对应数据类型的字长,例如jchar在Native语言中是16位,占两个字节,这和普通的char占一个字节的状况彻底不同。

2.2 引用数据类型转换

android系统核心机制 基础(08)JNI 基础_头文件_05

这里除了Java中基本数据类型的数组、Class、String和Throwable外,其他全部Java对象的数据类型在JNI中都用jobject表示。若是对象类型都用jobject表示,就比如是Native层的void*类型同样,对码农来讲,是彻底透明的。既然是透明的,那如何使用和操做呢?先看这组函数的参数对比:

//Java层processFile有三个参数。
processFile(String path, StringmimeType,MediaScannerClient client);
//JNI层对应的函数,最后三个参数和processFile的参数对应。
android_media_MediaScanner_processFile(JNIEnv*env, jobject thiz,
jstring path, jstring mimeType, jobject client)

这里专一于android_media_MediaScanner_processFile的前两个参数,

第一个参数:JNIEnv,接下来开始解读

第二个参数:jobject表明Java层的MediaScanner对象,它表示是在哪一个MediaScanner对象上调用的processFile。若是Java层是static函数,那么这个参数将是jclass,表示是在调用哪一个Java Class的静态函数。

接下来解读 JNIEnv

@1 JNI介绍

JNIEnv是一个和线程相关的,表明JNI环境的结构体,JNIEnv提供了一些JNI系统函数。经过这些函数能够作到:调用Java的函数
和操做jobject对象,在以前JNI_OnLoad函数中,第一个参数是JavaVM,不论进程中有多少个线程,JavaVM倒是独此一份;而JavaVM和JNIEnv的关系就如进程与线程通常,说的详细一点,以下:

  1. 调用JavaVM的AttachCurrentThread函数,获得这个线程的JNIEnv结构体,就能够在后台线程中回调Java函数
  2. 后台线程退出前,须要调用JavaVM的DetachCurrentThread函数来释放对应的资源。

@2 操做jobject

先从另一个角度来解释这个问题。一个Java对象是它的成员变量和成员函数组成。那么操做jobject的本质就应当是操做这些对象的成员变量和成员函数。所以这里从jfieldID和jmethodID的介绍和使用来解读。

@@2.1 jfieldID和jmethodID的介绍

在JNI规则中,用jfieldID 和jmethodID 来表示Java类的成员变量和成员函数,它们经过JNIEnv的下面两个函数能够获得:

jfieldID GetFieldID(jclass clazz,const char*name, const char *sig);
jmethodID GetMethodID(jclass clazz, const char*name,const char *sig);

参数中jclass表明Java类,name表示成员函数或成员变量的名字,sig为这个函数和变量的签名信息。成员函数和成员变量都是类的信息,这两个函数的第一个参数都是jclass。jmethodID的获取案例以下所示:

MyMediaScannerClient(JNIEnv *env, jobjectclient)......
{
	//先找到android.media.MediaScannerClient类在JNI层中对应的jclass实例。
	jclass mediaScannerClientInterface =
	env->FindClass("android/media/MediaScannerClient");
 	//取出MediaScannerClient类中函数scanFile的jMethodID。
	mScanFileMethodID = env->GetMethodID(mediaScannerClientInterface, "scanFile",
                           "(Ljava/lang/String;JJ)V");
    //...
}

@@2.2 jfieldID和jmethodID的使用

取出jmethodID后,使用案例以下:

virtualbool scanFile(const char* path, long long lastModified,long long fileSize)
{
    jstring pathStr;
    if((pathStr = mEnv->NewStringUTF(path)) == NULL) return false;     
	/*
	调用JNIEnv的CallVoidMethod函数:
	第一个参数:MediaScannerClient的jobject对象,
	第二个参数:函数scanFile的jmethodID,后面是Java中scanFile的参数。
	*/
    mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr,lastModified, fileSize);
    mEnv->DeleteLocalRef(pathStr);
    return (!mEnv->ExceptionCheck());
}

经过JNIEnv输出的CallVoidMethod,再把jobject、jMethodID和对应参数传进去,JNI层就可以调用Java对象的函数。事实上JNIEnv输出了一系列相似CallVoidMethod的函数,形式为:

NativeType Call<type>Method(JNIEnv *env,jobject obj,jmethodID methodID, ...)。

一样,对于jfieldID操做jobject的成员变量,形式以下:

//得到fieldID后,可调用Get<type>Field系列函数获取jobject对应成员变量的值。
NativeType Get<type>Field(JNIEnv *env,jobject obj,jfieldID fieldID)

//或者调用Set<type>Field系列函数来设置jobject对应成员变量的值。
void Set<type>Field(JNIEnv *env,jobject obj,jfieldID fieldID,NativeType value)

综上,JNI函数中在被调用时,第一个参数JNIEnv表示对应线程,第二个thiz表示对应类的java对象。

2.3 jstring类型特殊说明

Java中的String也是引用类型,因为使用很是频繁,在JNI规范中单首创建了一个jstring类型来表示Java中的String类型。通常咱们会依靠JNIEnv来操做jstring,看几个有关jstring的函数:

  1. 调用JNIEnv的NewString(JNIEnv *env, const jchar*unicodeChars,jsize len),能够从Native的字符串获得一个jstring对象。
  2. 调用JNIEnv的NewStringUTF将根据Native的一个UTF-8字符串获得一个jstring对象

上面两个函数将本地字符串转换成Java的String对象,JNIEnv还提供了GetStringChars和GetStringUTFChars函数,将Java String对象转换成本地字符串。

  1. GetStringChars获得一个Unicode字符串
  2. GetStringUTFChars获得一个UTF-8字符串。

注意:作完相关工做后,调用ReleaseStringChars或ReleaseStringUTFChars函数来释放资源,不然会致使JVM内存泄露。

一个使用案例以下:

static void android_media_MediaScanner_processFile(JNIEnv*env, jobject thiz, jstring path, jstring mimeType, jobject client)
{
    MediaScanner *mp = (MediaScanner *)env->GetIntField(thiz,fields.context);
    //...
	//调用JNIEnv的GetStringUTFChars获得本地字符串pathStr
    constchar *pathStr = env->GetStringUTFChars(path, NULL);
	//...
	//使用完后,必须调用ReleaseStringUTFChars释放资源
    env->ReleaseStringUTFChars(path, pathStr);
    //...
}

3 类型签名

3.1 为何须要签名sig?

由于Java支持函数重载,能够定义同名但不一样参数的函数。但仅仅根据函数名,是无法找到具体函数的。为了解决这个问题,JNI技术中就使用了参数类型和返回值类型的组合,做为一个函数的签名信息,有了签名信息和函数名,就能很顺利地找到Java中的函数。

3.2 JNI规范定义的函数格式案例

它的格式是:(参数1 类型标示 参数2 类型标示 ...参数n 类型标示) 返回值类型标示

以案例进行说明:

void processFile(String path, String mimeType)
对应的JNI函数签名就是
(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V

解读:括号内是参数类型的标示,最右边是返回值类型的标示,void类型对应的标示是V。当参数的类型是引用类型时,其格式是”L包名;”,其中包名中的”.”换成”/”。上面例子中的Ljava/lang/String;表示是一个Java String类型。

常见的函数签名格式以下:

android系统核心机制 基础(08)JNI 基础_成员变量_06

同时给出 几个签名案例:

android系统核心机制 基础(08)JNI 基础_android_07

虽然函数签名信息很容易写错,但Java提供一个叫javap的工具能帮助生成函数或变量的签名信息,用法以下:

javap –s -p xxx。其中xxx为编译后的class文件,s表示输出内部数据类型的签名信息,p表示打印全部函数和成员的签名信息,而默认只会打印public成员和函数的签名信息。

4 垃圾回收

为了解决垃圾回收问题,JNI技术一共提供了三种类型的引用,它们分别是:

  1. Local Reference:本地引用。在JNI层函数中使用的非全局引用对象都是Local Reference。它包括函数调用时传入的jobject、在JNI层函数中建立jobject。LocalReference在JNI层函数返回后,这些jobject就可能被垃圾回收。
  2. Global Reference:全局引用,这种对象如不主动释放,就永远不会被垃圾回收,留意主动释放便可。
  3. Weak Global Reference:弱全局引用,一种特殊的GlobalReference,在运行过程当中可能会被垃圾回收。因此在程序中使用它以前,须要调用JNIEnv的IsSameObject判断它是否是被回收了。

5 异常处理

JNI中也有异常,只是和C++、Java的不同。当调用JNIEnv的某些函数出错产生异常,它不会中断本地函数的执行,直到从JNI层返回到Java层后,虚拟机才会抛出这个异常。虽然在JNI层中产生的异常不会中断本地函数的运行,但一旦产生异常后,就只能作一些资源清理工做了(例如释放全局引用,或者ReleaseStringChars)。若是这时调用除上面所说函数以外的其余JNIEnv函数,则会致使程序死掉。这里给出一个简单的异常案例,代码以下:

virtualbool scanFile(const char* path, long long lastModified,long long fileSize)
{
       jstring pathStr;
       //NewStringUTF调用失败后,直接返回。
       if((pathStr = mEnv->NewStringUTF(path)) == NULL) return false;
       //...
}

JNI层函数能够在代码中截获和修改这些异常,JNIEnv提供了三个函数进行帮助:

  1. ExceptionOccured函数,用来判断是否发生异常。
  2. ExceptionClear函数,用来清理当前JNI层中发生的异常。
  3. ThrowNew函数,用来向Java层抛出异常。

同时 关注JDK文档中的《Java Native Interface Specification》,它是深刻学习JNI的权威指南。