从MediaScanner入手深入学习JNI

JNI,是Java Native Interface的缩写,中文为Java本地调用。Android系统底层很多是用Native语言写的,比如Java虚拟机。其实Java一直在使用JNI技术。

由上图可知,Java层和Native层可以互相调用。Java中叫方法,C中叫函数

Java要调用Native层函数,就必须通过一个位于JNI层的动态库(运行时加载库)才能做到。

Java层的MediaScanner.java

public class MediaScanner implements AutoCloseable {
    static {
        //加载对应的JNI库,media_jni是JNI库的名字。
        //实际加载动态库的时候在Linux上会拓展成libmedia_jni.so,在Windows平台上将拓展为media_jni.dll
        System.loadLibrary("media_jni");
        native_init();
    }
    
    //native为Java的关键字,表示它将由JNI层完成。
    private static native final void native_init();
    
}
复制代码

Java完成下面两个步骤,就可以使用JNI了。

  1. 加载JNI层动态库: 理论上是在调用native方法前的任何时间、地点都可以加载,通常的做法是在类的static语句中通过System.loadLibrary("动态库名字")加载
  2. 声明由关键字native修饰的方法

JNI层的android_media_MediaScanner.cpp

//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;
}
复制代码

现在,JNI层都对Java层的方法进行了实现。那么如何保证调用Java方法时,会准确地调用的相对应的JNI函数呢?

JNI函数的注册

“注册”之意就是将Java层的native方法和JNI层对应的实现函数关联起来,有了这种关联,调用Java层的native方法时,就能顺利转到JNI层对应的函数执行了。

注册分为两种:动态注册和静态注册

动态注册

1. 怎么注册

显然,Java的Native方法和JNI层的函数一一对应,在JNI技术中一个叫JNINativeMethod的结构用来记录这种一一对应关系。

typedef struct {

    //Java中native方法的名字,不用携带包的路径。例如“native_init”。
    constchar* name;    

    //Java函数的签名信息,用字符串表示,是参数类型和返回值类型的组合。
    const char* signature;

    //JNI层对应函数的函数指针,注意它是void*类型。
    void*  fnPtr;  

} JNINativeMethod;
复制代码

所以,我们可以初始化这些JNINativeMethod结构体来建立这种一一对应的关联。

--> android_media_MediaScanner.cpp

//定义一个JNINativeMethod数组,其成员就是MediaScanner中所有native函数的一一对应关系。
static JNINativeMethod gMethods[] = {
    {
        "native_init",       
        "()V",                     
        (void *)android_media_MediaScanner_native_init //函数指针
    },
    {
        "processFile" //Java中native函数的函数名。
        //processFile的签名信息,签名信息的知识,后面再做介绍。
        "(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",   
         (void*)android_media_MediaScanner_processFile //JNI层对应函数指针。
    },
    ......
};

//注册JNINativeMethod数组
int register_android_media_MediaScanner(JNIEnv*env){
   //调用AndroidRuntime的registerNativeMethods函数,第二个参数表明是Java中的哪个类
    return AndroidRuntime::registerNativeMethods(env,"android/media/MediaScanner", gMethods, NELEM(gMethods));
}
复制代码

--> AndroidRuntime.cpp

int AndroidRuntime::registerNativeMethods(JNIEnv*env,constchar* className, const JNINativeMethod* gMethods, int numMethods){

    //调用jniRegisterNativeMethods函数完成注册
    return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}
复制代码

--> JNIHelp.c

