通过上一篇文章,大家明白了调用 native 方法之前,首先要调用 System.loadLibrary 接口加载一个实现了native 方法的动态库才能正常访问,否则就会抛出 java.lang.UnsatisfiedLinkError 异常,找不到 XX 方法的提示。现在我们想想,在 Java 中调用某个 native 方法时,JVM 是通过什么方式,能正确的找到动态库中 C/C++ 实现的那个 native 函数呢?
JVM 查找 native 方法
JVM 查找 native 方法有两种方式:
- 按照 JNI 规范的命名规则 (静态注册)
- 调用 JNI 提供的 RegisterNatives 函数,将本地函数注册到 JVM 中。(动态注册,后面会详细介绍)
本文通过第一篇文章 HelloWorld 示例中的 Java_com_study_jnilearn_HelloWorld_sayHello 函数来详细介绍第一种方式:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_study_jnilearn_HelloWorld */
#ifndef _Included_com_study_jnilearn_HelloWorld
#define _Included_com_study_jnilearn_HelloWorld
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_study_jnilearn_HelloWorld
* Method: sayHello
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_study_jnilearn_HelloWorld_sayHello
(JNIEnv *, jclass, jstring);
#ifdef __cplusplus
}
#endif
#endif
JNIEXPORT 和 JNICALL 的作用
在上篇文章中,我们在将 HelloWorld.c 编译成动态库的时候,用-I
参数包含了 JDK 安装目录下的两个头文件目录:
gcc -dynamiclib -o /Users/yangxin/Library/Java/Extensions/libHelloWorld.jnilib jni/HelloWorld.c -framework JavaVM -I/$JAVA_HOME/include -I/$JAVA_HOME/include/darwin
其中第一个目录为jni.h
头文件所在目录,第二个是跨平台头文件目录(Mac os x系统下的目录名为 darwin,在 Windows 下目录名为 win32,linux 下目录名为 linux),用于定义与平台相关的宏,其中用于标识函数用途的两个宏 JNIEXPORT 和 JNICALL,就定义在 darwin 目录下的jni_md.h
头文件中。在 Windows 中编译 dll 动态库规定,如果动态库中的函数要被外部调用,需要在函数声明中添加__declspec
(dllexport)标识,表示将该函数导出在外部可以调用。在 Linux/Unix 系统中,这两个宏可以省略不加。这两个平台的区别是由于各自的编译器所产生的可执行文件格式不一样。这里有篇文章详细介绍了两个平台编译的动态库区别:http://www.cnblogs.com/chio/archive/2008/11/13/1333119.html。JNICALL 在 Windows 中的值为__stdcall
,用于约束函数入栈顺序和堆栈清理的规则。
Windows 下jni_md.h
头文件内容:
#ifndef _JAVASOFT_JNI_MD_H_
#define _JAVASOFT_JNI_MD_H_
#define JNIEXPORT __declspec(dllexport)
#define JNIIMPORT __declspec(dllimport)
#define JNICALL __stdcall
typedef long jint;
typedef __int64 jlong;
typedef signed char jbyte;
#endif
Linux 下jni_md.h
头文件内容:
#ifndef _JAVASOFT_JNI_MD_H_
#define _JAVASOFT_JNI_MD_H_
#define JNIEXPORT
#define JNIIMPORT
#define JNICALL
typedef int jint;
#ifdef _LP64 /* 64-bit Solaris */
typedef long jlong;
#else
typedef long long jlong;
#endif
typedef signed char jbyte;
#endif
从 Linux 下的jni_md.h
头文件可以看出来,JNIEXPORT 和 JNICALL 是一个空定义,所以在 Linux 下 JNI 函数声明可以省略这两个宏。
函数的命名规则:
用 javah 工具生成函数原型的头文件,函数命名规则为:Java_
类全路径_方法名。如Java_com_study_jnilearn_HelloWorld_sayHello
,其中Java_
是函数的前缀,com_study_jnilearn_HelloWorld
是类名,sayHello
是方法名,它们之间用 _
(下划线) 连接。
函数参数:
JNIEXPORT jstring JNICALL Java_com_study_jnilearn_HelloWorld_sayHello(JNIEnv *, jclass, jstring);
- 第一个参数:
JNIEnv*
是定义任意 native 函数的第一个参数(包括调用 JNI 的 RegisterNatives 函数注册的函数),指向 JVM 函数表的指针,函数表中的每一个入口指向一个 JNI 函数,每个函数用于访问 JVM 中特定的数据结构。 - 第二个参数:调用 Java 中 native 方法的实例或 Class 对象,如果这个 native 方法是实例方法,则该参数是 jobject,如果是静态方法,则是 jclass。
- 第三个参数:Java 对应 JNI 中的数据类型,Java 中 String 类型对应 JNI 的 jstring 类型。(后面会详细介绍 JAVA 与 JNI 数据类型的映射关系)。
函数返回值类型:夹在 JNIEXPORT 和 JNICALL 宏中间的 jstring,表示函数的返回值类型,对应 Java 的String 类型。
总结:当我们熟悉了 JNI 的 native 函数命名规则之后,就可以不用通过javah
命令去生成相应 java native方法的函数原型了,只需要按照函数命名规则编写相应的函数原型和实现即可。
比如com.study.jni.Utils
类中还有一个计算加法的 native 实例方法add
,有两个in
t参数和一个int
返回值:public native int add(int num1, int num2)
,对应 JNI 的函数原型就是:JNIEXPORT jint JNICALL Java_com_study_jni_Utils_add(JNIEnv *, jobject, jint,jint)
。
转载自: Android深入理解JNI(一)JNI原理与静态、动态注册
前言
JNI不仅仅在NDK开发中应用,它更是Android系统中Java与Native交互的桥梁,不理解JNI的话,你就只能停留在Java Framework层。这一个系列我们来一起深入学习JNI。
1.JNI概述
Android系统按语言来划分的话由两个世界组成,分别是Java世界和Native世界。那为什么要这么划分呢?Android系统由Java写不好吗?除了性能的之外,最主要的原因就是在Java诞生之前,就有很多程序和库都是由Native语言写的,因此,重复利用这些Native语言编写的库是十分必要的,况且Native语言编写的库具有更好的性能。
这样就产生了一个问题,Java世界的代码要怎么使用Native世界的代码呢,这就需要一个桥梁来将它们连接在一起,而JNI就是这个桥梁。
通过JNI,Java世界的代码就可以访问Native世界的代码,同样的,Native世界的代码也可以访问Java世界的代码。
为了讲解JNI我们需要分析系统的源码,在即将出版的《Android进阶之光》的最后一章中我拿MediaPlayer框架做了举例,这里换MediaRecorder框架来举例,它和MediaPlayer框架的调用过程十分类似。
2.MediaRecorder框架概述
MediaRecorder我们应该都不陌生,它用于录音和录像。这里不会主要介绍MediaRecorder框架,而是MediaRecorder框架中的JNI。
Java世界对应的是MediaRecorder.java,也就是我们应用开发中直接调用的类。JNI层对用的是libmedia_jni.so,它是一个JNI的动态库。Native层对应的是libmedia.so,这个动态库完成了实际的调用的功能。
3.Java层的MediaRecorder
我们先来查看MediaRecorder.java的源码,截取部分和JNI有关的部分如下所示。
frameworks/base/media/java/android/media/MediaRecorder.java
public class MediaRecorder{
static {
System.loadLibrary("media_jni");//1
native_init();//2
}
...
private static native final void native_init();//3
...
public native void start() throws IllegalStateException;
...
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
在静态代码块中首先调用了注释1处的代码,用来加载名为“media_jni“的动态库,也就是libmedia_jni.so。接着调用注释2处的native_init方法,注释3处的native_init方法用native来修饰,说明它是一个native方法,表示由JNI来实现。MediaRecorder的start方法同样也是一个native方法。
对于Java层来说只需要加载对应的JNI库,接着声明native方法就可以了,剩下的工作由JNI层来完成。
4.JNI层的MediaRecorder
MediaRecorder的JNI层由android_media_recorder.cpp实现,native方法native_init和start的JNI层实现如下所示。
frameworks/base/media/jni/android_media_MediaRecorder.cpp
static void
android_media_MediaRecorder_native_init(JNIEnv *env)
{
jclass clazz;
clazz = env->FindClass("android/media/MediaRecorder");
if (clazz == NULL) {
return;
}
...
fields.post_event = env->GetStaticMethodID(clazz, "postEventFromNative",
"(Ljava/lang/Object;IIILjava/lang/Object;)V");
if (fields.post_event == NULL) {
return;
}
}
static void
android_media_MediaRecorder_start(JNIEnv *env, jobject thiz)
{
ALOGV("start");
sp<MediaRecorder> mr = getMediaRecorder(env, thiz);
process_media_recorder_call(env, mr->start(), "java/lang/RuntimeException", "start failed.");
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
android_media_MediaRecorder_native_init方法是native_init方法在JNI层的实现,android_media_MediaRecorder_start方法则是start方法在JNI层的实现。那么,native_init方法是如何找到对应的android_media_MediaRecorder_native_init方法的呢?
这就需要了解JNI方法注册的知识。
5.JNI方法注册
JNI方法注册分为静态注册和动态注册,其中静态注册多用于NDK开发,而动态注册多用于Framework开发。
静态注册
在AS中新建一个Java Library名为media,这里仿照系统的MediaRecorder.java,写一个简单的MediaRecorder.java,如下所示。
package com.example;
public class MediaRecorder {
static {
System.loadLibrary("media_jni");
native_init();
}
private static native final void native_init();
public native void start() throws IllegalStateException;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
接着进入项目的media/src/main/java目录中执行如下命令:
javac com.example.MediaRecorder.java
javah com.example.MediaRecorder
- 1
- 2
第二个命令会在当前目录中(media/src/main/java)生成com_example_MediaRecorder.h文件,如下所示。
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_MediaRecorder */
#ifndef _Included_com_example_MediaRecorder
#define _Included_com_example_MediaRecorder
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_example_MediaRecorder
* Method: native_init
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_example_MediaRecorder_native_1init
(JNIEnv *, jclass);//1
/*
* Class: com_example_MediaRecorder
* Method: start
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_example_MediaRecorder_start
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
- 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
native_init方法被声明为注释1处的方法,格式为Java_包名_类名_方法名
,注释1处的方法名多了一个“l”,这是因为native_init方法有一个“_”,它会在转换为JNI方法时变成“_l”。
其中JNIEnv * 是一个指向全部JNI方法的指针,该指针只在创建它的线程有效,不能跨线程传递。
jclass是JNI的数据类型,对应Java的java.lang.Class实例。jobject同样也是JNI的数据类型,对应于Java的Object。关于JNIEnv * 以及JNI的数据类型会在本系列的后续文章中进行介绍。
当我们在Java中调用native_init方法时,就会从JNI中寻找Java_com_example_MediaRecorder_native_1init方法,如果没有就会报错,如果找到就会为native_init和Java_com_example_MediaRecorder_native_1init建立关联,其实是保存JNI的方法指针,这样再次调用native_init方法时就会直接使用这个方法指针就可以了。
静态注册就是根据方法名,将Java方法和JNI方法建立关联,但是它有一些缺点:
- JNI层的方法名称过长。
- 声明Native方法的类需要用javah生成头文件。
- 初次调用JIN方法时需要建立关联,影响效率。
我们知道,静态注册就是Java的Native方法通过方法指针来与JNI进行关联的,如果Native方法知道它在JNI中对应的方法指针,就可以避免上述的缺点,这就是动态注册。
动态注册
JNI中有一种结构用来记录Java的Native方法和JNI方法的关联关系,它就是JNINativeMethod,它在jni.h中被定义:
typedef struct {
const char* name;//Java方法的名字
const char* signature;//Java方法的签名信息
void* fnPtr;//JNI中对应的方法指针
} JNINativeMethod;
- 1
- 2
- 3
- 4
- 5
系统的MediaRecorder采用的就是动态注册,我们来查看它的JNI层是怎么做的。
frameworks/base/media/jni/android_media_MediaRecorder.cpp
static const JNINativeMethod gMethods[] = {
...
{"start", "()V", (void *)android_media_MediaRecorder_start},//1
{"stop", "()V", (void *)android_media_MediaRecorder_stop},
{"pause", "()V", (void *)android_media_MediaRecorder_pause},
{"resume", "()V", (void *)android_media_MediaRecorder_resume},
{"native_reset", "()V", (void *)android_media_MediaRecorder_native_reset},
{"release", "()V", (void *)android_media_MediaRecorder_release},
{"native_init", "()V", (void *)android_media_MediaRecorder_native_init},
...
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
上面定义了一个JNINativeMethod类型的gMethods数组,里面存储的就是MediaRecorder的Native方法与JNI层方法的对应关系,其中注释1处”start”是Java层的Native方法,它对应的JNI层的方法为android_media_MediaRecorder_start。”()V”是start方法的签名信息,关于Java方法的签名信息后续的文章会介绍。
只定义JNINativeMethod 类型的数组是没有用的,还需要注册它,注册的方法为register_android_media_MediaRecorder:
frameworks/base/media/jni/android_media_MediaRecorder.cpp
//JNI_OnLoad in android_media_MediaPlayer.cpp
int register_android_media_MediaRecorder(JNIEnv *env)
{
return AndroidRuntime::registerNativeMethods(env,
"android/media/MediaRecorder", gMethods, NELEM(gMethods));
}
- 1
- 2
- 3
- 4
- 5
- 6
register_android_media_MediaRecorder方法中return了AndroidRuntime的registerNativeMethods方法,如下所示。
frameworks/base/core/jni/AndroidRuntime.cpp
/*static*/ int AndroidRuntime::registerNativeMethods(JNIEnv* env,
const char* className, const JNINativeMethod* gMethods, int numMethods)
{
return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}
- 1
- 2
- 3
- 4
- 5
registerNativeMethods方法中又return了jniRegisterNativeMethods方法:
external/conscrypt/src/openjdk/native/JNIHelp.cpp
extern "C" int jniRegisterNativeMethods(JNIEnv* env, const char* className,
const JNINativeMethod* gMethods, int numMethods)
{
...
if (env->RegisterNatives(c.get(), gMethods, numMethods) < 0) {//1
char* msg;
(void)asprintf(&msg, "RegisterNatives failed for '%s'; aborting...", className);
env->FatalError(msg);
}
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
从注释1处可以看出,最终调用的JNIEnv的RegisterNatives方法,JNIEnv在JNI中十分重要,后续文章会介绍它。
register_android_media_MediaRecorder方法最终会调用JNIEnv的RegisterNatives方法,但是register_android_media_MediaRecorder方法是在哪被调用的呢?答案在register_android_media_MediaRecorder方法的注释上:JNI_OnLoad in android_media_MediaPlayer.cpp。这个JNI_OnLoad方法会在System.loadLibrary方法后调用,因为多媒体框架中的很多框架都要进行JNINativeMethod数组注册,因此,注册方法就被统一定义在android_media_MediaPlayer.cpp中的JNI_OnLoad方法中,如下所示。
frameworks/base/media/jni/android_media_MediaPlayer.cpp
jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
{
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_MediaPlayer(env) < 0) {
ALOGE("ERROR: MediaPlayer native registration failed\n");
goto *bail;
}
if (register_android_media_MediaRecorder(env) < 0) {//1
ALOGE("ERROR: MediaRecorder native registration failed\n");
goto *bail;
}
...
result = JNI_VERSION_1_4;
bail:
return result;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
在JNI_OnLoad方法中调用了整个多媒体框架的注册JNINativeMethod数组的方法,注释1处的调用了register_android_media_MediaRecorder方法,同样的,MediaPlayer框架的注册JNINativeMethod数组的方法register_android_media_MediaPlayer也被调用了。