Jni的两种调用使用方式

1.配置信息

本篇博客用的是CLion去做c代码的编写,因为用的是Windows系统,最终会编库译成dll格式的库文件,然后去使用AndroidStudio去运行Java代码,引入这个库文件,实现双向的简单交互,本质和用AndroidStudio生成so文件是类似的,只做演示处理

先是在CLionCMakeLists中添加库的声明

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

第二个字段是STATICSHARED或者是MODULE
STATIC 表示静态链接库
SHARED 表示动态链接库
我们平常使用的都是动态链接库,静态链接库使用的场合很少
MODULE 这个不做讨论,只针对部分系统有效
第三个字段是EXCLUDE_FROM_ALL,是指定哪些文件不被编译,我这里并没有使用这个字段

最后是指定的具体文件了,如果指定了EXCLUDE_FROM_ALL那就是指定哪些文件不被编译。未指定就是要把哪些文件编译进去。

2.静态方式调用

先定义一个静态的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 <stdio.h>
#include <string.h>
#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

3.动态方式调用

动态方式调用则是通过方法注册的形式去操作的,没有了静态调用的.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 方法可以反射去调用相应的静态方法,返回值是ObjectCall...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
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值