深入理解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 ...
随机推荐
- 视频特性TI(时间信息)和SI(空间信息)的计算工具:TIandSI-压缩码流版
===================================================== TI(时间信息)和SI(空间信息)计算工具文章列表: 视频特性TI(时间信息)和SI(空间信 ...
- shape图形的使用
shape图形的使用 在项目中如果用到有规律的常规的图形,在能够掌握的前提下建议使用shape图形,shape图形相对与图片来说,占用资源更小,并且使用起来不会失真. 效果图 shape图形1 < ...
- 【移动开发】Service类onStartCommand()返回值和参数
Android开发的过程中,每次调用startService(Intent)的时候,都会调用该Service对象的onStartCommand(Intent,int,int)方法,然后在onStart ...
- 【一天一道LeetCode】#257. Binary Tree Paths
一天一道LeetCode 本系列文章已全部上传至我的github,地址:ZeeCoder's Github 欢迎大家关注我的新浪微博,我的新浪微博 欢迎转载,转载请注明出处 (一)题目 Given a ...
- 【Netty源码学习】EventLoopGroup
在上一篇博客[Netty源码解析]入门示例中我们介绍了一个Netty入门的示例代码,接下来的博客我们会分析一下整个demo工程运行过程的运行机制. 无论在Netty应用的客户端还是服务端都首先会初始化 ...
- 2015-2016机器人操作系统(ROS)及其应用暑期学校资料汇总 ROS Summer School 持续更新
综合信息:2015 2016 课程资料:2015 2016 其他重要机器人.ROS相关学习活动 知乎关于ROS的话题 1 ROS的开发流程?http://www.zhihu.com/qu ...
- 手把手带你走进MVP +Dagger2 + DataBinding+ Rxjava+Retrofit 的世界
0.0 Android开发现在的变化用一个词来形容就是 :翻天覆地 越来越多的项目使用了MVP + Rxjava+Retrofit +Dagger2 + DataBinding等等东西.. 但是这些东 ...
- CentOS上PHP完全卸载
想把PHP卸载干净,直接用yum的remove命令是不行的,需要查看有多少rpm包,然后按照依赖顺序逐一卸载. 1.首先查看机器上安装的所有php相关的rpm包 [root@localhost ngi ...
- SQL Server扫盲系列——镜像篇
为方便查看,并以专题形式展示,所以我会把一些文章整合起来.本部分为SQL Server镜像系列: 本文出处:http://blog.csdn.net/dba_huangzj/article/detai ...
- Ubuntu 16.04 LTS今日发布
Ubuntu 16.04 LTS今日发布 Ubuntu16.04 LTS 发布日期已正式确定为 2016 年 4 月 21 日,代号为 Xenial Xerus.Ubuntu16.04 将是非常受欢迎 ...