这是这个系列的第二篇,第一篇介绍了如何配置。这一篇介绍Java与C如何相互介绍。
没有配置过的可以去看看Android JNI开发系列之配置
首先介绍的就是Java如何调用C,而C调用Java核心使用的就是反射,下面会以此介绍。
一、Java调用C
第一篇中有个简单的例子,就是使用Java调用C,调用一个无参的native函数,并返回一个String,下面接着说点更多的情况:
- 基本类型对应情况
- 字符串处理
- 数组的处理
基本类型对应情况
因为Java和C的基本类型也有些许区别,而在这两者之间还有一个jni的类型作为桥梁连接转换类型,有一张图特别好,一看就清楚了,借了一下这位作者文章中的图,表示感谢。
下边对于数据的处理就是基于这些类型去处理的。
字符串的处理
1、首先先来一个字符串的拼接
这个也是坑了我这个萌新不少,体会到其实Java的垃圾回收机制还是很方便的。
其中在c中字符串的拼接主要就是使用strcat
方法,导入#include<string.h>
包。
还是老样子,先定义一个native方法,对于配置都是在上一篇的基础上的:
public class Hello {
static {
System.loadLibrary("Hello");
}
//传入一个字符串,拼接一段字符串后返回
public native String sayHello(String msg);
}
接着在Hello.c文件中写这个方法,这里有两种方法去写这个方法,第一种是手动自己写,也有点技巧:
- 首先看到返回的是String,对应的就是jstring
- 然后函数名就是:Java_类完全限定名_方法名,其中完全限定名,可以在Hello这个类上右键->Copy Reference,然后再把名字中间的点改为下划线。
- 然后函数的参数:前两个参数必须的,
JNIEnv *env, jobject instance
,然后第三个参数开始就是在Java中定义的方法的参数,这里传入了一个String,在这里的就改为jstring msg
,方法如下:
jstring Java_net_arvin_androidstudy_jni_Hello_sayHello(JNIEnv *env, jobject instance,
jstring msg) {
// implement code...
}
还有一种方法就是使用javah命令,处理.java文件就能得到定义的.h文件;方法就是在该项目的java目录下,使用命令javah 类的完全限定名
,在我这个项目里就是:
javah net.arvin.androidstudy.jni.Hello
这样在java目录下就有一个net_arvin_androidstudy_jni_Hello.h
文件,打开可以看到这个方法:
JNIEXPORT jstring JNICALL Java_net_arvin_androidstudy_jni_Hello_sayHello
(JNIEnv *, jobject, jstring);
其中JNIEXPORT和JNICALL关键字都可以去掉的,去掉后就和上边的方法一样了,然后自己去把参数的名字补充上即可。
最后对于字符串的拼接,没啥好说的,我这里提供一种方式:
jstring Java_net_arvin_androidstudy_jni_Hello_sayHello(JNIEnv *env, jobject instance,
jstring msg) {
char *fromJava = (char *) (*env)->GetStringUTFChars(env, msg, JNI_FALSE);
char *fromC = " add I am from C~";
char *result = (char *) malloc(strlen(fromJava) + strlen(fromC) + 1);
strcpy(result, fromJava);
strcat(result, fromC);
return (*env)->NewStringUTF(env, result);
}
- 先将jstring转为char*
- 然后把要拼接的字符串定义出来
- 接着关键来了,动态申请一块区域用于存储拼接后的字符串,申请的长度就是传进来的字符串和要添加的长度之和
- 接着就是把这两个字符串拼在一起,先使用strcpy是因为result还没有初始化,相当于把fromJava赋值给result,然后再把fromC拼接到result中
- 最后就是使用NewStringUFT将char*转换成jstring
最后就是去调用,这就简单了。
Hello jni = new Hello();
String result = jni.sayHello("I am from Java");
Log.d(TAG, result);
2、字符串比较
有了上文的介绍,这个比较就比较简单,核心就是使用strcmp
方法,Java代码如下:
public class Hello {
static {
System.loadLibrary("Hello");
}
//如果是c中要求的就返回200,否则就返回400
public native int checkStr(String str);
}
c代码如下:
jint Java_net_arvin_androidstudy_jni_Hello_checkStr
(JNIEnv *env, jobject instance, jstring jstr) {
char *input = (char *) (*env)->GetStringUTFChars(env, jstr, JNI_FALSE);
char *real = "123456";
return strcmp(input, real) == 0 ? 200 : 400;
}
这里就不接着介绍其他的处理方法了,需要时可以自己搜一下。
处理数组
同样有了上文的基础,Java代码如下:
public class Hello {
static {
System.loadLibrary("Hello");
}
public native void increaseArray(int[] arr);
}
C代码如下:
void Java_net_arvin_androidstudy_jni_Hello_increaseArray
(JNIEnv *env, jobject instance, jintArray arr) {
jsize length = (*env)->GetArrayLength(env, arr);
jint *elements = (*env)->GetIntArrayElements(env, arr, JNI_FALSE);
for (int i = 0; i < length; i++) {
elements[i] += 10;
}
(*env)->ReleaseIntArrayElements(env, arr, elements, 0);
}
可以看到:
- GetArrayLength:获取数组长度
- GetIntArrayElements:从java数组获取数组指针,注意JNI_FALSE这个参数,代码是否复制一份,false表示不复制,直接使用java数组的内存地址
- for循环,每个数组元素都加10
- 最后释放本地数组内存,最后一个参数,0表示将值修改到java数组中,然后释放本地数组,这个参数还有两个可选值:JNI_COMMIT和JNI_ABORT,前一个修改值到java数组,但是不释放本地数组内存,后一个,不修改值到java数组,但是会释放本地数组内存。
到这里Java调用C的介绍就到这里,方法基本介绍了,但是如何更好的运用还需努力实践。
C调用Java
上文中说到这个操作,主要是利用反射,这样就能调用Java代码了。
对于配置都不说了,也直接上代码,主要的细节都是在反射那里。
先来一个C调用Java无参无返回值的函数,Java代码如下:
public class CallJava {
static {
System.loadLibrary("Hello");
}
private static final String TAG = "CallJava";
//调用无参,无返回函数
public native void callVoid();
public void hello() {
Log.d(TAG, "Java的hello方法");
}
}
可以看到这里换了一个类了,但是没有影响,之后会介绍这一块知识。
C代码:
//调用public void hello()方法
void Java_net_arvin_androidstudy_jni_CallJava_callVoid
(JNIEnv *env, jobject instance) {
jclass clazz = (*env)->FindClass(env, "net/arvin/androidstudy/jni/CallJava");
jmethodID method = (*env)->GetMethodID(env, clazz, "hello", "()V");
jobject object = (*env)->AllocObject(env, clazz);
(*env)->CallVoidMethod(env, object, method);
}
这个就是四部曲:
- 获取Java中的class
- 获取对应的函数
- 实例化该class对应的实例
- 调用方法
获取Java中的class
第一步:使用FindClass
方法,第二个参数,就是要调用的函数的类的完全限定名,但是需要把点换成/
获取对应的函数
第二步:使用GetMethodID
方法,第二个参数就是刚得到的类的class,第三个就是方法名,第四个就是该函数的签名,这里有个技巧,使用javap -s 类的完全限定名
就能得到该函数的签名,但是需要在build->intermediates->classes->debug目录下,使用该命令,得到如下结果:
//else method...
public void hello();
descriptor: ()V
descriptor:后边的就是该方法的签名
实例化该class对应的实例
第三步:使用AllocObject
方法,使用clazz创建该class的实例。
调用方法
第四步:使用CallVoidMethod
方法,可以看到这个就是调用返回为void的方法,第二个参数就是第三步中创建的实例,第三个参数就是上边创建的要调用的方法。
有了这个四部就能在C中吊起Java中的代码了。
而对于有参,有返回的方法,在这四部曲的基础上,只需要修改第二步获取方法的名字和签名,其中签名以及第四步的Call<Type>Method方法,Type可以是int,string,boolean,float等等。
提示:对于基本类型又个技巧,括号内依次是参数的类型的缩写,括号右边是返回类型的缩写,用得多了就可以不用每次都去使用命令查询了,但是开始最好还是都查一下,免得出错
但是对于静态方法的调用就应该使用GetStaticMethodID
和CallStaticVoidMethod
了,而对于静态方法就不需要实例化对象,相对来说还少一步。
到这里,可能有使用过java的反射的同学有疑问了,如果是去调用private的方法,会不会报错呢,这个可以告诉你,我试过了,也是可以调用起来的,没有问题,不用担心啦。
到这里,Java调用C,C调用Java基本就算是完成了,这个代码我也会上传到github上,需要的同学可以自行下载比对,有不足之处也请多多指教。地址在文末。
添加多个C文件的配置
前文中说了,对于多文件的配置会在之后的文章中说到,果然,在第二篇中,想着方法太多了,我想放到别的文件中去处理,避免混乱了,所以就去了解了一下,在此告诉大家,其实很简答。
首先,在之前的配置基础上,再在cpp目录下创建一个文件,例如这里叫做Test.c,然后再到CMakeLists.txt文件中关联上就行了,关联方式如下:
cmake_minimum_required(VERSION 3.4.1)
add_library(Hello
SHARED
src/main/cpp/Hello.c
src/main/cpp/Test.c)
对比之前的配置,对了一行src/main/cpp/Test.c
相当于把Test.c文件也关联到叫做Hello的这个lib中。
虽然现在c代码也可以调试debug了,但是还是有打印日志才方便,printf
是没有用的,所以需要我们手动去添加一个日志库,首先在CMakeLists.txt中添加成如下:
cmake_minimum_required(VERSION 3.4.1)
add_library(Hello
SHARED
src/main/cpp/Hello.c
src/main/cpp/Test.c)
find_library(log-lib log)
target_link_libraries(Hello ${log-lib})
多了后两句代码。然后再需要用到的地方申明:
#include "android/log.h"
#define LOG_TAG "JNI_TEST"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
这样就能在这个类中使用了:
- LOGD:debug级别日志
- LOGI:info级别日志
- LOGE:error级别日志
这里就有个技巧了,定义一个Log.c文件,导入上文中的配置,然后在需要用日志的地方引入Log.c即可。
这样就不用在每个文件开头都去申明这些东西了。
示例代码
在这个项目中,java代码在包下的jni下,配置也可在相应位置查看。
感谢
部分代码来源尚硅谷Android视频《JNI》