JNI,即Java Native Interface,即 "Java本地调用";
JNI是一种技术,可以做到以下两点:
(1)Java程序中的函数可以调用Native语言写的函数(Native一般指的是C/C++编写的函数);
(2)Native程序中的函数可以调用Java层的函数;
Android系统按语言来划分的话由两个世界组成,分别是Java世界和Native世界。那为什么要这么划分呢?Android系统由Java写不好吗?
除了性能的之外,最主要的原因就是在Java诞生之前,就有很多程序和库都是由Native语言写的,因此,重复利用这些Native语言编写的库是十分必要的,况且Native语言编写的库具有更好的性能。
这样就产生了一个问题,Java世界的代码要怎么使用Native世界的代码呢,这就需要一个桥梁来将它们连接在一起,而JNI就是这个桥梁。
JNI方法注册在JNI开发过程中起到什么样的作用?
先来简单说一下java加载本地so库的过程:
(1)在java类中加载本地.so库文件,并声明native方法;
(2)在C代码中实现so库中对应的方法;
(3)在java层需要调用native方法的地方进行调用;
这里有个问题:java层调用的native方法是怎样和Native层实现的方法对应起来的?这就需要通过一种机制建立它们之间的对应关系,这种机制就是JNI方法注册机制。
JNI方法注册分为静态注册和动态注册,其中静态注册多用于NDK开发,而动态注册多用于Framework开发。本文主要讲解静态注册。
根据函数名来建立java方法和JNI函数间的一一对应关系,多用于NDK开发。
(1)编写java代码;
(2)编译java代码,生成.class文件;
(3)用过javah指令,利用生成的.class文件生成JNI的.h文件;
(4)生成后的JNI头文件中包含了Java函数在JNI层的声明;
注:具体代码创建、编写和编译过程见“第六章”,下面简单讲解一下静态注册的代码;
(1)首先创建一个JNIDemo.java文件,添加如下内容:
package com.test;
public class JNIDemo {
//定义一个方法,该方法在C中实现
public native void testHello();
public static void main(String[] args){
//加载C文件
System.loadLibrary("TestJNI");
JNIDemo jniDemo = new JNIDemo();
jniDemo.testHello();
}
}
(2)接着进入项目根目录下,执行如下命令:
javac com/test/JNIDemo.java
javah com.test.JNIDemo
(3)第2个命令执行后,会在当前目录下(com/test/)生成com_test_JNIDemo.h文件,如下所示:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class com_test_JNIDemo */
#ifndef _Included_com_test_JNIDemo
#define _Included_com_test_JNIDemo
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_test_JNIDemo
* Method: testHello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_test_JNIDemo_testHello
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
其中JNIEnv * 是一个指向全部JNI方法的指针,该指针只在创建它的线程有效,不能跨线程传递。
jobject是JNI的数据类型,对应于Java的Object。关于JNIEnv * 以及JNI的数据类型会在本系列的后续文章中进行介绍。
当我们在Java中调用testHello方法时,就会在JNI中寻找 Java_com_test_JNIDemo_testHello方法,如果没有就会报错,如果找到就会为testHello和Java_com_test_JNIDemo_testHello建立关联,其实是保存JNI的方法指针,这样再次调用 testHello 方法时就会直接使用这个方法指针就可以了。
静态注册就是根据方法名,将Java方法和JNI方法建立关联,但是它有一些缺点:
(1)JNI层的方法名称过长。
(2)声明Native方法的类需要用javah生成头文件。
(3)初次调用JIN方法时需要建立关联,影响效率。
我们知道,静态注册就是Java的Native方法通过方法指针来与JNI进行关联的,如果Native方法知道它在JNI中对应的方法指针,就可以避免上述的缺点,这就是动态注册。
直接告诉native函数其在JNI中对应函数的指针;
(1)利用结构体JNINativeMethod保存Java Native函数和JNI函数的对应关系;
(2)在一个JNINativeMethod数组中保存所有native函数和JNI函数的对应关系;
(3)在Java中通过System.loadLibrary加载完JNI动态库之后,调用JNI_OnLoad函数,开始动态注册;
(4)JNI_OnLoad中会调用AndroidRuntime::registerNativeMethods函数进行函数注册;
(5)AndroidRuntime::registerNativeMethods中最终调用jniRegisterNativeMethods完成注册。
注:具体代码创建、编写、编译流程见“第七章”;
克服了静态注册的弊端,即以“使用数组记录对应关系”的方式代替“每个Java文件对应一个JNI头文件”
Java | Native | Signature |
---|---|---|
byte | jbyte | B |
char | jchar | C |
double | jdouble | D |
float | jfloat | F |
int | jint | I |
short | jshort | S |
long | jlong | J |
boolean | jboolean | Z |
void | void | V |
从上表可以看出,基本数据类型转换,除了void,其他的数据类型只需要在前面加上“j”就可以了。第三列的Signature 代表签名格式,后文会介绍它。接着来看引用数据类型的转换。
Java | Native | Signature |
---|---|---|
所有对象 | jobject | L+classname +; |
Class | jclass | Ljava/lang/Class; |
String | jstring | Ljava/lang/String; |
Throwable | jthrowable | Ljava/lang/Throwable; |
Object[] | jobjectArray | [L+classname +; |
byte[] | jbyteArray | [B |
char[] | jcharArray | [C |
double[] | jdoubleArray | [D |
float[] | jfloatArray | [F |
int[] | jintArray | [I |
short[] | jshortArray | [S |
long[] | jlongArray | [J |
boolean[] | jbooleanArray | [Z |
从上表可看出,数组的JNI层数据类型需要以“Array”结尾,签名格式的开头都会有“[”。除了数组以外,其他的引用数据类型的签名格式都会以“;”结尾。
引用类型的继承关系:
前面表格已经列举了数据类型的签名格式,方法签名就由签名格式组成,那么,方法签名有什么作用呢?我们看下面的代码。
frameworks/base/media/jni/android_media_MediaRecorder.cpp
static const JNINativeMethod gMethods[] = {
...
{"native_init", "()V", (void *)android_media_MediaRecorder_native_init},
{"native_setup", "(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;)V",
(void *)android_media_MediaRecorder_native_setup},
...
};
说明:
gMethods数组中存储的是MediaRecorder的Native方法与JNI层方法的对应关系;
其中”()V”和 “(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;)V”就是方法签名;
我们知道Java是有重载方法的,可以定义方法名相同,但参数不同的方法,正因为如此,在JNI中仅仅通过方法名是无法找到 Java中的具体方法的;
JNI为了解决这一问题就将参数类型和返回值类型组合在一起作为方法签名。通过方法签名和方法名就可以找到对应的Java方法。
JNI的方法签名的格式为:
(参数签名格式...)返回值签名格式
拿上面gMethods数组的native_setup方法举例,他在Java中是如下定义的:
private native final void native_setup(Object mediarecorder_this,
String clientName, String opPackageName) throws IllegalStateException;
它在JNI中的方法签名为:
(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;)V
说明:
参照本文的类型转换表格,native_setup方法的第一个参数的签名为“Ljava/lang/Object;”,后两个参数的签名为“Ljava/lang/String;”,返回值类型void 的签名为“V”,组合起来就是上面的方法签名。
(1)如果我们每次编写JNI时都要写方法签名,也会是一件比较头疼的事,而Java提供了javap命令来自动生成方法签名。我们先写一个简单的MediaRecorder.java包含上面的native_setup方法:
public class MediaRecorder {//Java层
static {
System.loadLibrary("media_jni");
native_init();
}
private static native final void native_init();
private native final void native_setup(Object mediarecorder_this,
String clientName, String opPackageName) throws IllegalStateException;
}
(2)这个文件的在我的本地地址为D:/Android/MediaRecorder.java,接着执行如下命令:
javac D:/Android/MediaRecorder.java
(3)执行命令后会生成MediaRecorder.class文件,最后使用javap命令:
javap -s -p D:/Android/MediaRecorder.class
其中s
表示输出内部类型签名,p
表示打印出所有的方法和成员(默认打印public成员),最终在cmd中的打印结果如下:
可以很清晰的看到输出的native_setup方法的签名和此前给出的一致。
JNIEnv 是一个指向全部JNI方法的指针,该指针只在创建它的线程有效,不能跨线程传递,因此,不同线程的JNIEnv是彼此独立的;
主要作用有两点:
1.调用Java的方法。
2.操作Java(获取Java中的变量和对象等等)。
代码位置:libnativehelper/include/nativehelper/jni.h
#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;//C++中JNIEnv的类型
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;//C中JNIEnv的类型
typedef const struct JNIInvokeInterface* JavaVM;
#endif
说明:
这里使用预定义宏__cplusplus
来区分C和C++两种代码,如果定义了__cplusplus
,则是C++代码中的定义,否则就是C代码中的定义。
在这里我们也看到了JavaVM,它是虚拟机在JNI层的代表,在一个虚拟机进程中只有一个JavaVM,因此,该进程的所有线程都可以使用这个JavaVM。
通过JavaVM的AttachCurrentThread函数可以获取这个线程的JNIEnv,这样就可以在不同的线程中调用Java方法了。还要记得在使用AttachCurrentThread函数的线程退出前,务必要调用DetachCurrentThread函数来释放资源。
在JNI中用jfieldID和jmethodID来代表Java类中的成员变量和方法,可以通过JNIEnv的下面两个方法来分别得到:
//JNI层
jfieldID GetFieldID(jclass clazz,const char *name,const char *sig);
jmethodID GetFieldID(jclass clazz,const char *name,const char *sig);
其中,jclass代表Java类,name代表成员方法或者成员变量的名字,sig为这个方法和变量的签名。
我们来查看MediaRecorder框架的JNI层是如何使用上述的两个方法的,如下所示:
代码位置:frameworks/base/media/jni/android_media_MediaRecorder.cpp
//JNI层
static void
android_media_MediaRecorder_native_init(JNIEnv *env)
{
jclass clazz;
clazz = env->FindClass("android/media/MediaRecorder");//1
if (clazz == NULL) {
return;
}
fields.context = env->GetFieldID(clazz, "mNativeContext", "J");//2
if (fields.context == NULL) {
return;
}
fields.surface = env->GetFieldID(clazz, "mSurface", "Landroid/view/Surface;");//3
if (fields.surface == NULL) {
return;
}
jclass surface = env->FindClass("android/view/Surface");
if (surface == NULL) {
return;
}
fields.post_event = env->GetStaticMethodID(clazz, "postEventFromNative",
"(Ljava/lang/Object;IIILjava/lang/Object;)V");//4
if (fields.post_event == NULL) {
return;
}
}
说明:
注释1处,通过FindClass来找到Java层的MediaRecorder的Class对象,并赋值给jclass类型的变量clazz,因此,clazz就是Java层的MediaRecorder在JNI层的代表。
注释2和注释3处的代码用来找到Java层的MediaRecorder中名为mNativeContext和mSurface的成员变量,并分别赋值给context和surface。
注释4处获取Java层的MediaRecorder中名为postEventFromNative的静态方法,并赋值给post_event。其中fields的定义为:
struct fields_t {//JNI层
jfieldID context;
jfieldID surface;
jmethodID post_event;
};
static fields_t fields;
将这些成员变量和方法赋值给jfieldID和jmethodID类型的变量主要是为了效率考虑,如果每次调用相关方法时都要进行查询方法和变量,显然会效率很低,因此在MediaRecorder框架JNI层的初始化方法android_media_MediaRecorder_native_init中将这些jfieldID和jmethodID类型的变量保存起来,以供后续使用。
我们保存了jfieldID和jmethodID类型的变量,接着怎么使用它们呢,如下所示:
1. 使用jmethodID
代码位置:frameworks/base/media/jni/android_media_MediaRecorder.cpp
void JNIMediaRecorderListener::notify(int msg, int ext1, int ext2)
{//JNI层
ALOGV("JNIMediaRecorderListener::notify");
JNIEnv *env = AndroidRuntime::getJNIEnv();
env->CallStaticVoidMethod(mClass, fields.post_event, mObject, msg, ext1, ext2, NULL);//1
}
说明:
在注释1处调用了JNIEnv的CallStaticVoidMethod函数,其中就传入了fields.post_event,从上面我们得知,它其实是保存了Java层MediaRecorder的静态方法postEventFromNative;
postEventFromNative方法:
代码位置:frameworks/base/media/java/android/media/MediaRecorder.java
private static void postEventFromNative(Object mediarecorder_ref,
int what, int arg1, int arg2, Object obj)
{//Java层
MediaRecorder mr = (MediaRecorder)((WeakReference)mediarecorder_ref).get();
if (mr == null) {
return;
}
if (mr.mEventHandler != null) {
Message m = mr.mEventHandler.obtainMessage(what, arg1, arg2, obj);
mr.mEventHandler.sendMessage(m);
}
}
这样我们就能在JNI层中访问Java的静态方法了,同理,如果想要访问Java的方法则可以使用JNIEnv的CallVoidMethod函数。
2. 使用jfieldID
代码位置:frameworks/base/media/jni/android_media_MediaRecorder.cpp
static void
android_media_MediaRecorder_prepare(JNIEnv *env, jobject thiz)
{//JNI层
ALOGV("prepare");
sp mr = getMediaRecorder(env, thiz);
jobject surface = env->GetObjectField(thiz, fields.surface);//1
if (surface != NULL) {
const sp native_surface = get_surface(env, surface);
...
}
process_media_recorder_call(env, mr->prepare(), "java/io/IOException", "prepare failed.");
说明:
在注释1处调用了JNIEnv的GetObjectField函数,参数中的fields.surface用来保存Java层MediaRecorde中的成员变量mSurface,mSurface的类型为Surface,这样通过GetObjectField函数就得到了mSurface在JNI层中对应的jobject类型变量surface 。
使用静态注册的方式编写一个JNI实例,大致分为以下几个步骤:创建文件夹、编写Java代码、编译生成class文件、编译生成Java头文件、编写C/C++代码、编译生成so动态库文件、执行java程序(即class文件)。
(1)首先,先创建一个文件夹JNI,用来存放接下来所有的文件;
(2)cd JNI进入文件夹JNI,创建com文件夹,进入com文件夹,创建test文件夹;
在test文件夹下编写源程序JNIDemo.java;
package com.test;
public class JNIDemo {
//定义一个方法,该方法在C中实现
public native void testHello();
public static void main(String[] args){
//加载C文件
System.loadLibrary("TestJNI");
JNIDemo jniDemo = new JNIDemo();
jniDemo.testHello();
}
}
回到JNI文件夹,使用命令javac com/test/JNIDemo.java编译java文件生成JNIDemo.class(回到JNI文件夹的原因:编译时要有完整的包名,在java程序文件所在目录下编译会报错)
user@user-PC:~/Desktop/JNI/com/test$ cd ../../
user@user-PC:~/Desktop/JNI$ javac com/test/JNIDemo.java
user@user-PC:~/Desktop/JNI$ cd com/test/
user@user-PC:~/Desktop/JNI/com/test$ ls
JNIDemo.class JNIDemo.java
编译java文件时文件名带后缀:javac JNIDemo.java,执行class文件时文件名不带后缀:java JNIDemo
回到JNI目录,输入命令:javah -classpath . -jni com.test.JNIDemo,可以看到在JNI目录下生成了一个名为com_test_JNIDemo.h的头文件(注:"." 表示将生成的头文件保存在当前目录);
user@user-PC:~/Desktop/JNI/com/test$ cd ../../
user@user-PC:~/Desktop/JNI$ javah -classpath . -jni com.test.JNIDemo
user@user-PC:~/Desktop/JNI$ ls
com com_test_JNIDemo.h TestJNI.c
生成java头文件不需要 "生成class文件" 作为前提;
com_test_JNIDemo.h文件内容如下:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class com_test_JNIDemo */
#ifndef _Included_com_test_JNIDemo
#define _Included_com_test_JNIDemo
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_test_JNIDemo
* Method: testHello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_test_JNIDemo_testHello
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
这个头文件中便告诉了我们需要用C/C++实现的函数的原型,即
JNIEXPORT void JNICALL Java_HelloWorld_print ( JNIEnv * env, jobject obj)
函数名格式:Java_类名_函数名
参数env代表java虚拟机环境,Java传过来的参数和c有很大的不同,需要调用JVM提供的接口来转换成C类型的,就是通过调用env方法来完成转换的。
参数obj代表调用的对象,相当于c++的this。当c函数需要改变调用对象成员变量时,可以通过操作这个对象来完成。
回到JNI目录,编写C程序,代码如下:
注:其中的头文件jni.h在后面会添加,com_test_JNIDemo.h文件前面已经生成;
//TestJNI.c
#include "jni.h"
#include "stdio.h"
#include "com_test_JNIDemo.h"
JNIEXPORT void JNICALL Java_com_test_JNIDemo_testHello
(JNIEnv *env, jobject obj) {
printf("Hello World\n");
}
需求:C程序中包含的 jni.h文件需要从Linux系统安装的JDK目录下复制过来;
(1)先要查找Linux系统当前安装的JDK目录,查找方法参考如下文章;
查找JDK目录:在linux中查看jdk的版本以及安装路径-CSDN博客
(2)在你的JDK目录的include目录下有一个jni.h的文件,将其复制到TestJNI.c所在的目录,即JNI目录下;
C/C++程序编写好后,就可以使用gcc对其进行编译了;
编译命令如下,在编译的时候需要注意:
记得加上java的两个路径,该路径根据你的java环境的实际安装路径而设置,其余的和编译普通的动态库方法相同;
gcc -I"/usr/lib/jvm/java-8-openjdk-amd64/include/linux/" -I"/usr/lib/jvm/java-8-openjdk-amd64/include/" -fPIC -shared -o libJNIDemo.so TestJNI.c
如上命令中的两对双引号可加可不加;
问题1:未填写JNI函数形参
上图第一个红色方框中圈出了我们经常范的一个错误,就是没有填写JNI函数的两个形参,虽然我们这里用不到它们,但是也必须写上,否则无法通过编译。
问题2:找不到头文件"iostream"
因为这里使用的是gcc命令进行编译,gcc是GCC中的C编译器,g++才是GCC中的C++编译器
命令:java -Djava.library.path="." com/test/JNIDemo
在执行class文件之前,所有的代码文件都已完成,如下图,执行class文件后,看到Java程序执行的结果,则表示JNI结构没问题,到此就大功告成了!!!
其中path的值,引号可加可不加;
问题1:在class文件所在目录下运行java文件会报错
因为java文件中我们写的包的结构是:com.test,所以必须在com所在目录执行,这样,执行的文件路径就是"com.test.JNIDemo.java",和包名结构吻合;
问题2:java.library.path填写其他目录会报错
问题3: java文件中没写package com.test,也会报和 "问题1" 一样的错误
通过动态注册的方式编写一个JNI实例,大致分为以下几个步骤:创建文件夹、编写Java代码、编译生成class文件、编写C/C++代码、编译生成so动态库文件、执行java程序(即class文件)。
与静态注册的主要区别就是:不用生成java头文件了。
在操作过程中出现的报错问题,可以尝试在上一节“静态注册-项目编写”中寻找答案。
(1)首先,先创建一个文件夹JNI_Dynamic,用来存放接下来所有的文件;
(2)cd JNI进入文件夹JNI_Dynamic,创建com文件夹,进入com文件夹,创建test文件夹,用于存放Java文件;
在test文件夹下编写源程序JavaHello.java;
package com.test;
public class JavaHello {
public static native String testHello();
public static void main(String[] args){
//加载C文件
System.loadLibrary("JNIDemo");
JavaHello javaHello = new JavaHello();
javaHello.testHello();
}
}
回到JNI_Dynamic文件夹,使用命令javac com/test/JavaHello.java编译java文件生成JavaHello.class
user@user-PC:~/Desktop/JNI_Dynamic/com/test$ cd ../../
user@user-PC:~/Desktop/JNI_Dynamic$ javac com/test/JavaHello.java
user@user-PC:~/Desktop/JNI_Dynamic$ cd com/test/
user@user-PC:~/Desktop/JNI_Dynamic/com/test$ ls
JavaHello.class JavaHello.java
回到JNI_Dyanmic目录,编写C程序,代码如下:
注:其中的头文件jni.h是从JDK目录中复制过来的,详见静态注册中对应步骤;
#include
#include
#include
#include
#include
/**
* 定义native方法
*/
JNIEXPORT jstring JNICALL native_testHello(JNIEnv *env, jclass clazz)
{
printf("testHello in c native code.\n");
return (*env)->NewStringUTF(env, "hello world returned.");
}
// 指定要注册的类
#define JNIREG_CLASS "com/test/JavaHello"
// 定义一个JNINativeMethod数组,其中的成员就是Java代码中对应的native方法
static JNINativeMethod gMethods[] = {
{ "testHello", "()Ljava/lang/String;", (void*)native_testHello},
};
static int registerNativeMethods(JNIEnv* env, const char* className,
JNINativeMethod* gMethods, int numMethods) {
jclass clazz;
clazz = (*env)->FindClass(env, className);
if (clazz == NULL) {
return JNI_FALSE;
}
if ((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0) {
return JNI_FALSE;
}
return JNI_TRUE;
}
/***
* 注册native方法
*/
static int registerNatives(JNIEnv* env) {
if (!registerNativeMethods(env, JNIREG_CLASS, gMethods, sizeof(gMethods) / sizeof(gMethods[0]))) {
return JNI_FALSE;
}
return JNI_TRUE;
}
/**
* 如果要实现动态注册,这个方法一定要实现
* 动态注册工作在这里进行
*/
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env = NULL;
jint result = -1;
if ((*vm)-> GetEnv(vm, (void**) &env, JNI_VERSION_1_8) != JNI_OK) {
return -1;
}
assert(env != NULL);
if (!registerNatives(env)) { //注册
return -1;
}
result = JNI_VERSION_1_8;
return result;
}
C/C++程序编写好后,就可以使用gcc对其进行编译了;
编译命令如下,在编译的时候需要注意:
记得加上java的两个路径,该路径根据你的java环境的实际安装路径而设置,其余的和编译普通的动态库方法相同;
gcc -I"/usr/lib/jvm/java-8-openjdk-amd64/include/linux/" -I"/usr/lib/jvm/java-8-openjdk-amd64/include/" -fPIC -shared -o libJNIDemo.so hello.c
如上命令中的两对双引号可加可不加;
在执行class文件之前,所有的代码文件都已完成,如下图,执行class文件后,看到Java程序执行的结果,则表示JNI结构没问题,到此就大功告成了!!!
其中path的值,引号可加可不加;
GCC参数详解:GCC 参数详解 | 菜鸟教程
Linux查看JDK版本和安装路径:linux 查看java版本和路径_在linux中查看jdk的版本以及安装路径_徐徐徐大仙的博客-CSDN博客
执行Java命令时,提示找不到或无法加载主类:
在命令窗口执行java文件时,提示找不到或无法加载主类 - 耳语 - 博客园
参考资料:
|(详细):标签: Android深入理解JNI | BATcoder - 刘望舒
|(编写JNI代码-测试成功):JNI技术基础(2)——从零开始编写JNI代码 - 雅香小筑 - 博客园
| 使用AS开发JNI-1:Android Studio制作.so库实践_巫山老妖-CSDN博客
| 使用AS开发JNI-2:注册JNI函数的两种方式_巫山老妖-CSDN博客_jni方法注册
| (JNI实战例子):JNI 入门教程 | 菜鸟教程
| Android JNI原理分析 - Gityuan博客 | 袁辉辉的技术博客
| Android Framework层JNI的使用浅析 - Android开发 - 开发语言与工具 - 深度开源
| Android的JNI函数注册
| jni实例介绍:Android IO监控 | 性能监控系列 - 掘金