第二章 深入理解JNI
2.1 JNI 概述
JNI 是 Java Native Interface 的缩写,中文翻译为“ Java 本地调用“。JNI 的主要可以做到的两点为:
- Java 程序中的函数可以调用 Native 语言写的函数,Native 一般指的是 C/C++ 编写的函数。
- Native程序中的函数可以调用 Java 层的函数,也就是说在 C/C++ 程序中可以调用 Java 函数。
JNI 在 Android 平台上的位置:

2.2 学习 JNI 的实例:MediaScanner
先看 MediaScanner 和它的 JNI,如下图:

- Java 世界对应的是 MediaScanner,而这个MediaScanner 类有一些函数需要由 Native 层来实现。
- JNI层对应的是 libmedia_jni.so。media_jni是JNI库的名字,其中,下划线前的 “media” 是 Native 层库的名字,这里就是 libmedia 库。下划线后的 “jni" 表示它是一个 JNI 库。注意 JNI 库的名字是可以随便取的,Android平台基本采用 ”lib模块名_jni.so“ 的命名方式。
- Native 层对应的是 libmedia.so,这个库完成了实际的功能。
- MediaScanner 将通过 JNI 库 libmedia_jni.so 和 Native 层的 libmedia.so 交互。
JNI层必须实现为动态库的形式,这样Java虚拟机才能加载它并调用它的函数。
MediaScanner是Android平台中多媒体系统的重要组成部分,它的功能是扫描媒体文件,得到诸如歌曲时长、歌曲作者等媒体信息,并将它们存入到媒体数据库中,供其他应用程序使用。
2.3 Java 层的 MediaScanner 分析
2.3.1 加载 JNI 库
原则上在调用 native 函数前,任何时候、任何地方加载都可以。一般的做法时在类的 static 语句中加载,调用 System.loadLibrary 方法就可以了。
System.loadLibrary 函数的参数就是动态库的名字,即 media.jni。系统会自动根据不同平台拓展成真实的动态库文件名,比如在 Linux 系统上会被拓展成 libmedia_jni.so,而在 Windows 平台上则会拓展成 media_jni.dll。
2.3.2 Java 的 native 函数和总结
Java 层只需要完成以下两项工作就可以使用 JNI 了:
- 加载对应的 JNI 库。
- 声明由关键字native修饰的函数。
2.4 JNI 层 MediaScanner 的分析
2.4.1 注册 JNI 函数
native_init 函数位于 android.media 这个包中,它的全路径名应该是 android.media.MediaScanner.native_init ,而 JNI 层函数的名字是 android_media_MediaScanner_native_init。因为在Native语言中,符号 " . " 有着特殊意思,所以 JNI 层需要把 Java 函数名称(包括包名)中的 “ . ” 换成 " _ " 。
JNI 函数的注册其实就是将 Java 层的 native 函数和 JNI 层对应的实现函数关联起来。JNI 函数的注册方法有以下两种:
1、静态方法
整体流程如下:
- 先编写 Java 代码,然后编译成.class文件。
- 使用 Java 的工具程序 javah,如 javah -o output packagename.classname,这样它会生成一个叫 output.h 的 JNI 层头文件。其中 packagename.classname 是 Java 代码编译后的 class 文件,而在生成的 output.h 文件里,声明了对应的 JNI 层函数,只要实现里面的函数即可。
由前面可以看出,静态方法就是根据函数名来建立 Java 函数和 JNI 函数之间的关联关系的,而且它要求 JNI 层函数的名字必须遵循特定的格式,其弊端为:
- 需要编译所有声明了 native 函数的 Java 类,每个所生成的 class 文件都得用 javah 生成一个头文件。
- javah 生成的 JNI 层函数名特别长,书写不方便。
- 初次调用 native 函数时要根据函数名字搜索对应的 JNI 层函数来建立关系,这样会影响运行效率。
2、动态方法
动态注册主要由两个函数完成,如下图:
当 Java 层通过 System.loadLibrary 加载完 JNI 动态库后,紧接着会查找该库中一个叫 JNI_OnLoad 的函数。如果有,就调用它,而动态注册的工作就是在这里完成的。
libmedia_jni.so 的 JNI_OnLoad 函数是在 android_media_MediaPlayer.cpp中实现的。
JNI 层代码中一般要包含 jni.h 这个头文件。Android 源码中提供了一个帮助头文件 JNIHelp.h,它内部其实就包含了 jni.h,所以我们在自己的代码中直接包含这个 JNIHelp.h 即可。
2.4.2 数据类型转换
Java 数据类型分为基本数据类型和引用数据类型两种
1、基本数据类型的转换
2、引用数据类型的转换

