深入理解JNI

JNI系列

JNI两种注册过程实战

深入理解JNI

深入理解JNI

最近在学习android底层的一些东西,看了一些大神的博客,整体上有了一点把握,也产生了很多疑惑,于是再次把邓大神的深入系列翻出来仔细看看,下面主要是一些阅读笔记。

JNI概述

JNI是Java Native Interface的缩写 ,通常称为“Java本地调用”,通过这种技术可以做到:

Java程序中的函数可以调用Native语言写的函数,Native一般是指C/C++编写的函数;

Native程序中的函数可以调用Java层的函数,也就是说C/C++程序可以调用Java函数。

通过JNI可以将底层Native世界和java世界联系起来

学习JNI实例:MediaScanner

Java层对应的是MediaScanner,这个类有一些函数需要由Native层来实现

JNI层对饮libmedia_jni.so,一般采用lib模块名_jni.so的命名方式

Native层对应的是libmedia.so,这个库完成了实际的功能

1、调用native函数

Java调用native函数,就需要通过一个位于JNI层的动态库来实现,这个通常是在类的static语句中加载,调用System.loadLibrary方法,该方法的参数是动态库的名称,在这里为media_jni(系统会根据不同平台扩展成真实的动态库文件名,如在linux中libmedia_jni.so,而在windows平台则会扩展为media_jin.dll)
[MediaScanner.java]

`static {
    //加载对应的JNI库media_jni是JNI库的名称。实际动态加载时将其扩展成为libmedia_jni.so
    //在windows平台则扩展成为media_jni.dll
    System.loadLibrary("media_jni");
    native_init();//调用native_init函数
    ……
    //申明一个native函数,表示它由JNI层完成
    private native void processFile(String path, String mimeType, MediaScannerClient client);
    ……
    private static native final void native_init();
}`

2、Java层和JNI层函数关联

即java层的native_init和processFile[MediaScanner.java如上]函数对应的是JNI层的android_media_MediaScanner_native_init和android_media_MediaScanner_processFile[android_media_MediaScanner.cpp如下]函数呢?

`
//native_init的JNI层实现
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;
    }
}
    ……

//processFile的JNI层实现
static void android_media_MediaScanner_processFile(
    JNIEnv *env, jobject thiz, jstring path,
    jstring mimeType, jobject client)
{
ALOGV("processFile");

// Lock already hold by processDirectory
MediaScanner *mp = getNativeScanner_l(env, thiz);
     ……
//调用JNIEnv的GetStringUTFChars得到本地字符串pathStr
const char *pathStr = env->GetStringUTFChars(path, NULL);
if (pathStr == NULL) {  // Out of memory
    return;
}
const char *mimeTypeStr =
    (mimeType ? env->GetStringUTFChars(mimeType, NULL) : NULL);
if (mimeType && mimeTypeStr == NULL) {  // Out of memory
    // ReleaseStringUTFChars can be called with an exception pending.
    //使用完记得释放资源否则会引起JVM内存泄露
    env->ReleaseStringUTFChars(path, pathStr);
    return;
}
    ……
}   

`

注册JNI函数

注册之意就是将Java层的native函数与JNI层对应的实现函数关联起来,这样在调用java层的native函数时,就能顺利转到JNI层对应的函数执行。
拿native_init来说,在android.media这个包中,全路径为andorid.media.MediaScanner.native_init而JNI函数名字是android_media_MediaScanner_native_init,由于在Native语言中符号“.”有着特殊意义需要将java函数名(包括包名)中的“.”换成“_”,这样java中的native_init找到JNI中的android_media_MediaScanner_native_init

注册的两种方式

静态方式

动态方式

静态方式

根据函数名来找对应的JNI函数,需要java的工具程序javah参与,流程如下:

先编写java代码,然后编译生成.class文件

使用java的工具程序javah,如javah -o output packagename.classname 这样就会生成一个叫output的JNI层头文件(函数名有_转换后为_l)

在静态方法中native函数是如何找到的,过程如下:当java层调用native_init函数时,它会从对应的JNI库中寻找java_android_media_MediaScanner_native_linit函数,如果没有找到,就会报错,如果找到就会为native_init和java_android_media_MediaScanner_native_linit建立一个函数指针,以后再调用native时直接使用这个指针即可,这个工作是由虚拟机完成

