JNI的基本介绍

首先,我们都知道Android的内核是修改过的linux内核,然后Android应用是用Java的方言写的。

 

JNI(Java Native Interface)是Dalvik(包括其他JVM)和Linux之间的一个接口,JNI可以使得Java程序直接调用OS上原生语言开发的lib。Java追求平台无关性,但JVM不是万能的。当我们出于开发效率、执行效率的目的希望使用原生lib的时候我们就需要JNI来完成:Java -> Native Language的调用。

 

在Android的源码里面大量地使用了JNI,android/media/MediaScanner.java就是一个典型的JNI调用,它对应的Native实现(C/C++)是:/media/jni/MediaScanner.cpp和/media/jni/android_media_MediaPlayer.cpp,后者是JNI层上MediaScanner对应的类,通过JNI的调用规则,JVM对MediaScanner类的方法调用,最终会转化为android_media_MediaPlayer当中对应方法的调用。

 

首先我们来讲讲JNI的原理,实际上和其它一切所谓跨平台的Object Access技术一样,JNI本质上就是一系列精心设计的Hard Coding,关于数据类型和方法签名的Hard Coding。

 

首先是数据类型,JNI定义了Java数据类型在Native当中的对应类型,例如Java基本类型int,在JNI当中就定义了jint作为它的对应Native类型(其实是一个struct);对于引用类型任何Object的子类,JNI映射为jobject对象;而对于数组类型,JNI定义了jintArray,jobjectArray等类型,分别对应到Java当中的int[],和Object子类的数组引用(例如:MyClass[])。鉴于Java调用JNI接口时特殊的异常处理过程,还定义了Java中Throwable的对应类型,jthrowable(具体的异常处理方法,后面会再提及)。

 

那么方法签名呢?JNI实际上解决的是Java函数调用和Native函数之间的1:1对应问题,实现的方法大概是这样的,使用一个结构将Java函数的signature和Native函数的函数指针对应起来。具体的实现是一个叫JNINativeMethod的结构:

typedef struct {

   //Java函数名

   const char* name;

   //Java函数的签名除了函数名以外的部分:包括参数和返回值

   const char* signature;

   //Native函数指针

   void* fnprt;

} JNINativeMethod;

因为Java函数的签名是唯一的,所以这个结构记录了Java函数和Native函数的对应关系。

 

操作这个结构的方法,有两种,一种是静态编译的注册方法:

使用Java Tool里面的:javah -o <输出的头文件> <JNI Java 类的ClassPath>

生成一个头文件,这个头文件里面Hard Coding了Java类中的所有Method的Native 函数定义,例如android.media.MediaScanner.processFile,对应的Native函数定义就是:

JNIEXPORT void JNICALL Java_android_media_MediaScanner_processFile(...)

从这个函数名我们就能看到完美的Hard Coding形式!

当中JNIEXPORT、和JNICALL这两个宏负责完成函数的注册工作;

 

另外一种是为每个JNI动态库定义一个JNI_Onload方法,在当中通过调用Android提供的dalvik/libnativehelper/JNIHelp.c当中的jniRegisterNativeMethod进行动态注册。JVM通过System.loadLibrary加载JNI库的时候,会在加载完成之后马上调用这个JNI_Onload。

通过上面两种方法,android.media.MediaScanner.processFile,最终会得到以下的这样一个结构:

{

   "processfile",

   "(Ljava/lang/String;LJava/lang/String;Landroid/media/MediaScannerClient;)v",

   (void *) <对应的JNI函数名>

}


JNIEnv是一个与线程相关的结构体,与之相应的我们也有一个与JVM进程的相关的结构体JavaVM。一般而言JVM进程当中可能同时存在多个线程,我们可以调用JavaVM结构当中的AttachCurrentThread函数获得当前进程的JNIEnv结构体。

 

我们来看看JNIEnv里面到底有什么: 

JNIEnv -> JVM内部的数据结构(Class) -> Array{JNI函数指针} -> JNI函数

 

因为JNIEnv内部的定义比较长,我们暂且用上面这个图来表示。JNIEnv本质上就是当前进程中访问到Class的一个引用,而一个Class当中的对应的JNI函数以一个函数指针数组的形式保存了对应的JNI层函数的指针。

 

这样说起来可能比较抽象,我们来用一个例子说明。

 

上一次我们谈到JNI层会用jobject来引用绝大部份Java Object的子类的对象。那么JNI具体是怎么用通用的操作这些几乎无限种的Class的呢?其实就和大家平常使用反射的方法是差不多的

 

反射的基本原理是,从JVM当中的Class对象中读取相关的成员,通过比对成员名和签名来确定对应的函数成员。因为Java当中一切皆对象的设计哲学,Class实际上在JVM当中也是以对象的形式存在的。而Class对象当中保存的实际就是类的成员的信息。

 

举个例子,我们希望调用一个MediaScannerClient当中的方法void handleStringTag(String arg1, String arg2)。我们可以这样写:

 

jclass mediaScannerClient = env -> FindClass("android/media/MediaScannerClient");

long long mHandleStringTagMethodID = env -> GetMethod(mediaScannerClient, "handleStringTag", "(Ljava/lang/String;Ljava/Lang/String;)V");

 

然后,我们就可以通过这个ID和以下这个JNI模板函数Call<type>Method调用对应的方法了:

 

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

 

事实上我们也可以直接调用JNIEnv里面的函数

例如jniEnv -> CallVoidMethod(mediaScannerClient, mHandleStringTagMethodID, arg1, arg2);

 

而对于Field,我们可以使用一对setter 和 getter函数

 

NativeType Get<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID)

void Set<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID, NativeType value)


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值