深入理解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数据类型分为基本数据类型和引用数据类型两种
先看基本数据类型
| Java | Native | JNI层字长 | 
|---|---|---|
| boolean | jboolean | 8位 | 
| byte | jbyte | 8位 | 
| char | jchar | 16位 | 
| short | jshort | 16位 | 
| int | jint | 32位 | 
| long | jlong | 64位 | 
| float | jfloat | 32位 | 
| double | jdouble | 64位 | 
再看引用类型
| Java引用类型 | Native类型 | 
|---|---|
| All objects | jobject | 
| java.lang.Class | jclass | 
| java.lang.String | jstring | 
| 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类型 | 
|---|---|
| Z | boolean | 
| B | byte | 
| C | char | 
| S | short | 
| I | int | 
| J | long | 
| F | float | 
| D | double | 
| L/java/lanaugeString | String | 
| [I | int[] | 
| [L/java/lang/object | Object[] | 
函数签名手动写很容易出错,java提供了一个javap的工具可以帮助生成函数或变量的签名信息
垃圾回收
JNI中提供三种类型的引用来解决垃圾回收问题
Local Reference:本地引用,一旦JNI层函数返回,这些jobject就可能被垃圾回收
Global Reference:全局引用,不主动释放,永远不会被回收
Weak Global Reference:弱全局引用,在运行过程中可能会被垃圾回收,因此在使用之前,需要调用JNIEnv的isSameObject判断是否被回收
小结
通过阅读本章主要学习了
JNI作用
结合MediaScanner等源码学习了JNI注册调用等过程
JNIEnv的用法
JNI中签名、数据类型、垃圾回收机制
深入理解JNI的更多相关文章
- Android深入理解JNI(二)类型转换、方法签名和JNIEnv
		相关文章 Android深入理解JNI系列 前言 上一篇文章介绍了JNI的基本原理和注册,这一篇接着带领大家来学习JNI的数据类型转换.方法签名和JNIEnv. 1.数据类型的转换 首先给出上一篇文章 ... 
- Android深入理解JNI(一)JNI原理与静态、动态注册
		前言 JNI不仅仅在NDK开发中应用,它更是Android系统中Java与Native交互的桥梁,不理解JNI的话,你就只能停留在Java Framework层.这一个系列我们来一起深入学习JNI. ... 
- 深入理解JNI 邓平凡
		深入理解JNI 邓凡平 1)使用的时候 :加载libmedia_jni.so 并接着调用JNI_Onload->register_android_media_MediaScanner动态注册JN ... 
- JAVA基础之理解JNI原理
		JNI是JAVA标准平台中的一个重要功能,它弥补了JAVA的与平台无关这一重大优点的不足,在JAVA实现跨平台的同时,也能与其它语言(如C.C++)的动态库进行交互,给其它语言发挥优势的机会. 有了J ... 
- 深入理解JNI(《深入理解android》(author : 邓凡平)读书札记)
		JNI的技术特点: java能够调用native代码. native代码能够调用java代码. JNI的技术考虑: 实现java代码的平台无关型. java语言发展初期使用C和C++代码,避免重复 ... 
- java JNI 的实现(1)-又进一步加深对JVM实现的理解
		目录 概述 主要优点 主要缺点 JNI实现的简单例子 开发工具 简略步骤 1,在eclipse的 'java类' 中声明一个 'native方法'; 2,使用 'javah' 命令生成包含'nativ ... 
- JNI详解---从不懂到理解
		转载:https://blog.csdn.net/hui12581/article/details/44832651 Chap1:JNI完全手册... 3 Chap2:JNI-百度百科... 11 C ... 
- Android JNI 本地开发接口
		前言 我们为什么要用JNI --> 高效.扩展 高效:Native code效率高,数学运算,实时渲染的游戏上,音视频处理 (极品飞车,opengl,ffmpeg,文件压缩,图片处理-) 扩展: ... 
- 在 JNI 编程中避免内存泄漏
		JAVA 中的内存泄漏 JAVA 编程中的内存泄漏,从泄漏的内存位置角度可以分为两种:JVM 中 Java Heap 的内存泄漏:JVM 内存中 native memory 的内存泄漏. Java H ... 
随机推荐
- Universal-Image-Loader 图片异步加载类库的使用
			这个图片异步加载并缓存的类已经被很多开发者所使用,是最常用的几个开源库之一,主流的应用,随便反编译几个火的项目,都可以见到它的身影. 可是有的人并不知道如何去使用这库如何进行配置,网上查到的信息对于刚 ... 
- JBOSS EAP 6 系列四 EJB实现——调用(贯穿始终的模块)
			本文主要介绍在JBOSS EAP 6.2(或者JBOSS AS7)中模块是如何贯穿EJB实现的始终.延续上一博文<认识模块的使用>的话题继续聊JBOSS做为模块申明式容器的这一特性在EJB ... 
- Java: How to resolve Access Restriction error
			Issue: Access restriction: The constructor 'BASE64Decoder()' is not API (restriction on required lib ... 
- Android 访问assets下的文件
			assets下经常可以放一些比较大的资源,对于这些资源我们如何访问. 步骤 1.获取AssetManager. AssetManager am = getResources().getAssets() ... 
- 4.Lucene3.案例介绍,创建索引,查询等操作验证
			 案例: Article.java package cn.toto.lucene.quickstart; publicclassArticle { privateintid; private St ... 
- DBoW2应用
			图像对应的bag-of-words向量\(v_t\) 假设词典总共有\(W\)个单词,那么每一幅图像能够用一个\(W\)维的向量表示 \((t_1, t_2, t_3, ..., t_W)\)其中 \ ... 
- Spark技术内幕:Master的故障恢复
			Spark技术内幕:Master基于ZooKeeper的High Availability(HA)源码实现 详细阐述了使用ZK实现的Master的HA,那么Master是如何快速故障恢复的呢? 处于 ... 
- cocos2dx 3.3 C++工程添加lua支持
			准备工作: 1. 拷贝cocos2d-x-3.3rc0\external\lua整个文件夹到项目中(如myProject\cocos2d\external\lua) 2. 拷贝cocos2d-x-3. ... 
- java实现异步调用实例
			在JAVA平台,实现异步调用的角色有如下三个角色: 调用者 取货凭证 真实数据 一个调用者在调用耗时操作,不能立即返回数据时,先返回一个取货凭证.然后在过一断时间后凭取货凭证来获取真正的数据. ... 
- 华为解锁BL
			华为手机要怎么查看手机是否需要解锁呢?相信许多机油都不懂自己入手的手机是否需要解锁.而华为手机自华为C8812之后的高通手机均需要先解锁才能够尽兴刷机或获取Root权限的.那么下面我给大家分享一下华为 ... 