缺点:每个class都需要使用javah生成一个头文件,并且生成的名字很长书写不便;初次调用时需要依据名字搜索对应的JNI层函数来建立关联关系,会影响运行效率

动态注册

使用一种数据结构JNINativeMethod来记录Java native函数和JNI函数的对应关系

`typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;`

[android_media_MediaScanner.cpp]中native_init和processFile的动态注册

`//动态注册
//定义一个JNINativeMethod数组,其成员就是MS中所有native函数一一对应关系
static JNINativeMethod gMethods[] = {
    ……
{
    "processFile", //java中native函数的函数名
    //processFile的签名信息
    "(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
    (void *)android_media_MediaScanner_processFile//JNI层对应的函数指针
},
    ……
{
    "native_init",
    "()V",
    (void *)android_media_MediaScanner_native_init
},
    ……
};

// This function only registers the native methods, and is called from
// JNI_OnLoad in android_media_MediaPlayer.cpp
//注册JNINativeMethod数组
int register_android_media_MediaScanner(JNIEnv *env)
{
return AndroidRuntime::registerNativeMethods(env,
            kClassMediaScanner, gMethods, NELEM(gMethods));
}

`

这里使用AndroidRunTime类提供的registerNativeMethods将getMethods来完成注册工作

[AndroidRunTime.cpp]

`/*
* Register native methods using JNI.
*/
/*static*/ 
int AndroidRuntime::registerNativeMethods(JNIEnv* env,
const char* className, const JNINativeMethod* gMethods, int numMethods)
{
return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}`

这里最终调用jniRegisterNativeMethods,这个函数是android平台为了方便JNI使用的一个帮助函数

[JNIHelp.c]

`
/*
 * Register native JNI-callable methods.
 *
 * "className" looks like "java/lang/String".
 */
int jniRegisterNativeMethods(JNIEnv* env, const char* className,
    const JNINativeMethod* gMethods, int numMethods)
{
    jclass clazz;

    LOGV("Registering %s natives\n", className);
    clazz = (*env)->FindClass(env, className);
    if (clazz == NULL) {
        LOGE("Native registration unable to find class '%s'\n", className);
        return -1;
    }
    //实际上是调用了JNIEnv的RegisterNatives函数完成注册的
    if ((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0) {
        LOGE("RegisterNatives failed for '%s'\n", className);
        return -1;
    }
    return 0;
}

`

从这里我们可以清晰看出函数调用关系

`AndroidRuntime::registerNativeMethods
                jniRegisterNativeMethods`

而在jniRegisterNativeMethods中核心步骤只有两步

通过类名找到类(env指向一个JNIEnv结构体,className为对应Java类名,由于JNINativeMethod中使用的函数名并非全路径名,这里要指明具体类)

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

调用JNIEnv的RegisterNatives函数完成注册关联关系

(*env)->RegisterNatives(env, clazz, gMethods, numMethods)

何时调用该动态注册函数?

在第一小节调用native函数时首先使用System.loadLibrary来加载动态库,当加载完成JNI动态库后,紧接着会查找该库汇总一个叫JNI_OnLoad的函数,如果有就调用该函数,动态注册工作就是在这里完成。因此要实现动态注册就必须实现JNI_OnLoad函数,只有在这个函数中才有机会完成动态注册的工作。这里是放在了android_media_MediaPlayer.cpp中

[android_media_MediaPlayer.cpp]

`jint JNI_OnLoad(JavaVM* vm, void*  reserved )
{
//该函数的第一个参数类型为JavaVM,这是虚拟机在JNI层的代表
//每个java进程只有一个这样的JavaVM
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_MediaScanner(env) < 0) {
    ALOGE("ERROR: MediaScanner native registration failed\n");
    goto bail;
}
……

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

bail:
return result;
}`

ok,至此JNI注册结束

JNIEnv介绍

在注册过程中JNIEnv已经多次出现,这里做下详细介绍。代表JNI环境的结构体

而且JNIEnv是一个线程相关的,也就是说线程A有个JNIEnv,线程B有个JNIEnv。由于线程相关不能在B线程中去访问线程A的JNIEnv结构体。由于我们无法保存一个线程的JNIEnv结构体,然后放到后台线程中去使用。为了解决这个问题,在
JNI_OnLoad函数中第一个参数是JavaVM对象,它是虚拟机在JNI层的代表

