- JNI概述
- 学习JNI实例:MediaScanner
- 注册JNI函数
- 数据类型转换
- JNIEnv介绍
一、JNI概述
JNI全称,JavaNativeInterface——Java提供了Java层与Native层交互的桥梁。通过JNI技术我们可以做到如下:
- Java程序中的函数可以调用Native[C/C++]语言编写的函数。
- Native层中的函数可以调用Java层的函数,也就是说C/C++函数中可以调用Java函数。
二、JNI学习实例:MediaScanner类
MediaScanner类中的部分函数由Native层实现,JNI层中对应的是libmedia_jni.so,media_jni为JNI库的名字。libmedia.so库完成了实际功能。MediaScanner通过JNI库libmedia_jni.so和Native层的libmedia.so进行交互。
#2.1 Java层的MediaScanner分析
- MediaScanner
class MediaScanner {
......
static {
System.loadLibrary("media_jni");
native_init();
}
......
private static native final void native_init();
......
}
Media类中的静态代码块执行了两个操作:
- 加载media_jni库。
- 调用native_init()完成native层初始化操作。
Java函数中调用native函数,必须通过位于JNI层的动态库来实现。一般采用的做法是在类的static代码块中,通过调用System.loadLibrary(String libraryName)来完成对动态库的加载。
- 加载JNI库
由System类的静态成员函数loadLibrary负责加载动态库。
- System
public static void loadLibrary(String libname) {
Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
}
loadLibrary方法中调用了Runtime类的成员函数loadLibrary0();
- Runtime
synchronized void loadLibrary0(ClassLoader loader, String libname) {
if (libname.indexOf((int)File.separatorChar) != -1) {
throw new UnsatisfiedLinkError(
"Directory separator should not appear in library name: " + libname);
}
String libraryName = libname;
//如果 loader不为空进入该分支
if (loader != null) {
//查找库所在的路径
String filename = loader.findLibrary(libraryName);
if (filename == null) {
// It's not necessarily true that the ClassLoader used
// System.mapLibraryName, but the default setup does, and it's
// misleading to say we didn't find "libMyLibrary.so" when we
// actually searched for "liblibMyLibrary.so.so".
throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
System.mapLibraryName(libraryName) + "\"");
}
//加载库
String error = doLoad(filename, loader);
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
return;
}
String filename = System.mapLibraryName(libraryName);
List candidates = new ArrayList();
String lastError = null;
for (String directory : getLibPaths()) {
String candidate = directory + filename;
candidates.add(candidate);
if (IoUtils.canOpenReadOnly(candidate)) {
String error = doLoad(candidate, loader);
if (error == null) {
return; // We successfully loaded the library. Job done.
}
lastError = error;
}
}
if (lastError != null) {
throw new UnsatisfiedLinkError(lastError);
}
throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates);
}
loadLibrary0方法中所完成的操作是,如果ClassLoader不为空,则调用其成员函数findLibrary获取到库所在的路径,然后调用Runtime类的成员函数doLoad对库进行加载,doLoad函数则将具体的加载过程转发给Runtime类中定义的native层函数nativeLoad,进而完成后续加载过程。
- ClassLoader.findLibrary
获取库所在的本地路径。
/**
* Returns the absolute path name of a native library. The VM invokes this
* method to locate the native libraries that belong to classes loaded with
* this class loader. If this method returns null, the VM
* searches the library along the path specified as the
* "java.library.path" property.
*
* @param libname
* The library name
*
* @return The absolute path of the native library
*
* @see System#loadLibrary(String)
* @see System#mapLibraryName(String)
*
* @since 1.2
*/
protected String findLibrary(String libname) {
return null;
}
- Runtime.doLoad
调用native层函数nativeLoad完成对库加载。
private String doLoad(String name, ClassLoader loader) {
......
// internal natives.
synchronized (this) {
return nativeLoad(name, loader, librarySearchPath);
}
}
nativeLoad后续的执行步骤大致为:
- 调用dlopen函数,打开一个so文件并创建一个handle;
- 调用dlsym()函数,查看相应的so文件的JNI_OnLoad()函数指针,并执行相应函数。
#2.2 JNI层的MediaScanner分析
MediaScanner的JNI层代码在android_media_MediaScanner.cpp中,如下所示:
- android_media_MediaScanner.cpp
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;
}
}
Java层的native_init对应JNI层的android_media_MediaScanner_native_init,下面详细分析其绑定过程。
当Java层调用native_init函数时,它会从对应的JNI库中寻找Java_android_media_Media_Scanner_native_init函数,如果没有,就会报错。如果找到,则会为这个native_init和Java_android_media_Media_Scanner_native_init建立一个关联关系,其实就是保存JNI函数的函数指针。以后调用native_init函数时,直接使用这个函数指针就可以了,这项工作是由虚拟机完成的。
三、注册JNI函数
“注册”是将Java层的native函数和JNI层对应的实现函数关联起来。JNI函数注册的方式有两种:
- 静态注册
- 动态注册
#3.1 静态注册
静态注册的大体流程如下:
- 先编写Java代码,然后编译生成.class文件。
- 使用Java的工具程序javah,如javah -o output packagename.classname,这样会生成一个叫output.h的JNI层头文件。其中packagename.classname是Java代码编译后的class文件,而在生成的output.h文件里,声明了对应的JNI层函数,只要实现里面的函数即可。
#3.1.1 :编写Java代码
package com.next.hhu.jnidemo;
public class StaticJNITest {
public static native int add(int a, int b);
}
#3.1.2 编译生成.class文件
执行[1]操作进入到java目录下,然后执行[2]操作生成.class文件。
//[1] 进入到java目录下
cd app/src/main/java
//[2] 生成.class文件
javac com/next/hhu/jnidemo/StaticJNITest.java
#3.1.3 生成头文件
然后利用javah命令生成头文件,这样会在/java目录下生成com_next_hhu_jnidemo_StaticJNITest.h头文件。
javah com.next.hhu.jnidemo.StaticJNITest
#3.1.4 配置CMakeLists.text文件
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.4.1)
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
add_library( # Sets the name of the library.
native-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
src/main/cpp/native-lib.cpp)
add_library( # Sets the name of the library.
native-lib1
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
src/main/cpp/com_next_hhu_jnidemo_StaticJNITest.cpp)
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log )
# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
target_link_libraries( # Specifies the target library.
native-lib
# Links the target library to the log library
# included in the NDK.
${log-lib} )
#3.2 动态注册
JNI中用JNINativeMethod结构来记录Java的Native方法和JNI方法的关联关系。
- jni.h
typedef struct {
const char* name; //Java方法名
const char* signature; //Java方法的签名信息
void* fnPtr; //JNI中对应的函数指针
} JNINativeMethod;
MediaScanner JNI层中采用的是静态注册。
#3.2.1 定义JNINativeMethod[]
定义一个JNINativeMethod[],其成员就是MediaScanner中所有成员函数的一一对应关系。
android_media_MediaScanner.cpp
static JNINativeMethod gMethods[] = {
{
"processDirectory",
"(Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
(void *)android_media_MediaScanner_processDirectory
},
{
"processFile",
"(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
(void *)android_media_MediaScanner_processFile
},
{
"setLocale",
"(Ljava/lang/String;)V",
(void *)android_media_MediaScanner_setLocale
},
{
"extractAlbumArt",
"(Ljava/io/FileDescriptor;)[B",
(void *)android_media_MediaScanner_extractAlbumArt
},
{
"native_init",
"()V",
(void *)android_media_MediaScanner_native_init
},
{
"native_setup",
"()V",
(void *)android_media_MediaScanner_native_setup
},
{
"native_finalize",
"()V",
(void *)android_media_MediaScanner_native_finalize
},
};
#3.2.2 注册JNINativeMetod[]
- android_media_MediaScanner.cpp
// This function only registers the native methods, and is called from
// JNI_OnLoad in android_media_MediaPlayer.cpp
int register_android_media_MediaScanner(JNIEnv *env)
{
return AndroidRuntime::registerNativeMethods(env,
kClassMediaScanner, gMethods, NELEM(gMethods));
}
调用AndroidRuntime的registerNativeMethods函数来完成注册工作。
- 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);
}
调用JNIHelp.c中的jniRegisterNativeMethods方法来完成注册。
- JNIHelp.c
int jniRegisterNativeMethods(JNIENV *env,const char *className,
const JNINativeMethod *gMethods,int numMethods)
{
jclass clazz;
clazz = (*env)->findClass(env,className);
......
//实际上调用JNIEnv的RegisterNatives函数完成注册的
if((*env)->RegisterNatives(env,clazz,gMethods,numMethods) < 0) {
return -1;
}
return 0;
}
#3.2.3 JNI_OnLoad函数中调用注册函数
当Java层通过System.loadLibrary加载完成JNI动态库后,紧接着会查找该库中一个叫JNI_OnLoad的函数。如果有,就调用它,动态注册工作在这里完成。
//该函数的第一个参数类型为JavaVM,代表JNI层的Java虚拟机,每一个Java进程有且仅有一个
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserve) {
JNIEnv *env = NULL;
jint result = -1;
if (vm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) {
goto bail;
}
if (env == NULL) {
return -1;
}
jclass clazz = env->FindClass("com/next/hhu/jnidemo/JNIHelper");
if (clazz == NULL) {
return -1;
}
......
//动态注册MediaScanner的JNI函数
if(register_android_media_MediaScanner(env) < 0) {
goto bail;
}
.....
return JNI_VERSION_1_4;//必须返回这个值,否则会报错。
}
四、数据类型转换
#4.1 基本数据类型转换
Signature格式 | Java | Native |
---|---|---|
B | byte | jbyte |
C | char | jchar |
D | double | jdouble |
F | float | jfloat |
I | int | jint |
S | short | jshort |
J | long | jlong |
Z | boolean | jboolean |
V | void | void |
#4.2 数组数据类型
Signature格式 | Java | Native |
---|---|---|
[B | byte[] | jbyte |
[C | char[] | jchar |
[D | double[] | jdouble |
[F | float[] | jfloat |
[I | int[] | jint |
[S | short[] | jshort |
[J | long[] | jlong |
[Z | boolean[] | jboolean |
#4.3 引用数据类型转换
Signature格式 | Java | Native |
---|---|---|
Ljava/lang/String; | String | jstring |
L+classname+; | 所有对象 | jobject |
[L+classname+; | Object[] | jobjectArray |
Ljava.lang.Class; | Class | jclass |
Ljava.lang.Throwable; | Throwable | jthrowable |
#4.4 Signature
Java函数 | 对应的签名 |
---|---|
void foo() | ()V |
float foo(int i) | (I)F |
long foo(int[] i) | ([I)J |
double foo(Class c) | (Ljava/lang/Class;)D |
boolean foo(int[] i,String s) | ([ILjava/lang/String;)Z |
String foo(int i) | (I)Ljava/lang/String; |
五、JNIEnv介绍
JNIEnv是一个与线程相关的代表JNI环境的结构体,JNIEnv提供了一些JNI系统函数。通过这些函数可以做到:
- 调用JAVA函数。
- 操作jobject对象等很多事情。
JNIEnv是一个线程相关的变量。由于线程相关,所以不能在一个线程中使用另一个线程的JNIEnv结构体,有一种情况当后台线程收到一个网络消息,而又需要由Native层函数主动回调Java层函数时,JNIEnv该如何处理?
//全进程只有一个JavaVM对象,所以可以保存,并且在任何地方使用都没有问题。
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved);
JavaVM和JNIEnv之间的关系:
- 调用JavaVM的AttachCurrentThread函数,就可以得到这个线程的JNIEnv结构体。这样就可以在后台线程中回调Java函数。
- 另外,在后台线程退出前,需要调用JavaVM的DetachCurrentThread的函数来释放对应的资源。
/*
* C++ object wrapper.
*
* This is usually overlaid on a C struct whose first element is a
* JNINativeInterface*. We rely somewhat on compiler behavior.
*/
struct _JNIEnv {
/* do not rename this; it does not seem to be entirely opaque */
const struct JNINativeInterface* functions;
#if defined(__cplusplus)
......
#endif /*__cplusplus*/
};
#5.1 通过JNIEnv操作jobject
Java的引用类型除了少数几个外,最终在JNI层都会用jobject来表示对象的数据类型,操作jobject步骤:
#5.1.1 jfieldID和jmethodID介绍
jfieldID (*GetFieldID)(JNIEnv*, jclass clazz, const char *name, const char *sig);
jmethodID (*GetMethodID)(JNIEnv*, jclass clazz, const char *name, const char *sig);
其中,jclass代表Java类,name表示成员函数或成员变量的名字,sig为这个函数和变量的签名信息。
public:
MyMediaScannerClient(JNIEnv *env, jobject client)
: mEnv(env),
mClient(env->NewGlobalRef(client)),
mScanFileMethodID(0),
mHandleStringTagMethodID(0),
mSetMimeTypeMethodID(0)
{
ALOGV("MyMediaScannerClient constructor");
jclass mediaScannerClientInterface =
env->FindClass(kClassMediaScannerClient);
if (mediaScannerClientInterface == NULL) {
ALOGE("Class %s not found", kClassMediaScannerClient);
} else {
mScanFileMethodID = env->GetMethodID(
mediaScannerClientInterface,
"scanFile",
"(Ljava/lang/String;JJZZ)V");
mHandleStringTagMethodID = env->GetMethodID(
mediaScannerClientInterface,
"handleStringTag",
"(Ljava/lang/String;Ljava/lang/String;)V");
mSetMimeTypeMethodID = env->GetMethodID(
mediaScannerClientInterface,
"setMimeType",
"(Ljava/lang/String;)V");
}
}
将scanFile和handleStringTag函数的jmethodId保存为MyMediaScannerClient的成员变量,供后续使用。
#5.1.2 使用jfieldID和jmethodID
virtual status_t scanFile(const char* path, long long lastModified,
long long fileSize, bool isDirectory, bool noMedia)
{
ALOGV("scanFile: path(%s), time(%lld), size(%lld) and isDir(%d)",
path, lastModified, fileSize, isDirectory);
jstring pathStr;
if ((pathStr = mEnv->NewStringUTF(path)) == NULL) {
mEnv->ExceptionClear();
return NO_MEMORY;
}
//调用JNIEnv的CallVoidMethod函数,注意CallVoidMethod的参数:
//第一个参数代表MediaScannerClient的jobject对象,
//第二个参数是函数scanFile的jmethodID,后面是Java中scanFile的参数。
mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified,
fileSize, isDirectory, noMedia);
mEnv->DeleteLocalRef(pathStr);
return checkAndClearExceptionFromCallback(mEnv, "scanFile");
}
通过JNIEnv输出CallVoidMethod,再把jobject、jmethodID和对应的参数传进去,JNI层就能够调用Java对象的函数了。
#5.2 jstring介绍
Java中String为引用类型,JNI规范中单独创建一个jstring类型来表示Java中的String类型。
- 调用JNIEnv的NewString(JNIEnv *env,const jchar *unicodeChars,jsize len),可以从Native字符串得到一个jstring对象。可以把jstring对象看作是Java中String对象在JNI层的代表,也就是说jstring是一个Java String。由于Java String中存储的是Unicode字符串,所以NewString函数的参数也必须是Unicode字符串。
- 调用JNIEnv的NewStringUTF将根据Native的一个UTF-8字符串得到一个jstring对象。
- JNIEnv中GetStringChars和GetStringUTFChars函数,它们可以将Java String对象转换成本地字符串。
- 如果在代码中调用了上面几个函数,在做完相关工作后,就需要调用ReleaseStringChars或ReleaseStringUTFChars函数来对应地释放资源,否则会导致JVM内存泄漏。
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);
if (mp == NULL) {
jniThrowException(env, kRunTimeException, "No scanner available");
return;
}
if (path == NULL) {
jniThrowException(env, kIllegalArgumentException, NULL);
return;
}
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.
env->ReleaseStringUTFChars(path, pathStr);
return;
}
MyMediaScannerClient myClient(env, client);
MediaScanResult result = mp->processFile(pathStr, mimeTypeStr, myClient);
if (result == MEDIA_SCAN_RESULT_ERROR) {
ALOGE("An error occurred while scanning file '%s'.", pathStr);
}
env->ReleaseStringUTFChars(path, pathStr);
if (mimeType) {
env->ReleaseStringUTFChars(mimeType, mimeTypeStr);
}
}
#5.3 JNI类型签名介绍
Java中支持函数重载,也就是说,可以定义同名但不同参数的函数。因此,仅仅靠函数名没办法找到具体的函数。JNI技术中将参数类型和返回值类型的组合作为一个函数的签名信息,有了签名信息和函数名,就能找到Java中的函数了。
Java中提供了javap的工具来帮助生成函数或变量的签名信息。
//XXX是编译后的.class文件
javap -s -p XXX
C:\Users\hhu\Desktop\11\JNIDemo\app\src\main\java>javap -s -p com.next.hhu.jnidemo.StaticJNITest
Compiled from "StaticJNITest.java"
public class com.next.hhu.jnidemo.StaticJNITest {
public com.next.hhu.jnidemo.StaticJNITest();
descriptor: ()V
public static native int add(int, int);
descriptor: (II)I
static {};
descriptor: ()V
}
#5.3 垃圾回收
JNI提供了三种引用类型:
- Local Reference:本地引用。在JNI层函数中使用的非全局引用对象都是Local Reference,它包括函数调用时传入的jobject和在JNI层函数中创建的jobject。Local Reference最大特点是,一旦JNI层函数返回,这些object就可能被垃圾回收。
- Global Reference:全局引用,这种对象如不主动释放,它永远不会被垃圾回收。
- Weak Global Reference:弱全局引用,一种特殊的Global Reference,在运行过程中可能会被垃圾回收,因此在使用它之前,需要调用JNIEnv的IsSameObject判断它是否被回收了。
public:
MyMediaScannerClient(JNIEnv *env, jobject client)
: mEnv(env),
mClient(env->NewGlobalRef(client)),
{
......
}
virtual ~MyMediaScannerClient()
{
ALOGV("MyMediaScannerClient destructor");
mEnv->DeleteGlobalRef(mClient);
}
#5.4 JNI中的异常处理
如果调用JNIEnv的某些函数出错了,则会产生一个异常,但这个异常不会中断本地函数的执行,直到从JNI层返回到Java层后,虚拟机才会抛出这个异常。虽然在JNI层中产生的异常不会中断本地函数的运行,但一旦产生异常后,就只能做一些资源清理工作。所以安全编码显得十分重要。