Android JNI开发系列之Java与C相互调用

这是这个系列的第二篇,第一篇介绍了如何配置。这一篇介绍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中的代码了。

而对于有参,有返回的方法,在这四部曲的基础上,只需要修改第二步获取方法的名字和签名,其中签名以及第四步的CallMethod方法,Type可以是int,string,boolean,float等等。

提示:对于基本类型又个技巧,括号内依次是参数的类型的缩写,括号右边是返回类型的缩写,用得多了就可以不用每次都去使用命令查询了,但是开始最好还是都查一下,免得出错

但是对于静态方法的调用就应该使用GetStaticMethodIDCallStaticVoidMethod了,而对于静态方法就不需要实例化对象,相对来说还少一步。

到这里,可能有使用过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即可。

这样就不用在每个文件开头都去申明这些东西了。

示例代码

Android JNI学习

在这个项目中,java代码在包下的jni下,配置也可在相应位置查看。

感谢

部分代码来源尚硅谷Android视频《JNI》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值