`
//全进程只有一个javavm对象,所以可以保存,并且在任何地方使用都没有问题
JNI_OnLoad(JavaVM* vm, void*  reserved )`

其中

调用JavaVM的AttachCunrrentThread函数,就可以的得到这个线程的JNIEnv结构体。这样就可以在后台线程中回调Java函数

在后台线程退出前,需要调用JavaVM的Detach的DetachCurrentThread函数来释放对应的资源

这样就是可以方便使用JNIEnv了。

如何使用JNIEnv

在JNI中除了基本类型数组、Class、String和Throwable外其余所有Java对象的数据类型在JNI中都用jobject表示(数据类型下一节会介绍),因此JNIEnv如何操作jobject显得很重要。

首先要取得这些属性和方法。操作jobject的本质就是操作这些对象的成员变量和成员函数。在JNI中使用jfieldID和jmethodID来表示Java类的成员变量和成员函数

`jfieldID GetFieldID(jclass clazz,const char *name,const char *sig)
 jmethodID GetMethod(jclass clazz,const char *name,const char *sig)
`

其中jclass表示java类,name表示成员变量/成员函数名称,sig表示变量/函数的签名信息,使用如下所示
[android_media_MediaScanner.cpp]

` mScanFileMethodID = env->GetMethodID(
                                mediaScannerClientInterface,
                                "scanFile",
                                "(Ljava/lang/String;JJZZ)V");`

这里所做就是将这些ID保存以便于后续使用,使得运行效率更高。

获取这些属性/方法ID后再看如何使用,如前面已经获取了mScanFileMethodID,下面是使用

` mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified,
            fileSize, isDirectory, noMedia);
`

清清楚楚,使用JNIEnv输出CallVoidMethod,再把jobject、jMethodID和对应的参数传入,就可以调用java对象的函数了。这里是无返回值对象,实际上JNIEnv输出了一些列类似CallVoidMethod的函数,如CallIntMethod等,实际形式如下

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

其中type对应java函数返回值,要是调用java中的static函数,则需要使用JNIEnv输出的CallStaticMethod系列

同理通过jfieldID操作jobject的成员变量

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

JNI类型和签名

类型

java数据类型分为基本数据类型和引用数据类型两种

先看基本数据类型

JavaNativeJNI层字长
booleanjboolean8位
bytejbyte8位
charjchar16位
shortjshort16位
intjint32位
longjlong64位
floatjfloat32位
doublejdouble64位

再看引用类型

Java引用类型Native类型
All objectsjobject
java.lang.Classjclass
java.lang.Stringjstring
Object[]jobjectArray
boolean[]jbooleanArray
byte[]jbyteArray
java.lang.Throwabe实例jthrowable

签名

由于java支持函数重载,因此仅仅根据函数名是无法找到具体函数的,为解决这个问题,JNI技术中就将参数类型和返回值类型组合作为一个函数的签名,
如在[MedaiScanner.java]processFile函数定义

`  private native void processFile(String path, String mimeType, MediaScannerClient client);`

对应的JNI函数签名是
(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V
其中,括号内是参数标识,最右边是返回值类型的标识,void类型标识是V,当参数类型是引用类型时其格式是”L包名”,包中的点换成/。

类型标识表

类型标识java类型
Zboolean
Bbyte
Cchar
Sshort
Iint
Jlong
Ffloat
Ddouble
L/java/lanaugeStringString
[Iint[]
[L/java/lang/objectObject[]

函数签名手动写很容易出错,java提供了一个javap的工具可以帮助生成函数或变量的签名信息

垃圾回收

JNI中提供三种类型的引用来解决垃圾回收问题

Local Reference:本地引用,一旦JNI层函数返回,这些jobject就可能被垃圾回收

Global Reference:全局引用,不主动释放,永远不会被回收

Weak Global Reference:弱全局引用,在运行过程中可能会被垃圾回收,因此在使用之前,需要调用JNIEnv的isSameObject判断是否被回收

小结

通过阅读本章主要学习了

JNI作用

结合MediaScanner等源码学习了JNI注册调用等过程

JNIEnv的用法

JNI中签名、数据类型、垃圾回收机制

  • 4
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值