美文网首页Android开发经验谈Android开发Android技术知识
Android JNI开发系列之Java与C相互调用

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

作者: arvinljw | 来源:发表于2018-09-03 10:16 被阅读14次

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

    相关文章

      网友评论

        本文标题:Android JNI开发系列之Java与C相互调用

        本文链接:https://www.haomeiwen.com/subject/xnhbwftx.html