JNI调用总结

一,CMakeLists 

英文文档:https://cmake.org/documentation/
中文文档:https://www.zybuluo.com/khan-lau/note/254724
1,路径配置,在buildGradle的android层目中进行配置例如:

 

externalNativeBuild {
    cmake {
        path "src/main/cpp/CMakeLists.txt"
        version "3.10.2"
    }
}


 path配置cmake的引用路径,version配置cmake的版本
2,添加库,在cmakeList中调用add_library函数
     里面传入三个参数
  第一个是要引入的库别名,第二个是库的类型,是静态库还是动态库。在Android中是不能引用静态库的也就是.dll后缀的库,所以第二个参数只要是引入第三方的.so库都是写“SHARED”,第三个是通过什么样方式引入进来,第三方的一般都是通过包含进来,所以第三个参数基本也是固定的都是写“IMPORTED”。
add_library后,就要设置.so的详细路径了,通过set_target_properties()函数来设置;该函数也是要传入三参数来指定.so库的路径。第一个参数和add_library的第一个参数一样,不过这里的库别名要和add_library的库别名要一致,要不然在编译时会报找不到库的错误。第二个参数是固定的,都是写“ PROPERTIES IMPORTED_LOCATION”主要用来指定库的引入方式。都是通过本地引入。第三个就是库的具体路径,这个不能写错,如果写错了,编译时也同样会找不到库的。只要是引入第三方的库使用add_library就要使用set_target_propeties这个组合,所以它们是成对出现的。3,编译我们自己在项目中写的c/c++文件
调用方式和上面的add_library方法类似,前两个参数和上面传入的是一样的,最后一个参数写对应我们的c++的文件路径,可以写多个路径,如下:

 

add_library( # Sets the name of the library.
        main-lib

        # Sets the library as a shared library.
        SHARED

        # Provides a relative path to your source file(s).
        main-lib.cpp
        runoobtest.cpp)


这里就不用set_target_propeties()
4,下面一个函数是find_library
这个方法是用来寻找系统库的,如果有用用到系统库就要调用这个函数
第一个参数是库的别名,第二个参数表示该库在系统ndk中的名字如下:

 

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

find_library( # Sets the name of the path variable.
        log-lib

        # Specifies the name of the NDK library that
        # you want CMake to locate.
        log)


   5,下一个方法是target_link_libraries,依赖的库
如果是系统的库要用这个格式${库的名字}。
如下:

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
        native-lib

        # Links the target library to the log library
        # included in the NDK.
        ${log-lib})

target_link_libraries( # Specifies the target library.
        main-lib
        # Links the target library to the log library
        # included in the NDK.
        ${log-lib}
        )

二,java调用层C代码流程
   第一种方式静态注册:
   1,首先加载共享库,加载库一般是在在静态代码块中进行:

 static {
    System.loadLibrary("native-lib");
}

    2,在Java中声明native方法
public native String stringFromJNI();
   3,实现原生方法

 

 #include <jni.h>
#include <string>
#include <android/log.h>

extern "C" JNIEXPORT jstring JNICALL
Java_com_test_jni_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    __android_log_print(ANDROID_LOG_ERROR,"TEST","HELLO ....... native lib");
    return env->NewStringUTF(hello.c_str());
}


为了找到c代码中对应的方法,需要要Java关键字开头,在方法前面加上包名,并且以下划线隔开。上面的stringFromJjni声明在了com.test.jni这个包名的MainActivity里面。
这样在java代码里就可以调用stringFromJni这个方法了
第二种方式:动态注册
1,和静态注册一样,先加载共享库

 

static {
    System.loadLibrary("dynamic-lib");
}


2,在java中声明native方法
 

public native int sum(int x, int y);
public native String getNativeString();


3,在c代码中中添加回调函数JNI_OnLoad方法,loadLibrary会执行该方法。
然后在JNI_OnLoad方法中调用方法RegisterNatives就完成注册了如下:

 

JNIEXPORT int JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env;
    if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
        return JNI_FALSE;
    }
    registerNativeMethonds(env,JAVA_CLASS,gMethods,2);
    LOGE("jni onload call");
    return JNI_VERSION_1_6;
}
int registerNativeMethonds(JNIEnv *env, const char *name,
        JNINativeMethod *methods,jint nMethods) {
    jclass jcls;
    jcls = env->FindClass(name);
    if (jcls == nullptr) {
        return JNI_FALSE;
    }
    if (env->RegisterNatives(jcls, methods, nMethods) < 0) {
        return JNI_FALSE;
    }
    return JNI_TRUE;

}

static JNINativeMethod gMethods[] = {
        { "getNativeString","()Ljava/lang/String;",(void*)getMessage},
        { "sum","(II)I",(void*)plus}
};


