Bootstrap

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

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


本章关键点总结 & 说明:

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

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

说明:

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

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

首先从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进行解读

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 基本数据类型转换

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

2.2 引用数据类型转换

这里除了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类型。

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

同时给出 几个签名案例:

虽然函数签名信息很容易写错,但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的权威指南。

 

 

;