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

这是这个系列的第二篇,第一篇介绍了如何配置。这一篇介绍Java与C如何相互介绍。

没有配置过的可以去看看Android JNI开发系列之配置

首先介绍的就是Java如何调用C,而C调用Java核心使用的就是反射,下面会以此介绍。

一、Java调用C

第一篇中有个简单的例子,就是使用Java调用C,调用一个无参的native函数,并返回一个String,下面接着说点更多的情况:

  • 基本类型对应情况
  • 字符串处理
  • 数组的处理

基本类型对应情况

因为Java和C的基本类型也有些许区别,而在这两者之间还有一个jni的类型作为桥梁连接转换类型,有一张图特别好,一看就清楚了,借了一下这位作者文章中的图,表示感谢。

type_relationship.png

下边对于数据的处理就是基于这些类型去处理的。

字符串的处理

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等等。

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

但是对于静态方法的调用就应该使用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》

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容