RegisterNatives接收三个参数,
一地个参数,clazz就是native函数所在的类,可通过FindClass(将.换成/)获取;第二个单数methods是一个数组,其中包含注册信息,第三个参数nMethods是方法数量。上面的例子中我调用了本地的plus和sum两个方法所以传了一传了个2
FindClass里传入的就是类名,例如本例中我传入的宏定义JAVA_CLASS
#define JAVA_CLASS "com/example/nativetest/load/JNIDynamicLoad"
就是将.换成了/。
4,NINativeMethod是个结构体,里面有三个成员变量
第一个是java层的方法名,第二个是方法签名,第三个是C函数指针。这样三个参数就便成了一组注册信息。
5,然后在java层中直接调用就可以调用c层的代码了:

 

JNIDynamicLoad jniDynamicLoad = new JNIDynamicLoad();
jniDynamicLoad.getNativeString() 
jniDynamicLoad.sum(5,6)


对应C层代码:
 

jstring getMessage(JNIEnv *env, jobject obj) {
    return env->NewStringUTF("this is msg");
}
jint plus(JNIEnv *env, jobject jobject1, int x, int y) {
    return x + y;
}


6,方法签名如下:


       备注:long对应的类型的签名应该是J,有的博客博客写的是L可能笔误
     括号里面表示函数方法参数,尾部表示返回值
