实现jni头文件
本文档主要介绍如何实现jni头文件,包括JNI函数宏,JNI数据类型,Java,JNI和C/C++之间的数据传递; JNI调用JVM,动态注册本地方法等等。 本文档里提到的JNI函数的更详细说明请查看:JNI函数手册
Java调用JNI
通过头文件实现jni方法
在JNI_design中的本地方法链接部分,讲到java程序如何链接到本地方法——名字协议。 所以头文件并非是必要的,但是在头文件下实现本地方法,方向更明确,更不容易出错,也更符合C/C++风格。
这是实现jni的标准方法具体做法:
- 根据java函数原型,使用javah命令生成头文件
- 引用同文件,并实现jni方法
- 编写Android.mk和Application.mk,用Ndk-build编译实现文件,得到动态库
- java加载动态库,根据java函数原型,调用jni方法
通过javah命令生成的头文件,看起来是这样的
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_testjni_MainActivity */
#ifndef _Included_com_example_testjni_MainActivity
#define _Included_com_example_testjni_MainActivity
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_example_testjni_MainActivity
* Method: getJniStringA
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_example_testjni_MainActivity_getJniStringA
(JNIEnv *, jclass);
/*
* Class: com_example_testjni_MainActivity
* Method: getJniStringB
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_example_testjni_MainActivity_getJniStringB
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
extern C
#ifdef __cplusplus //如果程序里包含了C++,ze采用extern C声明
extern "C" { //C++是C的超集,但是二者的编译器并不相同;
#endif //当程序是混合代码时,编译C++代码用C++编译器
//编译C代码时,需要链接C的变量和函数的声明,
//按照C的编译规范编译相关代码。这样,就可以
//整个程序里混合使用C++和C了。
/*...*/
#ifdef __cplusplus
}
#endif
jni.h
jni.h文件可以在Android源码或者ndk目录下找到,它包含了很多宏,变量,数据结构和函数的定义。
JNIEXPORT和JNICALL
JNIEXPORT和JNICALL是jni.h里的宏定义,表示此函数是要被JNI调用的。
jstring
JNI里的String类型,定义在jni.h,还有其他很多类型的数据类型。它本质上是个jobject:
class _jobject {};
更多介绍查看后文。
关于ndk
可参考Ndk-build配置
直接实现jni方法
当然也可以不用头文件做中介,而直接实现jni方法,但是本地方法的原型需要仔细配置,比较容易出错。
动态注册本地方法
根据jni方法的命名规范,将导致冗长的方法命名,使用起来也不方便。可以不使用这种链接本地方法的规范,而使用 动态注册的方式实现java中定义的函数,到本地函数的映射。
静态注册与动态注册
通过——java函数原型,生成标准jni头文件,实现头文件,编译加载库,java运行时链接本地方法,完成调用——这样的方式使用 JNI,由jni本身去保证java方法和本地方法的链接,这样的本地方法的注册方式成为静态注册; 而直接从java函数原型映射到本地方法,编译加载库,用额外的操作保证java函数原型链接到本地方法,完成调用——这样的方 式使用jni,本地方法的注册方式称为动态注册。其中“额外的操作”要由我们完成。
java函数原型
假设在java文件里声明了一个函数原型:
private static native String nativeGetJniStringD();
我们想让它直接映射到一个同名的本地方法:
JNIEXPORT jstring JNICALL nativeGetJniStringD
(JNIEnv * env, jobject klass) {
return (*env)->NewStringUTF(env, "Call from Java_com_example_testjni_MainActivity_getJniStringD");
}
构造JNINativeMethod数组
首先我们要构造一个JNINativeMethod数组,JNINativeMethod在jni.h中定义:
typedef struct {
const char* name; //java函数原型的名字
const char* signature; //java函数原型的签名,用(参数类型)返回值类型的格式组织,比如"()Ljava/lang/String;",
//具体介绍在jni数据结构里详细介绍
void* fnPtr; //本地函数指针
} JNINativeMethod;
在本例中,可以这样写:
static JNINativeMethod gMethods[] = {
/* name, signature, funcPtr */
{ "nativeGetJniStringD", "()Ljava/lang/String;", (void*) nativeGetJniStringD }
};
注册单个类里的本地方法
假设已经为单个类的本地方法建立了一个JNINativeMethod数组,可以这样去注册它:
/*
* 为一个类注册所有的本地方法
*/
static int registerNativeMethods(JNIEnv * env, const char* className,
JNINativeMethod* gMethods, int numMethods)
{
jclass 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;
}
为所有类注册本地方法
/*
* 为所有java类注册本地方法,示例只有一个类
*/
static int registerNatives(JNIEnv * env)
{
if (!registerNativeMethods(env,
"com/example/testjni/MainActivity", //类名,每类是不相同的
gMethods, sizeof(gMethods) / sizeof(gMethods[0])))
return JNI_FALSE;
return JNI_TRUE;
}
实现JNI_onLoad方法
JNI_onLoad方法在jni.h中定义,它在java类调用System.LoadLibarary方法后调用,去注册本地方法。
/*
*
* 实现JNI_OnLoad 方法,此方法在System.LoadLibrary后调用
*/
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM * vm, void* reserved)
{
JNIEnv* env = NULL;
jint result = -1;
if ((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK) {
return result;
}
assert(env != NULL);
if (!registerNatives(env)) {
return result;
}
/* success -- return valid version number */
result = JNI_VERSION_1_4;
return result;
}
小结
实现和链接本地方法有三种方式:
- 通过头文件实现,静态注册
- 直接实现,静态注册
- 直接实现,动态注册
三种方式其实互相独立,互不影响。所以在一个实现文件里,可以同时包含这三种实现方式。
JNI调用java
一个示例
假设有个java层定义了一个类:
package com.example.calljava;
public class Student {
public static final String TAG = "Student";
public String name;
private long score;
public Student(String name, long score) {
this.name = name;
this.score = score;
}
public long getScore() {
return score;
}
public void setScore(long score) {
this.score = score;
}
public static String getTag() {
return TAG;
}
@Override
public String toString() {
return "Student [name=" + name + ", score=" + score + "]";
}
}
类里面有public, private, static成员和方法,在JNI层去使用这个类:
void jniCallJava(JNIEnv * env) {
jclass cls = NULL;
jmethodID mid = NULL;
jfieldID fid = NULL;
jobject obj = NULL;
jstring arg = NULL;
jstring ret = NULL;
//创建一个con.example.calljava.Student类的引用
cls = (*env)->FindClass(env, "com/example/calljava/Student");
//获取此类的构造函数
mid = (*env)->GetMethodID(env, cls, "<init>", "(Ljava/lang/String;J)V");
//创建一个实例
if (cls != NULL && mid != NULL) {
arg = (*env)->NewStringUTF(env, "Jack");
obj = (*env)->NewObject(env, cls, mid, arg, 100L);
}
fid = (*env)->GetFieldID(env, cls, "name", "Ljava/lang/String;");
if (fid != 0) {
//访问Student的name共有成员
ret = (jstring)(*env)->GetObjectField(env, obj, fid);
//修改Student的那么有成员
arg = (*env)->NewStringUTF(env, "icejoywoo");
(*env)->SetObjectField(env, obj, fid, arg);
}
//通过getter访问Student的score成员
mid = (*env)->GetMethodID(env, cls, "getScore", "()J");
if (mid != 0) {
jlong score = (*env)->CallLongMethod(env, obj, mid);
}
//通过setter方法修改Student的score成员
mid = (*env)->GetMethodID(env, cls, "setScore", "(J)V");
if (mid != 0) {
jlong math_score = 99;
(*env)->CallVoidMethod(env, obj, mid, math_score);
}
//调用toString方法
mid = (*env)->GetMethodID(env, cls, "toString", "()Ljava/lang/String;");
if (mid != 0) {
ret = (jstring)(*env)->CallObjectMethod(env, obj, mid);
}
fid = (*env)->GetStaticFieldID(env, cls, "TAG", "Ljava/lang/String;");
if (fid != 0) {
//访问Student的TAG静态共有成员
ret = (jstring)(*env)->GetStaticObjectField(env, cls, fid);
}
//调用静态共有方法
mid = (*env)->GetStaticMethodID(env, cls, "getTag", "()Ljava/lang/String;");
if (mid != 0) {
ret = (jstring)(*env)->CallStaticObjectMethod(env, cls, mid);
}
}
创建对象
创建java对象,就是要创建一个jobject实例。关于jobject将在jni数据结构里介绍,它的本质是一个结构体指针。
- 首先创建一个java对象的引用,调用FindClass方法
- 获取java对象的构造函数的映射,调用GetMethodID
- 将构造函数里的java参数类型映射为jni数据类型,java基本类型数据映射成jni基本类型数据,java字符串映射成jstring, 其他java类类型映射成jobject
- 通过NewObject生产jni本地对象。
访问public成员
- 先使用GetFieldID, 获取成员的域对象jfieldID
- 然后调用Get
Field,访问相应的域 - 可以调用Set
Field来修改域成员,要注意此过程里的数据类型转换
访问public方法
- 先使用GetMethodID, 获取成员的域对象methodId
- 然后调用Call
Method,Call MethodA或Call MethodV调用相应的Java方法
访问public static成员
与访问public成员方式相同,只要将用到的jni方法换成static版本的即可,比如GetFieldID换成GetStaticFieldID
访问public static方法
与访问public成员方式相同,只要将用到的jni方法换成static版本的即可