深入理解Android-JNI

JNI概述

  • Java程序中的函数可以调用Native语言写的函数,Native一般指的是C/C++写的函数
  • Native层中的函数可以调用Java层的函数,也就是在C/C++程序中调用Java的函数
    要想做到以上两点,JNI(Java Native Interface)技术应运而生。
    深入分析后可以知道,虽然Java代码具有平台无关性,但运行在具体平台上的Java虚拟机并不能做到这一点,但是JNI技术的出现屏蔽了不同平台的差异,使得上层的Java具有了平台无关的特性。
深入理解Android-JNI_第1张图片

MediaScanner的分析

android大量使用了JNI技术,书中分析了MediaScanner这个例子。

  1. Java世界对应的是MediaScanner类,这个类一些函数需要Native层来实现,比如对目录的扫描。
  2. Native层对应是libmedia.so,他完成了实际的功能。
  3. JNI层对应的是libmedia_jni.so,这里可以看出JNI层其实也是Native代码写的。其中下划线前的media是Native层库的名字,这里指的就死libmedia库,android对JNI库的名字有规范,一般是:
    lib模块名_jni.so
  4. Java层的MediaScanner将通过libmedia_jni.so来与Native层的libmedia.so交互。

Java层的MediaScanner

framewor/base/media/java/android/media/MediaScanner.java是MediaScanner在Java层源码的位置。

   public class MediaScanner
   {    
      static {
          /**加载对应的JNI库,media_jni是对应JNI库的名字,实际上加载动态库的时候他会拓展成libmedia_jni.so **/        
          System.loadLibrary("media_jni");        
          native_init();   //调用native_init()函数,他是一个native函数
       }
      ......
      //扫面目录的函数,里面用到了processDirectory这个native函数
      public void scanDirectories(String[] directories, String volumeName) {
      ......
      for (int i = 0; i < directories.length; i++) { 
            processDirectory(directories[i], mClient);
      }
      ......
      private native void processDirectory(String path, MediaScannerClient client);
      private static native final void native_init();
      ......

这里说明了两点内容,在需要native函数的java类中会包括JNI的加载和Native函数的声明,指示它由JNI层去完成。

  1. JNI加载流程
    Java层要调用Native函数,必须通过位于JNI层的动态库才能实现。动态库的必须在运行时加载,所以,我们通常的做法就是在类的static语句中加入System.loadLibrary方法完成对动态库的加载,系统会自动将参数转化成对应环境下真实动态库的名字。linux上是so文件,win下就是dll文件。
  2. Java的native函数
    加载完JNI库后,我们只需要在Java中声明由关键字native修饰的函数即可。是不是很简单,但是JNI层的MediaScanner就没那么轻松了。

JNI层的MediaScanner

JNI层的MediaScanner代码位于
frameworks/base/media/jni/android_media_MediaScanner.cpp
上面再Java层声明的native_init和processFile在这里都有具体的实现,那么问题就来了,Java层声明的函数如何才能对应到JNI层的。

  1. 注册JNI函数
深入理解Android-JNI_第2张图片

native_init函数位于android.media这个包中,这样可以得到它的全路径名称android.media.MediaScanner.native_init,之后再将.全部换成_,native_init就找到了JNI层的实现。
上述就是JNI函数注册的问题

  • 静态注册
    流程如下:


    深入理解Android-JNI_第3张图片

注意:因为packagename是该类完整的包名,所以javah命令需要在包外面执行,否则会找不到该类
静态注册的弊端:1、需要编译所有声明了native函数的java类。2、javah生成的jni函数名会特别长。 3、初次调用native函数时要根据函数名来搜索对应JNI层的函数,并建立关联,这会影响效率。

  • 动态注册
    既然Java Native函数和JNI函数一一对应,能不能有这种结构可以保存这种关联关系呢,肯定是有的,在JNI技术中有一种JNINativeMethod的结构,它记录下了这些关联关系。其实Android大部分采用的是动态注册的方法
    MediaScanner的JNI层中的示例:
    static const JNINativeMethod gMethods[] = {
    {
    "processDirectory",
    "(Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
    (void )android_media_MediaScanner_processDirectory
    },
    {
    "processFile",
    "(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
    (void )android_media_MediaScanner_processFile
    },
    .......
    {
    "native_init",
    "()V",
    (void )android_media_MediaScanner_native_init
    },
    ......
    };
    基本结构定义:
    typedef struct {
    //java类中native函数的名字,不用携带包名,例如“native_init”
    const char
    name;
    //java函数的签名信息,用字符串表示,是参数类型和返回值类型的组合
    const char
    signature;
    //JNI层对应函数的函数指针,它是void
    类型
    void* fnPtr;
    }JNINativeMethod;

    定义完毕之后,需要执行注册

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

    我们发现AndroidRunTime类提供了注册函数,而它又调用了JNIHelp中的jniRegisterNativeMethods方法,最终会走到JNIEnv的RegisterNatives完成注册工作。

那到底是什么时候去执行这个?
在Java层执行System.loadLibrary加载JNI动态库后,紧接着会查找该库中一个JNI_OnLoad的函数,如果有的话,动态注册就在这里完成。然而在MediaScanner对应的android_media_MediaScanner.cpp中并没有发现这个函数。由于多媒体系统中很对地方用到JNI,所以register_android_media_MediaScanner这个注册方法被放在了android_media_MediaPlayer.cpp的JNI_OnLoad方法中,当然还有其它的多媒体相关的注册函数。

数据类型转换

主要解决的问题是:在Java中调用native函数传递的参数是Java数据类型,如何将他们转化成Native的数据类型。
Java分基本数据类型和引用数据类型,先看基本数据类型:

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位

这里要注意的是转换成Native类型后对应的数据类型的字长,char在Java中是占两个字节,jchar同样也是,要区别于普通char只占一个字节。

引用类型的转换

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

再拿MediaScanner举例,其中的processFile
android_media_MediaScanner_processFile( JNIEnv *env, jobject thiz, jstring path, jstring mimeType, jobject client){
Java层的函数中只传了三个参数,即path,mineType和client,这里它们的类型都转化成了native类型。前两个参数分别是JNIEnv和Java层的MediaScanner对象(jobject)

JNIEnv

JNIEnv其实是一个与线程相关的代表JNI环境的结构体。


深入理解Android-JNI_第4张图片

JNIEnv提供了JNI相关的系统函数,这些函数可以做到:调用Java函数和操作jobject对象等。

  1. 通过JNIEnv操作jobject
    在JNI规则中,用jfiledID和jMethodID来表示java类的成员变量和成员函数。比如MyMediaScannerClient中。
    代码中将scanFile和handleStringTag函数的jmethodID保存为MyMediaScannerClient的成员变量,这是为了解决每次操作object都会去查询jmethodID或jfieldID而带来的运行效率降低。
    使用可以看virtual status_t scanFile中的mEnv->CallVoidMethod
    完成JNI层调用Java对象的函数。
  2. jstring
    JNIEnv提供有关jstring的函数
  3. JNI签名介绍
    Java函数支持函数重载,也就是说,可以定义同名但是不同参数的函数,但仅仅根据函数名是没法找到具体函数的。为了解决这个问题,JNI技术就将参数类型和返回值类型的组合作为一个函数的签名信息,有了这个签名和函数名,就能很顺利的找到Java中的函数。
  4. 垃圾回收
    JNI的三种类型引用技术:
    local reference:本地引用,一旦JNI函数返回时,jobject就可能会被回收
    Global reference:全局引用,不主动释放不会被回收
    Weak Global reference:特殊的全局引用,可能会被回收,每次使用时需要判断是否回收
  5. JNI中的异常处理


    深入理解Android-JNI_第5张图片

    JNIEnv提供了三个函数给予帮助
    ExceptionOccured函数,用来判断是否发生异常
    ExceptionClear函数,用来清理JNI层发生的异常。
    ThrownNew函数,用来Java层抛出异常。

你可能感兴趣的:(深入理解Android-JNI)