//Android平台中,为了方便JNI使用而提供的一个帮助函数。
int jniRegisterNativeMethods(JNIEnv* env, constchar* className,constJNINativeMethod* gMethods, int numMethods){

    jclassclazz;
    clazz= (*env)->FindClass(env, className);
    ......

    //实际上是调用JNIEnv的RegisterNatives函数注册关联关系
    if((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0) {
       return -1;
    }
    return 0;
}
复制代码

总结一下就是,JNINativeMethod记录Java方法和JNI函数的关联关系,最终调用JNIEnv的函数来初始化JNINativeMethod数组

1. clazz= (*env)->FindClass(env, className);

2. (*env)->RegisterNatives(env, clazz, gMethods, numMethods)
复制代码

2. 何时注册,何处注册

上面说明了如何注册 JNINativeMethod数组。那么问题来了,我们什么时候去注册 JNINativeMethod数组呢?答案即将揭晓。 还记得,Java层的方法吗?

public class MediaScanner implements AutoCloseable {
    static {
        //加载对应的JNI库,media_jni是JNI库的名字。
        //实际加载动态库的时候在Linux上会拓展成libmedia_jni.so,在Windows平台上将拓展为media_jni.dll
        System.loadLibrary("media_jni");
        native_init();
    }
    
    //native为Java的关键字,表示它将由JNI层完成。
    private static native final void native_init();
}
复制代码

当Java层通过System.loadLibrary加载完JNI动态库后,紧接着会查找该库(libmedia_jni.so)中一个叫JNI_OnLoad 的函数。如果有这个函数,就调用它。所以,我们就可以在 JNI_OnLoad函数中,进行JNI函数的注册。所以,要实现动态注册的话,就要实现JNI_OnLoad函数。

libmedia_jni.so的JNI_OnLoad函数是在哪里实现的呢?由于多媒体系统很多地方都使用了JNI,所以码农把它放到android_media_MediaPlayer.cpp中了 -->android_media_MediaPlayer.cpp

jint JNI_OnLoad(JavaVM* vm, void* reserved){
    JNIEnv* env = NULL;
    jintresult = -1;
    
    if(vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
        gotobail;
    }
    ...... 
    
    //动态注册MediaScanner的JNI函数。
    if(register_android_media_MediaScanner(env) < 0) {
        goto bail;
    }
    ......
    
    return JNI_VERSION_1_4;//必须返回这个值,否则会报错。
}
复制代码

果不其然,调用了android_media_MediaScanner.cppregister_android_media_MediaScanner进行动态注册。

静态注册:根据函数名来找对应的JNI函数

1. 生成JNI头文件

这种方法需要Java的工具程序javah参与,整体流程如下:

  1. 先编写Java代码,然后编译生成.class文件。
  2. 使用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。

/* DO NOT EDIT THIS FILE - it is machinegenerated */

#include   //必须包含这个头文件,否则编译通不过

/* Header for class android_media_MediaScanner*/

#ifndef _Included_android_media_MediaScanner
#define _Included_android_media_MediaScanner

extern "C" {
    ......
    
    //native_init对应的JNI函数,Java层函数名中如果有一个”_”的话,转换成JNI后就变成了”_l”:native_init -> native_linit
    JNIEXPORT void JNICALL Java_android_media_MediaScanner_native_1init(JNIEnv*, jclass);
}
复制代码

2. 何时注册,如何注册

当Java层调用native_init方法时,它会从对应的JNI头文件查找Java_android_media_MediaScanner_native_linit。如果没有,就会报错;如果找到,则会为这个native_init和Java_android_media_MediaScanner_native_linit建立一个关联关系(注册),其实就是保存JNI层函数的函数指针。以后再调用native_init函数时,直接使用这个函数指针就可以了,当然这项工作是由虚拟机完成的。

综上,静态注册有以下弊端:

  • 需要编译所有声明了native函数的Java类,每个生成的class文件都得用javah生成一个头文件
  • JNI层函数的名字必须遵循特定的格式, javah生成的JNI层函数名特别长,书写起来很不方便
  • 初次调用native函数时要根据方法名字搜索对应的JNI层函数来建立关联关系,这样会影响运行效率

动态、静态注册对比

动态注册,直接在JNINativeMethod结构体中说明了相应的JNI层的函数指针。这使得Java层调用时通过方法名找到结构体数组中的对应结构体,就可以直接找到对应的JNI层函数指针。

static JNINativeMethod gMethods[] = {
    {
        "native_init",       
        "()V",                     
        (void *)android_media_MediaScanner_native_init //函数指针
    },
    {
        "processFile" //Java中native函数的函数名。
        //processFile的签名信息,签名信息的知识,后面再做介绍。
        "(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",   
         (void*)android_media_MediaScanner_processFile //JNI层对应函数指针。
    },
    ......
};
复制代码

而静态注册,Java层首次调用时,要根据方法名字在javah头文件中搜索对应的JNI层函数,并建立关联关系,下次才能直接使用这个函数指针。 一个是一开始就有方法和函数指针的对应关系(结构体);一个是要先通过方法名建立方法和函数指针的对应关系。所以,动态注册效率会好一些。


Java层和JNI层的数据类型转换

Java层

public class MediaScanner implements AutoCloseable {
    static {
        //加载对应的JNI库,media_jni是JNI库的名字。
        //实际加载动态库的时候在Linux上会拓展成libmedia_jni.so,在Windows平台上将拓展为media_jni.dll
        System.loadLibrary("media_jni");
        native_init();
    }
    
    //native为Java的关键字,表示它将由JNI层完成。
    private static native final void native_init();
    
    private native void processFile(String path, String mimeType,MediaScannerClient client);
}
复制代码

JNI层

//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;
}

//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);
    ......

    constchar *pathStr = env->GetStringUTFChars(path, NULL);
    ......

    if(mimeType) {
       env->ReleaseStringUTFChars(mimeType, mimeTypeStr);
    }
}
复制代码

对比上面两端代码可以发现,Java层和JNI层方法/函数 的参数类型不同。这是因为Java层和JNI层存在数据类型转换。

基本数据类型转换关系表

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

引用数据类型的转换表

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

从上表可知,All objects -> jobject,所有的引用数据类型在JNI层都是jobject。如果一个方法有多个引用数据的参数,那JNI层把它们都当成是jobject,怎么使用它们呢?

JNIEnv理解

Java层和JNI层方法/函数参数个数不同,都有JNIEnv

JNI类型签名介绍

垃圾回收

其他方面的深入了解请看书。

总结自:《深入了解Android卷I》-邓凡平

转载于:https://juejin.im/post/5b42a9a86fb9a04f8a216b67

你可能感兴趣的:(从MediaScanner入手深入学习JNI)