深入理解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 ...
随机推荐
- [nginx]统计文件下载是否完整思路(flask)
有一个需求是统计文件是否被用户完整下载,因为是web应用,用js没有找到实现方案,于是搜索下nginx的实现方案,把简单的探索过程记录下. 实验一 最原始的思路,查看日志,下载了一个文件之后我们看日志 ...
- 阻塞IO服务器模型之单线程服务器模型
单线程服务器模型是最简单的一个服务器模型,几乎我们所有程序员在刚开始接触网络编程(不管是B/S结构还是C/S结构)都是从这个简单的模型开始.这种模型只提供同时一个客户端访问,多个客户端访问必须要等到前 ...
- JSP简单隔行变色和日期格式化
以前好像在找,都没找到简单点的,所以后面就自己写了一个,感觉超级简单又好理解,分享给大家 <%@ page language="java" import="java ...
- Java学习之二维数组定义与内存分配详解
二维数组:就是元素为一维数组的一个数组. 格式1: 数据类型[][] 数组名 = new 数据类型[m][n]; m:表示这个二维数组有多少个一维数组. n:表示每一个一维数组的元素有多少个. 注意: ...
- 22 Notification 通知栏代码
结构图: MainActivity.java package com.qf.day22_notification; import android.app.Activity; import androi ...
- 4.0、Android Studio配置你的构建
Android构建系统编译你的app资源和源码并且打包到APK中,你可以用来测试,部署,签名和发布.Android Studio使用Gradle,一个高级的构建套件,来自动化和管理构建进程,同时可以允 ...
- 开源项目——小Q聊天机器人V1.2
小Q聊天机器人V1.0 http://blog.csdn.net/baiyuliang2013/article/details/51386281 小Q聊天机器人V1.1 http://blog.csd ...
- SDL2源代码分析3:渲染器(SDL_Renderer)
===================================================== SDL源代码分析系列文章列表: SDL2源代码分析1:初始化(SDL_Init()) SDL ...
- Android的图片,字符串,demin,color,以及Array,boolean,Integer资源的使用-android学习之旅(五十四)
总体介绍 颜色值的定义 定义字符串,颜色,尺寸资源 字符串 颜色资源 尺寸资源 使用字符串,颜色,尺寸资源 boolean的定义与使用 整形常量的定义与使用 数组资源的定义与使用 图片资源的使用
- Win7/Win8/Win10下安装Ubuntu14.04双系统 以及常见问题
整理自网络. 1. 制作镜像 将ubantu镜像刻录到优盘(我使用UltraISO刻录,镜像下载地址:链接: http://pan.baidu.com/s/1bndbcGv 密码: qsmb) 2. ...