例如:
方法 1):
public string addTail(String tail, int index)
其对应的签名如下:
(Ljava/util/String;I)Ljava/util/String;
方法 2):
public int addValue(int index, String value,int[] arr)
其对应的签名如下:
(ILjava/util/String;[I)I
         无返回值类型: void set (byte[ ] bytes) 
签名:([B)V

注意:引用类型后面要加分号


三,C调用Java代码
主要是利用反射,这样就能调用Java代码了。

      1,先写一个java的方法

public void hello(){

    Log.e("CallJava","hello I am from java");
}

2,调用四部曲:
//调用public void hello()方法

 

void callJavaVoid(JNIEnv *env, jobject instance) {
    jclass clazz = env->FindClass("com/test/jni/CallJava");
    jmethodID method = env->GetMethodID(clazz, "hello", "()V");
    jobject object = env->AllocObject(clazz);
    env->CallVoidMethod(object, method);
}


a,获取Java中的class
b,获取对应的函数
c,实例化该class对应的实例
D,调用方法
获取Java中的class
第一步:使用FindClass方法,第二个参数,就是要调用的函数的类的完全限定名,但是需要把点换成/
获取对应的函数
第二步:使用GetMethodID方法,第二个参数就是刚得到的类的class,第三个就是方法名,第四个就是该函数的签名
实例化该class对应的实例
第三步:使用AllocObject方法,使用clazz创建该class的实例。
调用方法
第四步:使用CallVoidMethod方法,可以看到这个就是调用返回为void的方法,第二个参数就是第三步中创建的实例,第三个参数就是上边创建的要调用的方法。
有了这个四部就能在C中调用Java中的代码了。
而对于有参,有返回的方法,在这四部曲的基础上,只需要修改第二步获取方法的名字和签名,其中签名以及第四步的Call<Type>Method方法,Type可以是int,string,boolean,float等等。
提示:对于基本类型又个技巧,括号内依次是参数的类型的缩写,括号右边是返回类型的缩写,但是对于静态方法的调用就应该使用GetStaticMethodID和CallStaticVoidMethod了,而对于静态方法就不需要实例化对象,相对来说还少一步。   2,调用带参数和返回值的方法
   Demon如下:
java中的方法:
//有参数的方法

 

public String hello(String name) {
    Log.e("CallJava", "hello I am from java name is " + name);

    return name;
}


   C代码中调用方式:
//调用public 有参数 hello()方法

void callJavaMeathod(JNIEnv *env) {
    jclass clazz = env->FindClass("com/test/jni/CallJava");
    jmethodID method = env->GetMethodID(clazz, "hello", "(Ljava/lang/String;)Ljava/lang/String;");
    jobject object = env->AllocObject(clazz);
    jstring jmsg = env->NewStringUTF("lilei");
    jstring nameStr = static_cast<jstring>(env->CallObjectMethod(object, method, jmsg));
    const char* name = env->GetStringUTFChars(nameStr,0); //转化为c的char*类型,这样才能打印出来
    __android_log_print(ANDROID_LOG_ERROR, "TEST", "call java return:%s",name);
    env -> ReleaseStringUTFChars(nameStr,name); //释放
}

3,下面是访问String的一些方法:
◆GetStringUTFChars将jstring转换成为UTF-8格式的char*
◆GetStringChars将jstring转换成为Unicode格式的char*
◆ReleaseStringUTFChars释放指向UTF-8格式的char*的指针
◆ReleaseStringChars释放指向Unicode格式的char*的指针
◆NewStringUTF创建一个UTF-8格式的String对象
◆NewString创建一个Unicode格式的String对象
◆GetStringUTFLengt获取UTF-8格式的char*的长度
◆GetStringLength获取Unicode格式的char*的长度
4,C调用Java中的其他类型的方法举例:
CallIntMethod举例:
          java方法:

         public int helloInt(){
    Log.e("CallJava", "helloInt ");
    return 3;
}
            


调用方式:
 

void callJavaIntMeathod(JNIEnv *env) {
    jclass clazz = env->FindClass("com/test/jni/CallJava");
    jmethodID method = env->GetMethodID(clazz, "helloInt", "()I");
    jobject object = env->AllocObject(clazz);
    jint jint1 = env->CallIntMethod(object,method);
    __android_log_print(ANDROID_LOG_ERROR, "TEST", "callJavaIntMeathod return:%d",jint1);
}


CallStaticVoidMethod举例:
  java侧方法:

 

public static void hellStatic(){
    Log.e("CallJava", "hellStatic ");
}


调用方式:
 

void callJavaStaticVoidMeathod(JNIEnv *env) {
    jclass clazz = env->FindClass("com/test/jni/CallJava");
    jmethodID method = env->GetStaticMethodID(clazz, "hellStatic", "()V");
    __android_log_print(ANDROID_LOG_ERROR, "TEST", "callJavaStaticVoidMeathod");
      env->CallStaticVoidMethod(clazz,method);

}


其他方法类似,另外还有两种类型的调用方法如下:


CallVoidMethodA举例:
java方法:

 

public void helloVoidA(int a, boolean b, String c) {
    Log.e("CallJava", "helloVoidA: " + a + "  " + b + "   " + c);
}


 调用方式:

void callJavaVoidMethodA(JNIEnv *env) {
    jvalue myJvalues[] ={};
    myJvalues[0].i = 777;
    myJvalues[1].z = true;
    myJvalues[2].l = env -> NewStringUTF("FEfaeteatraret");
    jclass clazz = env->FindClass("com/test/jni/CallJava");
    jmethodID method = env->GetMethodID(clazz, "helloVoidA", "(IZLjava/lang/String;)V");
    jobject object = env->AllocObject(clazz);
    env->CallVoidMethodA(object, method, myJvalues);
}


其中第三个参数需要传入一个Jvalue的联合体数组,里面传入要写入得参数
CallVoidMethodV举例:
Java方法:

 

public void helloVoidV(int a, boolean b, String c) {
    Log.e("CallJava", "helloVoidV: " + a + "  " + b + "   " + c);
}


调用方式:
 

void callJavaVoidMethodV(JNIEnv *env,...) {
    va_list ap;
    va_start(ap,env);
    jclass clazz = env->FindClass("com/test/jni/CallJava");
    jmethodID method = env->GetMethodID(clazz, "helloVoidA", "(IZLjava/lang/String;)V");
    jobject object = env->AllocObject(clazz);
    env->CallVoidMethodV(object,method,ap);
    va_end(ap);
}


由于va_list的可变参数类型,所以我们 应该是写在一个可变参数的方法里面,调用该方法的时候可以就写成这样:
callJavaVoidMethodV(env,676,false, env -> NewStringUTF("AAAAAAA"));

四,C调用java的成员变量

 获取普通成员变量的属性值:
     在java中声明成员变量,

 

public String instanceField = "I am instanceField";


 调用方式如下:
//调用java中的成员变量

 

void callJavaInstanceField(JNIEnv *env){
    jclass clazz = env->FindClass("com/test/jni/CallJava");
    jfieldID  jfieldId = env->GetFieldID(clazz,"instanceField","Ljava/lang/String;");
    jobject object = env->NewObject(clazz, env->GetMethodID(clazz, "<init>", "()V"));
    jobject  jobject1 = env->GetObjectField(object, jfieldId);
    const char *strChar = env->GetStringUTFChars(static_cast<jstring>(jobject1), 0); //转化为c的char*类型,这样才能打印出来
    __android_log_print(ANDROID_LOG_ERROR, "TEST", "callJavaInstanceField:%s",strChar);
}


  1,首先调用GetFieldID获取jfield 第二个参数是成员变量名,第三个参数是签名
  2,然后创建对象NewObject,第二个参数是构造方法的方法ID,获取构造方法ID时方法名统一是<init>
  3,然后调用GetObjedtField就可以获取成员变量的属性值了
 

附文档下载地址:https://download.csdn.net/download/wxj280306451/12416024


版权声明:本文为wxj280306451原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。