参考链接:
《深入理解Android 卷I》
什么是JNI
JNI是Java Native Interface的缩写(Java本地调用),Java程序中的函数可以调用Native语言写的函数(一般指的是C/C++编写的函数),Native语言写的函数可以调用Java层的函数。
什么是JNI
Java语言的跨平台是因为在不同平台上可以运行Java虚拟机,而虚拟机是跑在具体平台上的,而本质上Java是通过JNI技术实现的跨平台,很多基层的模块在Java语言诞生之前已经有了比较优秀的实现,为了避免重复造轮子所以我们要使用JNI技术来使用已有的模块。JNI技术与Java应运而生,它也是推动Java快速发展的真正原因。
实现过程
在弄清楚JNI实现过程之前,先给大家介绍一个查看Android系统源码的网站
查看Android系统源码
接下来结合邓凡平的《深入理解Android 卷1》中的MediaScanner的案例来分析。
Java层
/frameworks/base/media/java/android/media/MediaScanner.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| public class MediaScanner
{
//加载对应的so库
static {
System.loadLibrary("media_jni");
native_init();
}
//声明native方法
private native void processDirectory(String path, String extensions, MediaScannerClient client);
private native void processFile(String path, String mimeType, MediaScannerClient client);
public native void setLocale(String locale);
public native byte[] extractAlbumArt(FileDescriptor fd);
private static native final void native_init();
private native final void native_setup();
private native final void native_finalize();
}
|
可以看到MediaScanner.java中有很多native方法声明,这些方法的实现是被定义到动态库libmedia_jni.so中的。
JNI层对应的是libmedia_jni.so。media_jni就是JNI库的名字,下划线前的media是native库的名字,jni表示他是一个JNI库。Android平台默认采用“lib模块名_jni.so”的命名方式。
原则上来讲加载JNI库可以在使用native方法之前的任何时候任何地方加载,通常的做法是在static代码块中加载。
Native层
以上面processDirectory的native方法声明为例,打开native实现
/frameworks/base/media/jni/android_media_MediaScanner.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| static void
android_media_MediaScanner_processDirectory(
JNIEnv *env, jobject thiz, jstring path, jstring extensions, jobject ent)
{
MediaScanner *mp = (MediaScanner *)env->GetIntField(thiz, fields.context);
if (path == NULL) {
jniThrowException(env, "java/lang/IllegalArgumentException", NULL);
return;
}
if (extensions == NULL) {
jniThrowException(env, "java/lang/IllegalArgumentException", NULL);
return;
}
const char *pathStr = env->GetStringUTFChars(path, NULL);
if (pathStr == NULL) { // Out of memory
jniThrowException(env, "java/lang/RuntimeException", "Out of memory");
return;
}
const char *extensionsStr = env->GetStringUTFChars(extensions, NULL);
if (extensionsStr == NULL) { // Out of memory
env->ReleaseStringUTFChars(path, pathStr);
jniThrowException(env, "java/lang/RuntimeException", "Out of memory");
return;
}
MyMediaScannerClient myClient(env, client);
mp->processDirectory(pathStr, extensionsStr, myClient, ExceptionCheck, env);
env->ReleaseStringUTFChars(path, pathStr);
env->ReleaseStringUTFChars(extensions, extensionsStr);
}
|
android_media_MediaScanner_processDirectory对应的是上面的processDirectory方法,对应规则就是包名加类名并替换“.”为“_”
Java方法和Native方法的映射关系
注册JNI函数
当Java层调用native_init函数时,会从对应的JNI库中寻找Java_android_media_MediaScanner_native_init函数,如果没有会报错。如果找到会建立一个关联关系,其实就是保存JNI层的函数指针,后面再次调用该方法直接使用这个函数指针,保存关联关系是又虚拟机来完成的。
上面的MediaScanner通过对应规则建立的关系我们叫静态注册,这种注册有几个弊端:
- 所有native方法必须生成对应的javah头文件。
- javah生成的JNI层函数名特别长。
- 初次调用native的时候需要建立关联关系,会影响运行效率。
解决这几个弊端的方法就是动态注册,通过手动关联起来。用来记录这种一一对应关系的是JNINativeMethod结构。
JNINativeMethod结构体定义
1
2
3
4
5
| typedef struct {
const char* name; //java中的native函数名
const char* signature; //java函数的签名信息,是参数和返回值类型的组合
void* fnPtr; //JNI层对应的函数指针,注意他是void*类型
} JNINativeMethod;
|
怎么使用这个结构体呢?
/frameworks/base/media/jni/android_media_MediaScanner.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| int register_android_media_MediaScanner(JNIEnv *env)
{
return AndroidRuntime::registerNativeMethods(env,
"android/media/MediaScanner", gMethods, NELEM(gMethods));
}
static JNINativeMethod gMethods[] = {
{"processDirectory", "(Ljava/lang/String;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},
};
|
AndroidRuntime类提供了一个registerNativeMethods方法来完成动态注册工作。现在的问题是这个register_android_media_MediaScanner方法是什么时候被调用的呢?
当Java层通过System.loadLibrary加载完JNI动态库后,紧接着会查找库中一个叫JNI_OnLoad函数,如果有则调用它。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
JNIEnv* env = NULL;
jint result = -1;
if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
LOGE("ERROR: GetEnv failed\n");
goto bail;
}
assert(env != NULL);
if (register_android_media_MediaScanner(env) < 0) {
LOGE("ERROR: MediaScanner native registration failed\n");
goto bail;
}
//....省略
}
|
可以看到在JNI_OnLoad中调用了register_android_media_MediaScanner方法来实现动态注册。
数据类型
Java和Native的基础数据类型之间的转换关系如下:
Java | Native | 符号属性 | 字长 |
---|
boolean | jboolean | 无符号 | 8bit |
byte | jbyte | 无符号 | 8bit |
char | jchar | 无符号 | 16bit |
short | jshort | 无符号 | 16bit |
int | jint | 有符号 | 32bit |
long | jlong | 有符号 | 64bit |
float | jfloat | 有符号 | 32bit |
double | jdouble | 有符号 | 64bit |
Java和Native的引用类型之间的转换关系如下:
Java | Native |
---|
Object类型 | jobject |
java.lang.Class实例 | jclass |
java.lang.String实例 | jstring |
Object[] | jobjectArray |
boolean[] | jbooleanArray |
byte[] | jbyteArray |
java.lang.Throwable实例 | jthrowable |
char[] | jcharArray |
short[] | jshortArray |
int[] | jintArray |
long[] | jlongArray |
float[] | jfloatArray |
double[] | jdoubleArray |
除了java基本类型的数组、Class、String和Throwable外,其余数据类型在JNI中都用jobject表示。
1
| private native void processFile(String path, String mimeType, MediaScannerClient client);
|
上面native方法对应的JNI的参数声明如下
1
2
3
4
5
6
| static void
android_media_MediaScanner_processFile(
JNIEnv *env, jobject thiz, jstring path, jstring mimeType, jobject client)
{
//...
}
|
可以看到后面三个参数和native方法的String、MediaScannerClient对应。
JNIEnv对象
上面我们已经看到每个native方法所对应的JNI函数都有一个JNIEvent指针参数。
1
2
3
4
5
6
7
8
9
10
11
12
| /*
* 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;
//...结构体内容太多,不方便贴出来
}
|
JNIEnv结构
JNIEnv是一个与线程相关的代表JNI环境的结构体。实际上是提供了一些JNI系统函数,通过这些函数可以做到调用Java的函数、操作jobject对象等。
通过JNIEnv操作jobject
一个Java对象是由成员属性和方法组成,所以在JNIEnv中定义了两个函数来分别对应Java中的属性和方法。
/dalvik/libnativehelper/include/nativehelper/jni.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| struct _JNIEnv {
/* do not rename this; it does not seem to be entirely opaque */
const struct JNINativeInterface* functions;
#if defined(__cplusplus)
//....
jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
{ return functions->GetFieldID(this, clazz, name, sig); }
jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
{ return functions->GetMethodID(this, clazz, name, sig); }
//....
}
|
jclass代表java类,name代表成员函数或成员变量的名字,sig为这个函数和变量的签名信息。
/frameworks/base/media/jni/android_media_MediaScanner.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| MyMediaScannerClient(JNIEnv *env, jobject client)
: mEnv(env),
mClient(env->NewGlobalRef(client)),
mScanFileMethodID(0),
mHandleStringTagMethodID(0),
mSetMimeTypeMethodID(0)
{
jclass mediaScannerClientInterface = env->FindClass("android/media/MediaScannerClient");
if (mediaScannerClientInterface == NULL) {
fprintf(stderr, "android/media/MediaScannerClient not found\n");
}
else {
mScanFileMethodID = env->GetMethodID(mediaScannerClientInterface, "scanFile",
"(Ljava/lang/String;JJ)V");
mHandleStringTagMethodID = env->GetMethodID(mediaScannerClientInterface, "handleStringTag",
"(Ljava/lang/String;Ljava/lang/String;)V");
mSetMimeTypeMethodID = env->GetMethodID(mediaScannerClientInterface, "setMimeType",
"(Ljava/lang/String;)V");
mAddNoMediaFolderMethodID = env->GetMethodID(mediaScannerClientInterface, "addNoMediaFolder",
"(Ljava/lang/String;)V");
}
}
|
通过上面的两个方法会获得对应的属性和方法的id(jmethodID和jfieldID),如果每次操作jobject前都去查询id将会影响效率,所以我们初始化的时候可以取出这些id并保存。
接下来看看如何调用scanFile方法。
1
2
3
4
5
6
7
8
9
| // returns true if it succeeded, false if an exception occured in the Java code
virtual bool scanFile(const char* path, long long lastModified, long long fileSize)
{
jstring pathStr;
if ((pathStr = mEnv->NewStringUTF(path)) == NULL) return false;
mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified, fileSize);
mEnv->DeleteLocalRef(pathStr);
return (!mEnv->ExceptionCheck());
}
|
可以看出来,通过JNIEnv的CallVoidMethod把jobject、jMethodID等对应参数传进去,JNI层就能调用Java函数了。
JNI类型签名
上面我们看到也多次提到sig为这个函数和变量的签名信息,为什么函数的参数需要这个信息呢?
因为Java支持函数重载,我们只是单纯的根据函数名无法确定是同一个函数,为了解决这个问题,JNI中将参数类型和返回值类型组合成了一个函数的签名信息,使用函数名和签名信息就可以唯一的确定一个函数。
常见的类型标识符如下:
类型标识 | Java类型 |
---|
Z | boolean |
B | byte |
C | char |
S | short |
I | int |
J | long |
F | float |
D | double |
L/java/languageString; | String |
[I | int[] |
[L/java/lang/object; | Object[] |
如果Java类型是数组则标识符中会有一个“[”,另外引用类型标识符后面会有一个“;”
函数签名的小例子:
函数签名 | Java函数 |
---|
()Ljava/lang/String; | String f() |
(ILjava/langClass;)J | long f(int i, Class c) |
([B)V | void f(byte[] bytes) |
垃圾回收
在JNI中使用下面语句是不会增加引用计数的,所以使用的对象可能已经被回收。
JNI中提供了三种类型的引用:
- Local Reference : 本地引用,一旦JNI层函数返回,这些object就可能被垃圾回收。
- Global Reference : 全局引用,不主动释放,永远不会被垃圾回收。
- Weak Global Reference : 弱全局引用,在运行过程中可能会被垃圾回收,使用前需要用JNIEnv的IsSameObject判断。
全局引用使用
1
2
3
4
5
6
7
8
9
| MyMediaScannerClient(JNIEnv *env, jobject client)
: mEnv(env),
mClient(env->NewGlobalRef(client)), //使用NewGlobalRef创建全局引用
mScanFileMethodID(0),
mHandleStringTagMethodID(0),
mSetMimeTypeMethodID(0)
{
//....
}
|
在构造函数中创建了全局引用,在析构函数中释放全局引用。
1
2
3
4
| virtual ~MyMediaScannerClient()
{
mEnv->DeleteGlobalRef(mClient); //释放全局引用
}
|
本地引用使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // returns true if it succeeded, false if an exception occured in the Java code
virtual bool scanFile(const char* path, long long lastModified, long long fileSize)
{
jstring pathStr;
//使用NewStringUTF创建了一个本地引用的String对象
if ((pathStr = mEnv->NewStringUTF(path)) == NULL) return false;
//当执行完CallVoidMethod方法后pathStr对象会被回收。
mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified, fileSize);
//既然上面说了pathStr会回收,这个DeleteLocalRef函数是不是多余的呢?
mEnv->DeleteLocalRef(pathStr);
return (!mEnv->ExceptionCheck());
}
|
上面代码清晰的展示了本地引用的使用过程,但是留下了一个疑问,我们试想假设scanFile方法被循环执行了10000次,那么pathStr是不是不能立即被回收,如果不主动调用DeleteLocalRef函数可能会造成内存泄漏。
总结
上面完整展示了JNI代码调用的过程,Java代码可以使用声明的native方法(未实现)来调用Native的实现,而Native方法可以通过JNIEnv对象的GetFieldID和GetMethodId方法来获取Java属性和方法来调用Java方法。对比一下会发现整个JNI的过程对Native层的要求比较高,Native层需要根据Java的native方法来注册并实现,而且还需要知道需要调用的Java方法的路径和名字来获取函数指针。
JNI职责关系