本篇博客用的是CLion
去做c代码的编写,因为用的是Windows
系统,最终会编库译成dll
格式的库文件,然后去使用AndroidStudio
去运行Java
代码,引入这个库文件,实现双向的简单交互,本质和用AndroidStudio
生成so
文件是类似的,只做演示处理
先是在CLion
的CMakeLists
中添加库的声明
add_library(jnitest-lib SHARED test/testJni.c test/com_learn_jnidemo_TestJni.h)
这里add_library
是方法名,这里传入了多个字段,其中格式描述对应是
add_library(<name> [STATIC | SHARED | MODULE]
[EXCLUDE_FROM_ALL]
[source1] [source2] [...])
jnitest-lib
是声明生成的库文件名name
,生成时会自动添加lib
前缀,这里最终生成的文件名就是libjnitest-lib.dll
文件了
SHARED
第二个字段是STATIC
,SHARED
或者是MODULE
STATIC
表示静态链接库
SHARED
表示动态链接库
我们平常使用的都是动态链接库,静态链接库使用的场合很少
MODULE
这个不做讨论,只针对部分系统有效
第三个字段是EXCLUDE_FROM_ALL
,是指定哪些文件不被编译,我这里并没有使用这个字段
最后是指定的具体文件了,如果指定了EXCLUDE_FROM_ALL
那就是指定哪些文件不被编译。未指定就是要把哪些文件编译进去。
先定义一个静态的Jni的本地方法类
package com.learn.jnidemo;
public class TestJni {
public static native String staticMethod(String pass);
public native String normalMethod(String pass);
}
这里定义了两个方法,一个静态的方法,一个非静态的方法
先用java指定把这个java
文件转成class
文件
com\learn\jnidemo>javac TestJni.java
然后进入包的根目录,对于项目其实是进入到....src\main\java
这个目录下,执行javah
指令生成一个.h
的头文件,如果熟悉.h
的规则,其实可以自己手写不用主动生成(主要是懒)
\src\main\java>javah com.learn.jnidemo.TestJni
这里需要指定完整的文件名,即包名要写全,但不需要指定后缀名
然后会生成一个com_learn_jnidemo_TestJni.h
的头文件,把这个文件拷贝到CLion
中,(Studio中其实也是可以编写的)
修改头文件信息,引入#include "jni.h"
。会发现这里报红,因为缺少jni的库,我们可以把jdk
中的jni.h
文件拷过来,同时拷过来的还有jni_md.h
文件。
分别在Java\jdk1.8.0_131\include\
和Java\jdk1.8.0_131\include\win32
目录下
这样com_learn_jnidemo_TestJni.h
的内容就变成
/* DO NOT EDIT THIS FILE - it is machine generated */
#include "jni.h"
#ifndef _Included_com_learn_jnidemo_TestJni
#define _Included_com_learn_jnidemo_TestJni
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT jstring JNICALL Java_com_learn_jnidemo_TestJni_staticMethod
(JNIEnv *, jclass, jstring);
JNIEXPORT jstring JNICALL Java_com_learn_jnidemo_TestJni_normalMethod
(JNIEnv *, jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif
.h
文件只是一个方法的声明,我们需要实现自己的.c
文件,在同目录下建立一个c
文件,把.h
的文件里的方法拷过来进行实现。
#include
#include
#include "com_learn_jnidemo_TestJni.h"
JNIEXPORT jstring
JNICALL Java_com_learn_jnidemo_TestJni_staticMethod
(JNIEnv *env, jclass jclass, jstring jstring) {
jboolean jb;
const char *c_str = NULL;
c_str = (*env)->GetStringUTFChars(env, jstring, &jb);
if (!jb) return NULL;
char buff[100] = {};
sprintf(buff, "staticMethodString->>%s", c_str);
(*env)->ReleaseStringUTFChars(env, jstring, c_str);
return (*env)->NewStringUTF(env, buff);
}
JNIEXPORT jstring
JNICALL Java_com_learn_jnidemo_TestJni_normalMethod
(JNIEnv *env, jobject jobject, jstring jstring) {
jboolean jb;
const char *c_str = NULL;
c_str = (*env)->GetStringUTFChars(env, jstring, &jb);
if (!jb) return NULL;
char buff[100] = {};
sprintf(buff, "normalMethodString->>%s", c_str);
(*env)->ReleaseStringUTFChars(env, jstring, c_str);
return (*env)->NewStringUTF(env, buff);
}
这里JNIEnv
表示的Java的环境,JNIEnviroment
,也可以理解是c和java交互的桥梁
比如我这里使用GetStringUTFChars
方法把java的字符串jstring
转换成立了c中的常量字符指针const char *c_str
,然后返回的时候使用NewStringUTF
把字符数组转成java中的字符串
注意这里的jstring其实也是一种Object,在这里是属于jobject类型的
第二个参数jobject
表示的是加载这个方法所对应的类,我们一般声明在同一个类中,所以这里指向的我们的TesetJni
这个对象本身
对于普通方法第二个参数是jobject
,对于静态方法,第二个参数是jclass
,指向的是类信息,毕竟静态方法不需要通过实例去调用
最后一个字段jstring
其实就是我们上面定义的native
方法中传入的参数值,如果定义一个无参的方法是没有这个字段的
这里只是很简单的通过c方法把传入的参数拼接后返回出去而已
然后通过Clion
的build编译会在cmake-build-debug
中生成一个libjnitest-lib.dll
的文件
我们去加载这个库文件,并打印部分信息
public class TestJni {
static {
System.load("E:\\Program Files.....\\Demo\\cmake-build-debug\\libjnitest-lib.dll");
}
public static void main(String[] args) {
System.out.println("main start");
System.out.println(staticMethod("fun1"));
System.out.println(new TestJni().normalMethod("fun2"));
System.out.println("main end");
}
public static native String staticMethod(String pass);
public native String normalMethod(String pass);
}
通过静态代码块去加载,调用System.load
方法传入全路径,注意这里的文件目录间使用\\
分隔。
而System.loadLibray
方法调用库文件只需要传入库的文件名,比如这里只需要传入jnitest-lib
,去掉lib
前缀,但文件位置有要求,比如放到资源指定的位置中,比如
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
打印结果是
main start
staticMethodString->>fun1
normalMethodString->>fun2
main end
动态方式调用则是通过方法注册的形式去操作的,没有了静态调用的.h
文件的处理
在System.load
方法去调用jni
的代码时,会首先调用这么一个方法
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *reversed)
我们需要的就是在这里去注册我们native
方法就可以了
同样定义一个native
方法的操作类
public class TestJni2 {
public static native String dyStaticMethod(String pass);
public native String dynormalMethod(String pass);
}
创建一个testdy.c
的文件,添加头文件信息,和上面的类似,比如"jni.h"
1.定义native
的方法信息
static const JNINativeMethod methods[] = {
{"dyStaticMethod", "(Ljava/lang/String;)Ljava/lang/String;", (void *) staticFun},
{"dynormalMethod", "(Ljava/lang/String;)Ljava/lang/String;", (void *) normalFun}
};
这个是JNINativeMethod
的构造体信息
typedef struct {
char *name;
char *signature;
void *fnPtr;
} JNINativeMethod;
第一个参数是 方法名
第二个参数是 方法的签名
,和smali
的汇编语言很类似
第三个参数是 方法指纹
,也就是我们需要定义方法的具体实现
方法签名的话也可以通过java指令去获取
com\learn\jnidemo>javap -s -p TestJni2.class
这里获取的结果是
public class com.learn.jnidemo.TestJni2 {
public com.learn.jnidemo.TestJni2();
descriptor: ()V
public static native java.lang.String dyStaticMethod(java.lang.String);
descriptor: (Ljava/lang/String;)Ljava/lang/String;
public native java.lang.String dynormalMethod(java.lang.String);
descriptor: (Ljava/lang/String;)Ljava/lang/String;
}
2.定义类名
static const char *cName = "com/learn/jnidemo/TestJni2";
这里需要定义完整的类名,使用/
分隔
3.处理JNI_OnLoad
方法
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *reversed) {
printf("dynamic onload executed\n");
JNIEnv *env = NULL;
int r = (*vm)->GetEnv(vm, (void **) &env, JNI_VERSION_1_4);
if (r != JNI_OK) {
return -1;
}
jclass jc = (*env)->FindClass(env, cName);
r = (*env)->RegisterNatives(env, jc, methods, 2);
if (r != JNI_OK) {
return -1;
}
printf("dynamic onload finished\n");
return JNI_VERSION_1_4;
}
注册的方法比较固定
先通过(*vm)->GetEnv(vm, (void **) &env, JNI_VERSION_1_4)
方法获取到JNIEnv
这里有三个值,第一个是JavaVM
也就是JVM
第二个参数传的是一个二级指针,就是上面定义的*env
的二级地址,因为通过方法修改指针的值得传入这个指针的地址,方法中去重新处理指针的偏向。C语言中方法对变量的处理都是通过变量的地址去操作的,比如交换两个int值int a = 10; int b =20
,那么就会定义方法
int swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
或者在c++
中可以使用
int swap2(int &a, int &b) {
int temp = a;
a = b;
b = temp;
}
最后一个是JNI对应的版本JNI_VERSION_1_4
,按实际情况调整
FindClass
和java中的反射类似,通过JNIEnv
和类名去获取JVM中加载的类信息
RegisterNatives
方法是动态注册的方法,把上面定义的类信息,方法信息传入,最后还要接一个方法数,一般和native
定义的个数相同
4.实现native
方法
jstring staticFun(JNIEnv *env, jclass jc, jstring jsp) {
char *str = "dynamic static fun excute";
printf("dynamic staic jni executed\n");
return (*env)->NewStringUTF(env, str);
}
jstring normalFun(JNIEnv *env, jobject jobject, jstring jsp) {
char *str = "dynamic normal fun excute";
printf("dynamic normal jni executed\n");
return (*env)->NewStringUTF(env, str);
}
5.编译并加载jni库
public class TestJni2 {
static {
System.load("E:\\Program Files...\\firstDemo\\cmake-build-debug\\libjnitest-lib2.dll");
}
public static void main(String[] args) {
System.out.println("main start");
System.out.println(dyStaticMethod(""));
System.out.println(new TestJni2().dynormalMethod(""));
System.out.println("main end");
}
public static native String dyStaticMethod(String pass);
public native String dynormalMethod(String pass);
}
代码和上面的类似,打印结果是
main start
dynamic static fun excute
dynamic normal fun excute
main end
dynamic onload executed
dynamic onload finished
dynamic staic jni executed
dynamic normal jni executed
可以发现,jni内的打印方法会比java方法的要慢,因为jni这个输出到控制台是跨JVM平台的,相对于java是比较耗时间的
上面提到用了反射,那么能否通过jni去反射调用java的方法呢?
答案也是可以的
比如我这里再定义一个非native
的方法
public class TestJni2 {
......
public static String logMsgMethod(String msg) {
String res = "logMsgMethod execute ->>" + msg + " !!!!!!! ";
System.out.println("logMsgMethod excuted :" + res);
return res;
}
}
也很简单,只是把字符串拼接后再返回出去,并添加一行打印,然后修改jni的方法去调用这个方法
jstring staticFun(JNIEnv *env, jclass jc, jstring jsp) {
char *str = "dynamic static fun excute";
printf("dynamic staic jni executed\n");
jmethodID jmd = (*env)->GetStaticMethodID(env, jc, "logMsgMethod", "(Ljava/lang/String;)Ljava/lang/String;");
jstring str2 = (*env)->NewStringUTF(env, "pass static mags");
jstring jsr = (*env)->CallStaticObjectMethod(env, jc, jmd, str2);
const char *str3 = (*env)->GetStringUTFChars(env, jsr, NULL);
printf("static fun pass ->>> %s\n", str3);
return (*env)->NewStringUTF(env, str);
}
GetStaticMethodID
方法可以获取相对应的静态方法,非静态去掉static
就可以了。这个方法里需要传入类信息,方法名以及方法签名,然后返回获取到的方法id。
CallStaticObjectMethod
方法可以反射去调用相应的静态方法,返回值是Object
。Call...Method
有很多种,中间的是返回值,比如CallIntMethod
,CallByteMethod
,CallBooleanMethod
等。需要传入类信息和上面获取的方法id。
从java中返回的值jstring
,jint
等需要转成c中的数据结构才能正常被c处理,GetStringUTFChars
就是把jstring
转成char*
的处理。
打印结果是
main start
logMsgMethod excuted :logMsgMethod execute ->>pass static mags !!!!!!!
dynamic static fun excute
dynamic normal fun excute
main end
dynamic onload executed
dynamic onload finished
dynamic staic jni executed
static fun pass ->>> logMsgMethod execute ->>pass static mags !!!!!!!
dynamic normal jni executed