注册native函数
JNI有两种注册native方法的途径:
- 静态注册:
先由Java得到本地方法的声明,然后再通过JNI实现该声明方法- 动态注册:
先通过JNI重载JNI_OnLoad()实现本地方法,然后直接在Java中调用本地方法。
静态注册
静态注册就是根据函数名来遍历Java和JNI函数之间的关联,而且要求JNI层函数的名字必须遵循特定的格式。具体的实现很简单,首先在Java代码中声明native函数,然后通过javah来生成native函数的具体形式,接下来在JNI代码中实现这些函数即可。
示例如下:1
2
3
4
5
6
7public class JniDemo1{
static {
System.loadLibrary("samplelib_jni");
}
private native void nativeMethod();
}
接来下通过javah来产生jni代码:1
javah -d ./jni/ -classpath /Users/YOUR_NAME/Library/Android/sdk/platforms/android-21/android.jar:../../build/intermediates/classes/debug/ com.xxxpackagename.JniDemo1
然后就会得到一个JNI的.h文件,里面包含这几个native函数的声明,观察一下文件名以及函数名。JNI方法名的规范:
返回值 + Java前缀+全路径类名+方法名+参数1JNIEnv+参数2jobject+其他参数
注意事项:
- 注意分隔符:
Java前缀与类名以及类名之间的包名和方法名之间使用”_”进行分割;- 注意静态:
如果在Java中声明的方法是”静态的”,则native方法也是static。否则不是。
动态注册
动态注册,也就是通过RegisterNatives方法把C/C++中的方法映射到Java中的native方法,而无需遵循特定的方法命名格式。
当我们使用System.loadLibarary()方法加载so库的时候,Java虚拟机就会找到这个JNI_OnLoad函数并调用该函数,这个函数的作用是告诉Dalvik虚拟机此C库使用的是哪一个JNI版本,如果你的库里面没有写明JNI_OnLoad()函数,VM会默认该库使用最老的JNI 1.1版本。由于最新版本的JNI做了很多扩充,也优化了一些内容,如果需要使用JNI新版本的功能,就必须在JNI_OnLoad()函数声明JNI的版本,同时也可以在该函数中做一些初始化的动作。该函数前面也有三个关键字分别是JNIEXPORT,JNICALL ,jint。其中JNIEXPORT和JNICALL是两个宏定义,用于指定该函数时JNI函数。jint是JNI定义的数据类型,因为Java层和C/C++的数据类型或者对象不能直接相互的引用或者使用,JNI层定义了自己的数据类型,用于衔接Java层和JNI层。与JNI_OnLoad()函数相对应的有JNI_OnUnload()函数,当虚拟机释放的该C库的时候,则会调用JNI_OnUnload()函数来进行善后清除工作。
该函数会有两个参数,其中*jvm为Java虚拟机实例,JavaVM结构体定义一下函数:1
2
3
4DestroyJavaVM
AttachCurrentThread
DetachCurrentThread
GetEnv
举例说明,首先是加载so库:1
2
3
4
5public class JniDemo1{
static {
System.loadLibrary("samplelib_jni");
}
}
在jni中的实现,并且在这个函数里面去动态的注册native方法,完整的参考代码如下: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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61#include <jni.h>
#include "Log4Android.h"
#include <stdio.h>
#include <stdlib.h>
using namespace std;
#ifdef __cplusplus
extern "C" {
#endif
static const char *className = "com/gebilaolitou/jnidemo/JNIDemo2";
static void sayHello(JNIEnv *env, jobject, jlong handle) {
LOGI("JNI", "native: say hello ###");
}
static JNINativeMethod gJni_Methods_table[] = {
{"sayHello", "(J)V", (void*)sayHello},
};
static int jniRegisterNativeMethods(JNIEnv* env, const char* className,
const JNINativeMethod* gMethods, int numMethods)
{
jclass clazz;
LOGI("JNI","Registering %s natives\n", className);
clazz = (env)->FindClass( className);
if (clazz == NULL) {
LOGE("JNI","Native registration unable to find class '%s'\n", className);
return -1;
}
int result = 0;
if ((env)->RegisterNatives(clazz, gJni_Methods_table, numMethods) < 0) {
LOGE("JNI","RegisterNatives failed for '%s'\n", className);
result = -1;
}
(env)->DeleteLocalRef(clazz);
return result;
}
jint JNI_OnLoad(JavaVM* vm, void* reserved){
LOGI("JNI", "enter jni_onload");
JNIEnv* env = NULL;
jint result = -1;
if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
return result;
}
jniRegisterNativeMethods(env, className, gJni_Methods_table, sizeof(gJni_Methods_table) / sizeof(JNINativeMethod));
return JNI_VERSION_1_4;
}
#ifdef __cplusplus
}
#endif
我们一个个来说,首先看JNI_OnLoad函数的实现,里面代码很简单,主要就是两个代码块,一个是if语句,一个是jniRegisterNativeMethods函数的实现。那我们一个一个来分析。1
2
3if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
return result ;
}
这里调用了GetEnv函数时为了获取JNIEnv结构体指针,其实JNIEnv结构体指向了一个函数表,该函数表指向了对应的JNI函数,我们通过这些JNI函数实现JNI编程。
然后就调用了jniRegisterNativeMethods函数来实现注册,这里面注意一个静态变量gJni_Methods_table。它其实代表了一个native方法的数组,如果你在一个Java类中有一个native方法,这里它的size就是1,如果是两个native方法,它的size就是2,大家看下这个gJni_Methods_table变量的实现1
2
3static JNINativeMethod gJni_Methods_table[] = {
{"sayHello", "(J)V", (void*)sayHello},
};
我们看到他的类型是JNINativeMethod ,那我们就来研究下JNINativeMethod
JNI允许我们提供一个函数映射表,注册给Java虚拟机,这样JVM就可以用函数映射表来调用相应的函数。这样就可以不必通过函数名来查找需要调用的函数了。Java与JNI通过JNINativeMethod的结构来建立联系,它被定义在jni.h中,其结构内容如下:1
2
3
4
5typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;
这里面有3个变量,那我们就依次来讲解下:
第一个变量name,代表的是Java中的函数名
第二个变量signature,代表的是Java中的参数和返回值
第三个变量fnPtr,代表的是的指向C函数的函数指针
下面我们再来看下jniRegisterNativeMethods函数内部的实现1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21static int jniRegisterNativeMethods(JNIEnv* env, const char* className,
const JNINativeMethod* gMethods, int numMethods)
{
jclass clazz;
LOGI("JNI","Registering %s natives\n", className);
clazz = (env)->FindClass( className);
if (clazz == NULL) {
LOGE("JNI","Native registration unable to find class '%s'\n", className);
return -1;
}
int result = 0;
if ((env)->RegisterNatives(clazz, gJni_Methods_table, numMethods) < 0) {
LOGE("JNI","RegisterNatives failed for '%s'\n", className);
result = -1;
}
(env)->DeleteLocalRef(clazz);
return result;
}
首先通过clazz = (env)->FindClass( className);找到声明native方法的类
然后通过调用RegisterNatives函数将注册函数的Java类,以及注册函数的数组,以及个数注册在一起,这样就实现了绑定。
上面在讲解JNINativeMethod结构体的时候,提到一个概念,就是”signature”即签名,我们下面就来讲解下。
JNI中的签名
Java是支持函数重载的。但如果JNI仅仅是根据函数名,是没有办法找到重载的函数的,所以为了解决这个问题,JNI就衍生了一个概念——“签名”,即参数类型和返回值类型的组合。如果拥有一个该函数的签名信息和这个函数的函数名,我们就可以顺序的找到对应的Java层中的函数了。
查看类中的方法的签名
可以使用 java -p命令(建议使用该命令来对比签名):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22javap -s -p MainActivity.class
Compiled from "MainActivity.java"
public class com.example.hellojni.MainActivity extends android.app.Activity {
static {};
Signature: ()V
public com.example.hellojni.MainActivity();
Signature: ()V
protected void onCreate(android.os.Bundle);
Signature: (Landroid/os/Bundle;)V
public boolean onCreateOptionsMenu(android.view.Menu);
Signature: (Landroid/view/Menu;)Z
public native java.lang.String stringFromJNI(); //native 方法
Signature: ()Ljava/lang/String; //签名
public native int max(int, int); //native 方法
Signature: (II)I //签名
}
我们看到上面有()V ,(Landroid/os/Bundle;)V,(Landroid/view/Menu;)Z,(II)I我们一脸懵逼,这是什么鬼,所以我们要来研究下签名的格式
JNI定义的函数签名信息
(参数1类型标示;参数2类型标示;参数3类型标示…)返回值类型标示
当参数为引用类型的时候,参数类型的标示的根式为”L包名”,其中包名的.(点)要换成”/“,看我上面的例子就差不多,比如String就是Ljava/lang/String,Menu为Landroid/view/Menu。
如果是基本类类型,其签名如下:
类型标示 | Java类型 |
---|---|
Z | boolean |
B | byte |
C | char |
S | short |
I | int |
J | long |
F | float |
D | double |
除了boolean和long,其他都是首字母大写。如果返回值是void,对应的签名是V。
这里重点说1个特殊的类型,一个是数组及Array:
类型标示 | Java类型 |
---|---|
[签名 | 数组 |
[i | int[] |
[Ljava/lang/Object | String[] |
Native调用Java代码
上面讲解了如何从JNI中调用Java类中的方法,其实在jni.h中已经定义了一系列函数来实现这一目的,下面我们就以此举例说明:
获取Class对象
为了能够在C/C++中调用Java中的类,jni.h的头文件专门定义了jclass类型表示Java中Class类。JNIEnv中有3个函数可以获取jclass。
- jclass FindClass(const char* clsName):
通过类的名称来获取jclass。- jclass GetObjectClass(jobject obj):
通过对象实例来获取jclass,相当于Java中的getClass()函数- jclass getSuperClass(jclass obj):
通过jclass可以获取其父类的jclass对象
通过类的名称获取jclass(类的全名,这时候包名不是用’”.”点号而是用”/“来区分的)。比如:1
2//获取Java中的String对象的class对象
jclass jcl_string=env->FindClass("java/lang/String");
获取属性方法
为了在C/C++获取Java层的属性和方法,JNI在jni.h头文件中定义了jfieldID和jmethodID这两种类型来分别代表Java端的属性和方法。
常见的调用Java层的方法如下,一般是使用JNIEnv来进行操作:
GetFieldID/GetMethodID:获取某个属性/某个方法
GetStaticFieldID/GetStaticMethodID:获取某个静态属性/静态方法
方法的具体实现如下:1
2
3
4jfieldID GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
jmethodID GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
jfieldID GetStaticFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
jmethodID GetStaticMethodID(JNIEnv *env, jclass clazz,const char *name, const char *sig);
他们都是4个入参,并且都一样。JNIEnv代表一个JNI环境接口,jclass上面也说了代表Java层中的”类”,name则代表方法名或者属性名,那最后一个char *sig代表签名。
构造一个对象
常用的JNI中创建对象的方法如下:
jobject NewObject(jclass clazz, jmethodID methodID, …)
比如有我们知道Java类中可能有多个构造函数,当我们要指定调用某个构造函数的时候,会调用下面这个方法,即把指定的构造函数传入进去即可:1
2jmethodID mid = (*env)->GetMethodID(env, cls, "<init>", "()V");
obj = (*env)->NewObject(env, cls, mid);
现在我们来看下他上面的二个主要参数
clazz:是需要创建的Java对象的Class对象
methodID:是传递一个方法ID,想一想Java对象创建的时候,需要执行什么操作?就是执行构造函数。
有人会说这要走两行代码,有没有一行代码的,是有的,如下:1
jobject NewObjectA(JNIEnv *env, jclass clazz, jmethodID methodID, jvalue *args);
这里多了一个参数,即jvalue *args,这里是args代表的是对应构造函数的所有参数的,我们可以应将传递给构造函数的所有参数放在jvalues类型的数组args中,该数组紧跟着放在methodID参数的后面。NewObject()收到数组中的这些参数后,将把它们传给所要调用的Java方法。
上面说到,参数是个数组,如果参数不是数组怎么处理,jni.h同样也提供了一个方法,如下:1
jobject NewObjectV(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args);
这个方法和上面不同在于,这里将构造函数的所有参数放到在va_list类型的参数args中,该参数紧跟着放在methodID参数的后面。
小结
- JNI获取类的成员变量的ID调用GetFieldID获取,通过Set[类型]Field修改变量值。
- JNI获取类的静态变量的ID调用GetStaticFieldID获取,通过SetStatic[类型]Field修改变量值。
- JNI获取类的方法的ID调用GetMethodID获取,通过Call[类型]Method调用方法。
- JNI获取类的静态方法的ID调用GetStaticMethodID获取,通过CallStatic[类型]Method调用方法。
- JNI获取类的构造方法的ID调用GetMethodID获取,通过NewObject构造,构造函数名为”
“。
参考资料
https://www.jianshu.com/p/b71aeb4ed13d
JNI学习Demo
JNI访问Java变量和方法