2.4.3 JNIEnv 介绍
JNIEnv 是一个与线程相关的代表 JNI 环境的结构体,内部结构如下图:
由图可以看到,JNIEnv实际上就是提供了一些 JNI 系统函数。通过这些函数可以做到:
- 调用 Java 的函数。
- 操作 jobject 对象等很多事情。
JavaVM 和 JNIEnv的关系如下:
- 调用 JavaVM 的 AttachCurrentThread 函数,就可得到这个线程的 JNIEnv 结构体。这样就可以在后台线程中回调 Java 函数了。
- 另外,在后台线程退出前,需要调用 JavaVM 的 DetachCurrentThread 函数来释放对应的资源。
2.4.4 通过 JNIEnv 操作 jobject
一个 java 对象是由它的成员变量和成员函数组成。成员变量又是由类定义的,它们是类的属性。
在 JNI 规则中,用 jfieldID 和 jmethodID 来表示 Java 类的成员变量和成员函数,可通过 JNIEnv的下面两个函数得到:
其中 jclass 代表 Java 类,name 表示成员函数或成员变量的名字,sig为这个函数和变量的签名信息。
如果每次操作 jobject 前都去查询 jmethodID 或 jfieldID,那么将会影响程序运行的效率,所以我们在初始化的时候可以取出这些 ID 并保存起来以供后续使用。
通过 JNIEnv 输出 CallVoidMethod,再把 jobject、jMethodID 和对应的参数传进去,JNI 层就能够调用 Java 对象的函数了。
2.4.5 jstring 介绍
下面是几个和 jstring 有关的函数:
- 调用 JNIEnv 的 NewString(JNIEnv *env,const jchar *unicodeChars,jsize len),可以从 Native 的字符串得到一个 jstring 对象。
- 调用 JNIEnv 的 NewStringUTF 将根据 Native的一个 UTF-8 字符串得到一个 jstring 对象。
- 上面两个函数将本地字符串转换成了 Java 的String 对象,JNIEnv 还提供了 GetStringChars 和 GetStringUTFChars 函数,它们可以将 Java String 对象转换成本地字符串。其中 GetStringChars 得到一个 Unicode 字符串,而 GetStringUTFChars得到一个 UTF-8 字符串。
- 如果在代码中调用了上面几个函数,在做完相关工作后,就都需要调用 ReleaseStringChars 函数或 ReleaseStringUTFChars 函数来对应地释放资源,否则会导致 JVM 内存泄露。
2.4.6 JNI 类型签名介绍
因为 Java 支持函数重载,也就是说,可以定义同名但不同参数的函数。但仅仅根据函数名是没法找到具体函数的。为了解决这个问题,JNI 技术中就将参数类型和返回值类型的组合作为了一个函数的签名信息,有了签名信息和函数名,就能很顺利地找到 Java 中的函数了。
JNI 规范定义的函数签名信息格式为:
(参数1类型标示参数2类型标示……参数n类型标示)
常见的类型标识:
举例如下,对照参考下:
Java 提供了一个叫 叫 javap 的工具能帮助生成函数或变量的签名信息,其用法如下:
javap -s -p xxx
其中 xxx 为编译后的 class 文件,s 表示输出内部数据类型的签名信息,p 表示打印所有函数和成员的签名信息,默认只会打印 public 成员和函数的签名信息。
2.4.7 垃圾回收
在 JNI 层使用下面语句不会增加引用计数:
save_thiz = thiz; //这种赋值不会增加 jobject 的引用计数。
JNI 技术提供了三种类型的引用,如下所示:
- Local Reference:本地引用。在 JNI 层函数中使用的非全局引用对象都是 Local Reference,它包括函数调用时传入的 jobject 和在 JNI 层函数中创建的 jobject。Local Reference 最大的特点就是,一旦 JNI 层函数返回,这些 jobject 就可能被垃圾回收。
- Global Reference:全局引用,这种对象如不主动释放,它永远不会被垃圾回收。
- Weak Global Reference:弱全局引用,一种特殊的 Global Reference,在运行过程中可能会被垃圾回收。所以在使用它之前,需要调用 JNIEnv 的 IsSameObject 判断它是否被回收了。
没有及时回收 Local Reference 或许是进程占用内存过多的一个原因,请务必注意这一点。
JNI 层函数可以在代码中截获和修改这些异常,JNIEnv 提供了三个函数给予帮助:
- ExceptionOccured 函数:用来判断是否发生异常。
- ExceptionClear 函数,用来清理当前 JNI 层中发生的异常。
- ThrowNew 函数,用来向 Java 层抛出异常。
本文深入探讨了JNI在Android中的应用,通过MediaScanner的例子详细解析了JNI的加载、函数注册、数据类型转换、JNIEnv的使用以及垃圾回收机制。Java层通过System.loadLibrary加载JNI库,JNI层通过JNI_OnLoad进行动态注册。JNI提供了调用Java函数和操作对象的方法,同时介绍了jstring的使用和类型签名的重要性。垃圾回收机制中,Local Reference在JNI函数返回后可能被回收,而Global Reference和Weak Global Reference则有不同的管理策略。
721

被折叠的 条评论
为什么被折叠?



