深入理解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的更多相关文章

  1. Android深入理解JNI(二)类型转换、方法签名和JNIEnv

    相关文章 Android深入理解JNI系列 前言 上一篇文章介绍了JNI的基本原理和注册,这一篇接着带领大家来学习JNI的数据类型转换.方法签名和JNIEnv. 1.数据类型的转换 首先给出上一篇文章 ...

  2. Android深入理解JNI(一)JNI原理与静态、动态注册

    前言 JNI不仅仅在NDK开发中应用,它更是Android系统中Java与Native交互的桥梁,不理解JNI的话,你就只能停留在Java Framework层.这一个系列我们来一起深入学习JNI. ...

  3. 深入理解JNI 邓平凡

    深入理解JNI 邓凡平 1)使用的时候 :加载libmedia_jni.so 并接着调用JNI_Onload->register_android_media_MediaScanner动态注册JN ...

  4. JAVA基础之理解JNI原理

    JNI是JAVA标准平台中的一个重要功能,它弥补了JAVA的与平台无关这一重大优点的不足,在JAVA实现跨平台的同时,也能与其它语言(如C.C++)的动态库进行交互,给其它语言发挥优势的机会. 有了J ...

  5. 深入理解JNI(《深入理解android》(author : 邓凡平)读书札记)

    JNI的技术特点: java能够调用native代码. native代码能够调用java代码.   JNI的技术考虑: 实现java代码的平台无关型. java语言发展初期使用C和C++代码,避免重复 ...

  6. java JNI 的实现(1)-又进一步加深对JVM实现的理解

    目录 概述 主要优点 主要缺点 JNI实现的简单例子 开发工具 简略步骤 1,在eclipse的 'java类' 中声明一个 'native方法'; 2,使用 'javah' 命令生成包含'nativ ...

  7. JNI详解---从不懂到理解

    转载:https://blog.csdn.net/hui12581/article/details/44832651 Chap1:JNI完全手册... 3 Chap2:JNI-百度百科... 11 C ...

  8. Android JNI 本地开发接口

    前言 我们为什么要用JNI --> 高效.扩展 高效:Native code效率高,数学运算,实时渲染的游戏上,音视频处理 (极品飞车,opengl,ffmpeg,文件压缩,图片处理-) 扩展: ...

  9. 在 JNI 编程中避免内存泄漏

    JAVA 中的内存泄漏 JAVA 编程中的内存泄漏,从泄漏的内存位置角度可以分为两种:JVM 中 Java Heap 的内存泄漏:JVM 内存中 native memory 的内存泄漏. Java H ...

随机推荐

  1. mysql进阶(二十七)数据库索引原理

    mysql进阶(二十七)数据库索引原理 前言   本文主要是阐述MySQL索引机制,主要是说明存储引擎Innodb.   第一部分主要从数据结构及算法理论层面讨论MySQL数据库索引的数理基础.    ...

  2. shell test和find命令实例解析

    shell test和find命令实例解析 下面以\build\core\product.mk相关部分来学习 define _find-android-products-files $(shell t ...

  3. Android动态换肤(一、应用内置多套皮肤)

    动态换肤在很多android应用中都有使用,用户根据自己的喜好设置皮肤主题,可以增强用户使用应用的舒适度. Android换肤可以分为很多种,它们从使用方式,用户体验以及项目框架设计上体现了明显的差异 ...

  4. 剑指Offer——滴滴笔试题+知识点总结

    剑指Offer--滴滴笔试题+知识点总结 情景回顾 时间:2016.9.18 15:00-17:00 地点:山东省网络环境智能计算技术重点实验室 事件:滴滴笔试   总体来说,滴滴笔试内容体量不算多, ...

  5. Libgdx教程目录

    Libgdx教程 Note:本教程用的Libgdx 1.9.2版本,在教程更新完毕之前应该不会更新版本.之前的博客中也发表过Libgdx的内容,不过当时都从别人那里拷贝的,因此现在想做一个Libgdx ...

  6. GDAL1.11版本对SHP文件索引加速测试

    GDAL库中对于矢量数据的读取中可以设置一些过滤器来对矢量图形进行筛选,对于Shapefile格式来说,如果数据量太大,设置这个过滤器时间慢的简直无法忍受.好在GDAL1.10版本开始支持读取Shap ...

  7. Ubuntu15.10下如何使用EasyGui模块开发Python GUI

    偶然的一个机会,发现了github上的这个开源的项目,easygui for python(一个基于TKinter的模块) 感觉很是惊讶,原来python也可以这么简单的开发出一些GUI界面(究其原因 ...

  8. Java与C之间的socket通信

    最近正在开发一个基于指纹的音乐检索应用,算法部分已经完成,所以尝试做一个Android App.Android与服务器通信通常采用HTTP通信方式和Socket通信方式.由于对web服务器编程了解较少 ...

  9. Douglas Adams - 3 Rules That Describe Our Reactions To Technologies 科技影响生活的三个规律

    文章摘自http://highscalability.com/. 这个博客是大家都应该订阅的.原文地址http://highscalability.com/blog/2014/3/11/douglas ...

  10. java方法重写和super关键字

    //java方法重写和super关键字 //在继承中,其实就是子类定义了和父类同名的方法 //就是方法,属性都是相通的 //重写限制: //被子类重写的方法不能拥有比父类方法更加严格的权限 //sup ...