因为adnroid项目中用到了获取mac地址做为唯一码的功能,c和c++在android却没有对应的api,还好有jni这个东西,用java写好获取mac地址的代码,用c/c++调用代码就可以直接获取mac字符串了。
JNI(Java Native Interface)Java本地接口,详情点击此处,就是java用来与本地的已经编译的语言交互的接口。这里使用的环境是windows 7 64,gcc 4.6.2,jdk 1.6。
首先看在Java中调用c/c++:
创建Java类(JavaJni),创建要交互的本地方法,使用native关键字,加载我们要调用的动态库。
编译创建的Java类,无错生成.class文件。
通过JDK中javah工具生成本地方法声明的头文件。(这里没有使用包名,正确方法是javah 包名.类名)
生成本地声明的头文件之后,编写对应的c或者c++文件,实现具体的本地方法。
编译生成动态链接库,windows下是dll文件,linux是so文件。这里是使用windows下的MinGW编译生成dll文件。(因为windows下jni的默认是使用vc的调用约定, 所以这步使用gcc生成dll文件时候需要注意不同平台调用约定的实现,下文会说)。
生成动态库后,将其放入java.library.path所指定的路径中,可能通过getProperty方法查看java.library.path所在的路径,将其放入任意一个即可加载。
执行java JavaJni,查看结果。
下面写一个测试,具体实现一下上面的步骤:
a. 创建Java类(JavaJni)如下:
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
39public class JavaJni
{
public static void main (String [] args)
{
JavaJni jni = new JavaJni();
//本地方法测试
jni.jniVoidTest();
System.out.println("===============================");
System.out.println(jni.jniStringTest());
System.out.println("===============================");
System.out.println(jni.jniIntTest());
System.out.println("===============================");
System.out.println(jni.jniWithString("From JavaJni"));
System.out.println("===============================");
int[] array = jni.jniArray();
for (int i = 0; i < array.length; i++)
{
System.out.println(array[i]);
}
System.out.println("===============================");
int[] temp = {1, 2, 3, 4, 5, 6, 7};
System.out.println(jni.jniWithArray(temp, temp.length));
System.out.println("===============================");
}
//类初始化时候加载动态库
static
{
System.loadLibrary("JavaJni");
}
//本地方法声明 使用native关键字
native void jniVoidTest();
native String jniStringTest();
native int jniIntTest();
native String jniWithString(String str);
native int[] jniArray();
native boolean jniWithArray(int[] array, int len);
}native关键字声明本地dll中要调用的方法,静态块在类初始化的时候加载动态库。主方法中调用本地方法做一个简单的测试,六个不同的返回值,不同参数的测试方法。
b.编译创建的Java类:
1
javac JavaJni.java
c. 通过javah工具生成本地方法声明的头文件,由于这里没有指定包名,直接使用类名:
1
javah JavaJni(注意这里是JavaJni,没有后缀)
这里会生成一个JavaJni.h的本地方法的头文件声明,类似如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15/* DO NOT EDIT THIS FILE - it is machine generated */
/* Header for class JavaJni */
extern "C" {
/*
* Class: JavaJni
* Method: jniVoidTest
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_JavaJni_jniVoidTest
(JNIEnv *, jobject);#ifdef __cplusplus
}
#endif
#endif1
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
62
63
64
65
66
d. 如注释所说,这是机器生成的方法声明,直接使用即可。现在要对应头文件创建JavaJni.c文件实现每个声明的本地方法,这里做简单的实现:
```c
#include <stdio.h>
#include <assert.h>
#include "JavaJni.h"
#define BUFF_SIZE 5
JNIEXPORT void JNICALL Java_JavaJni_jniVoidTest(JNIEnv *env, jobject obj)
{
printf(">>: Excute [jniVoidTest] success\n");
}
JNIEXPORT jstring JNICALL Java_JavaJni_jniStringTest(JNIEnv *env, jobject obj)
{
char *static_str = "excute [jniStringTest] success\n";
//创建可供返回的jstring
jstring jstr = (*env)->NewStringUTF(env, static_str);
return jstr;
}
JNIEXPORT jint JNICALL Java_JavaJni_jniIntTest(JNIEnv *env, jobject obj)
{
printf("excute [jniIntTest] success\n");
return 1024;
}
JNIEXPORT jstring JNICALL Java_JavaJni_jniWithString(JNIEnv *env, jobject obj, jstring str)
{
const char *temp = NULL;
//由传入的参数获取c类型的字符串
temp = (*env)->GetStringUTFChars(env, str, 0);
//只做测试,简单的写一个断言,其他的要做对应的参数为空处理
assert(temp);
printf(temp);
//减引用计数
(*env)->ReleaseStringUTFChars(env, str, temp);
char *r_str = "excute [jniWithString] success\n";
jstring jstr = (*env)->NewStringUTF(env, r_str);
return jstr;
}
JNIEXPORT jintArray JNICALL Java_JavaJni_jniArray(JNIEnv *env, jobject obj)
{
jint tmp_buffer[BUFF_SIZE] = {0};
for (int i = 0; i < BUFF_SIZE; i++)
{
tmp_buffer[i] = i + 4;
}
jintArray int_array = (*env)->NewIntArray(env, BUFF_SIZE);
//将数所设设置到将要返回给java端的数组中去
(*env)->SetIntArrayRegion(env, int_array, 0, BUFF_SIZE, tmp_buffer);
return int_array;
}
JNIEXPORT jboolean JNICALL Java_JavaJni_jniWithArray(JNIEnv *env, jobject obj, jintArray array, jint len)
{
//此处使用从java处copy数组数据到此,不对java处的数据做修改
jint tmp_array[len];
//由java处的数组获取数组数据,之后不要忘记减少引用计数
(*env)->GetIntArrayRegion(env, array, 0, len, tmp_array);
for (int i = 0; i < len; i++)
{
printf("%d\t", tmp_array[i]);
}
printf("\n");
}关键部分都做了注释,注意的地方就是:NewStringXXX之后不要忘记ReleaseString减少对应的引用计数以防止内存泄露。
e. 编译上一步所写的实现文件生成动态库,这里环境是windows,所以使用gcc生成dll文件。
1 | gcc -Wl, --kill-at -std=c99 -shared -o JavaJni.dll JavaJni.c |
参数一个一个看:
-std=c99 这里是使用C语言的c99标准,包括for循环的初始化部分变量声明和变长数组。
-shared 链接器选项,用于生成一个共享库目标文件。也就是生成这个动态库的选项。
-o 指定输出(地球人都知道)
-Wl, 这个选项是把这个后面的选项传递给链接器。后面的–kill-at才是真正起作用的。
由于JNI加载dll的中导出的函数名在windows下是与MSVC相同的。而JNI的调用约定使用的是__stdcall,gcc下生成的dll导出的函数名是func_name@shift_value的形式。MSVC下生成的dll导出的函数名是func_name,这里出现了差异,那么JNI在使用gcc生成的dll的时候就会出现找不到对应的函数名的错误,链接错误,是因为导出的函数名不同的原因。所以才会使用Wl, –kill-at给链接器的选项,使生成的dll导出的函数名是func_name这种形式,kill-at的意思也就是去掉@之后的部分。[这里如有说得不对,请指正,非常感激!]这里是一个不同的调用约定,在不同编译器下导出函数名:
还可以使用另一个链接器的选项,-Wl,–add-stdcall-alias即同时导出func_name@shift_value和func_name二种函数名的。
这里生成动态库的时候可能还会出现jni.h找不到的情况,在MinGW下最简单的方法就是把JDK目录下的include下的所有文件都拷贝到MinGW的include目录中(包括win32目录下的头文件都复制到MinGW下的include文件夹中)。这样gcc就可以找到jni.h了。
f. 生成dll之后加入java.library.path中,如果不知道具体的目录在哪,可以放到system32,或者自己看一下java.library.path,如下:
1 | class OutJavaPath |
得到path的值之后,把dll放到合适的目录。
g. 执行JavaJni(java JavaJni)。到这里就会看到console里的输出。如下:
到这里就是使用Java本地接口JNI调用本地语言实现的实现的整个过程。