Android-JNI的简单使用

一、什么是JNI

1、jni的含义

JNI即为java native interface Java本地接口;JNI是用来调用本地方法的技术,用来使Java和C/C++进行通信。
简单来说,Java运行一个程序需要和不同的系统进行交互,在windows里就要和windows底层平台交互,mac里就要和mac的底层平台交互, JVM就是通过大量的JNI技术能够使Java运行在不同的系统平台当中,与不同的系统平台底层进行交互。

2、Java与JNI类型对照表

基本类型对照


JNI与Java类型对照-基本类型.png

引用类型对照


JNI与Java类型对照-引用类型.png

3、什么是动态库 静态库

Android下的动态库和静态库就是Linux的静态库和动态库;Linux平台静态库以.a结尾,动态库以.so结尾

3.1、静态库

Linux下以.a结尾,静态库会将整个函数库的所有数据都整合到目标代码中,优点在于后期程序执行时不需要外部的函数库支持,移植方便但体量较大。

3.2、动态库

Linux下以.so即为,动态库不会在编译时将整个函数库都整合到目标代码中,而是在运行时执行到哪个函数才会从动态库中加载哪个函数,好处在于体量小,升级方便。

静态库的代码在编译过程中已经被载入可执行程序,因此体积比较大;动态库(共享库)的代码在可执行程序运行时才载
入内存,在编译过程中仅简单的引用,因此代码体积比较小。

3.3、什么是交叉编译

交叉编译就是只在A系统中编译出在B系统中能用的库,例如我们Android开发中,在Windows或者在Mac上能编译出适合Android执行使用的静态库或者动态库。

3.4、AndroidStudio CMakeList文件关于静态库和动态库的配置

详细的CMake文件的配置后面会有一小节去专门的讲,这里只需要看一下CMakeList中是如何区分静态库和动态库的。
在CMakelist中的add_library中进行配置,SHARED为动态库配置选项,STATIC为静态库配置选项


静态库和动态库的编译选项.png

二、静态注册和动态注册

1、静态注册

静态注册的步骤:
(1)编写Java类,例如JniTest.java
(2)通过命令行输入javac JniTest.java 生成JniTest.class文件
(3)在Jni.class目录下 通过javah xxx.JniTest(全类名)生成xxx_JniTest.h头文件
(4)编写xxx_JniTest.c文件,并拷贝xxx_JniTest.h中的函数 实现这些函数,且在.c文件上方引入jni.h头文件
(5)编写CMakeList.tet文件 ,编译生成对应的静态库/动态库

上面的步骤很繁琐,但其实使用熟练后,可以不用通过命令行生成.h文件;只需要自己创建.c文件并引入jni.h头文件后,其中的C函数严格对应Java中的方法(全类名加方法名,例如:com_test_JniTest_test),函数当中的方法和返回值也要严格对应上就可以了。
例如:
Java代码:

package com.hzf.test;

public class JniTest {

    static {
        System.loadLibrary("helloworldLib");
    }
    public static native String test();
}

对应的C代码:


#include 

JNIEXPORT jstring JNICALL Java_com_hzf_test_JniTest_test(JNIEnv *env,jclass jclass){
    return (*env)->NewStringUTF(env,"this is from HelloWorld.c");
}

cmakelist.txt中添加

add_library(helloworldLib SHARED HelloWorld.c)

其中如果Java方法是static的,jni函数中要传入jclass(因为static函数是类的);如果不是static的,jni函数要传入jobject

2、动态注册

动态注册顾名思义,就是动态的将C函数去匹配Java函数的过程,不需要像静态注册一样,完全对应上,方法名称不需要那么长。
例如:

java:
public static native void jniTestFunc1();
public static native String jniTestFunc2();

对应的C函数

#include 

void func1(JNIEnv *env,jclass jclass1){
}

jstring func2(JNIEnv *env,jclass jclass2){
    return (*env)->NewStringUTF(env,"this is from dynamicTest.c");
}

static const char * mClazzname = "com/hzf/test/DynamicJniTest";

static const JNINativeMethod methods[] = {
        {"jniTestFunc1","()V",(void*)func1},
        {"jniTestFunc2","()Ljava/lang/String;",(void*)func2},
};
//通过JNI_OnLoad函数动态的匹配到mClassname中的methods函数
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *reserved){
    JNIEnv* env = NULL;
    //获得env
    int r = (*vm)->GetEnv(vm,(void**) &env,JNI_VERSION_1_4);
    if (r != JNI_OK){
        return -1;
    }
    jclass dynamicClazz = (*env)->FindClass(env,mClazzname);
    r = (*env)->RegisterNatives(env,dynamicClazz,methods,2);
    if (r != JNI_OK){
        return -1;
    }
    return JNI_VERSION_1_4;
}

3、Jni_onLoad

当执行到System.loadLibary时,会自动执行JNI_OnLoad函数,主要作用有两个:
(1)告诉VM C组件使用哪一个JNI版本,如果没有指定,会默认指定最老的JNI1.1的版本
(2)当执行到system.loadLibary时会自动调用JNI_OnLoad方法,因此可以做一些初始化的相关工作

4、Sytem.load和System.loadLibaray的区别

System.load,指定的库文件路径是绝对路径,例如System.load("C:\test\test.so")
System.loadLibary指定是库文件的名字不包括扩展名,例如System.loadLibary("Test.so");
Android中常用的是System.loadLibary

三、JNIEnv类型jobject类型的解释

JNIEXPORT jstring JNICALL Java_com_hzf_test_JniTest_test(JNIEnv *env,jclass jclass){
    return (*env)->NewStringUTF(env,"this is from HelloWorld.c");
}

JNIEXPORT与JNICALL是JNI的关键字,表示此函数为JNI所调用
JNIEnv表示Java环境,通过*env指针就可以对Java函数进行操作,创建Java类中的对象,调用对象中的方法等。JNIEnv的指针会被JNI传入到本地方法的实现函数中对Java代码进行操作。
JNI函数的方法参数中都有固定的jclass或者jobject,那么这两个代表什么含义呢?
(1)如果Java的本地方法不是static的,JNI对应的函数就需要写jobject,代表native的实例
(2)如果Java的本地方法是static的,JNI对应的函数就需要写jclass,代表native方法的类的class实例

四、C/C++代码调用Java代码

1、初步使用

JNI还有一个非常重要的作用,也是在开发中经常用到的 就是通过C或者C++去调用Java本地的代码,获取Java的属性和函数等;jni.h中定义了jfiledId以及jmethodid分别代表Java端的属性和方法。
同样的根据属性和方法是否是static的 也对应了需要使用不同的filedid以及methodID
GetFiledId/GetMethodId
GetStaticFiledId/GetStaticMethodId

//GetFiledId的定义
jfieldID    (*GetFieldID)(JNIEnv*, jclass, const char*, const char*);

一共4个参数
第一个参数为 JNIEnv指针
第二个参数为 clazz对象
第三个参数为 属性名称
第四个参数为 该字段的签名
例如:

java函数:
static {
        System.loadLibrary("ccalljavalib");
    }
    public int property = 100;

    public native void accessProperty();

C函数:
#include 
#include 
#include 

#ifndef LOG_TAG
#define LOG_TAG "hzfTag"
#define logUtils(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#endif

//JNI获取Java层非静态的变量
JNIEXPORT void JNICALL Java_com_hzf_test_CCallJavaTest_accessProperty(JNIEnv *env,jobject object){
    jclass clazz = (*env)->GetObjectClass(env,object);
    jfieldID  jfieldId = (*env)->GetFieldID(env,clazz,"property","I");
    jint var = (*env)->GetIntField(env,object,jfieldId);
    logUtils("vat = %d",var);
    //设置Java层当中的值
    (*env)->SetIntField(env,object,jfieldId,var + 1000);
};

2、签名问题

1中提到了获取字段时,第四个参数为字段的签名,JNI与Java基本类型的签名对照如下:


Java引用类型签名对照表.png

其他类型:
void对应的签名为V
object对应的签名以L开头并以/分割包的形式 最后再加上分号; 比如String的签名为Ljava/lang/String;
Array对应的签名以[开头加数组类型对应的签名,例如int[]的签名为[I,int[][]的签名为[[I,Object[]的签名为[Ljava/lang/Object;
例如一个Java方法
public int function(int fu,Date date,int[] array){}
对应的JNI中GetMethodID(env,clazz,"function",(ILjava/util/Date;[I)I)
首先方法的参数在括号中,括号外是方法的返回值,因此先写成()I
其次的对应方式为 int fu对应的为I
Date date 对应的为 Ljava/util/Date;
int[] array 对应的为 [I

3、JNI访问字符串

由于JNI C/C++ Java三者的String编码方式各不相同,因此JNI访问Java的字符串时不能直接用jstring进行处理。
Java内部是utf-16
Jni是utf-8 Unicode编码方式,英文是1个字节,中文是3个字节
C/C++ 使用ASCII码 中文是GB2312的编码方式,中文2个字节
Java代码

public static native String sayHello(String str);

JNI代码

JNIEXPORT jstring JNICALL
Java_com_hzf_test_JniTest_sayHello(JNIEnv *env, jclass jclass, jstring str) {
    const char *c_str = NULL;
    char buf[128] = {0};
    jboolean isCopy;
    c_str = (*env)->GetStringUTFChars(env, str, isCopy);
    //需要做非空判断 防止崩溃
    if (c_str == NULL) {
        return NULL;
    }
    sprintf(buf, "Hello 收到你的信息了 : %s", c_str);
    //GetStringUTF会让JVM开辟一段新内存,因此资源要及时释放
    (*env)->ReleaseStringChars(env, str, c_str);
    return (*env)->NewStringUTF(env, buf);
}

4、JNI访问Java的函数

4.1、JNI访问Java的构造函数

Java代码

//无参构造函数
public CCallJavaTest() {
    }
//JNI调用 生成CCallJavaTest对象
public static native CCallJavaTest getInstance();

JNI代码

JNIEXPORT jobject JNICALL Java_com_hzf_test_CCallJavaTest_getInstance(JNIEnv *env, jclass clazz) {
    jclass jclazz = (*env)->FindClass(env, "com/hzf/test/CCallJavaTest");
    //获取类的无参构造函数;注意这里构造函数统一写成 ""
    jmethodID jmethodId = (*env)->GetMethodID(env, jclazz, "", "()V");
    //创建一个新对象
    jobject instance = (*env)->NewObject(env, jclazz, jmethodId);
    return instance;
};

调用代码

Log.d("hzfTag", "instance : " + CCallJavaTest.getInstance());
输出:
2020-04-13 19:30:19.919 12386-12386/? D/hzfTag: instance : com.hzf.test.CCallJavaTest@b50fcc2

4.2、JNI访问Java的静态函数

java代码

public static void staticTest(String str, int i) {
        Log.d("hzfTag", "str : " + str + ",i : " + i);
    }

    /**
     * 调用static的代码
     */
  public static native void cCallJavaStaticMethod();

JNI函数

JNIEXPORT void JNICALL
Java_com_hzf_test_CCallJavaTest_cCallJavaStaticMethod(JNIEnv *env, jclass clazz) {
    jclass jclazz = (*env)->FindClass(env, "com/hzf/test/CCallJavaTest");
    if (jclazz == NULL) {
        return;
    }
    jmethodID jmethodId = (*env)->GetStaticMethodID(env, jclazz, "staticTest",
                                                    "(Ljava/lang/String;I)V");
    if (jmethodId == NULL) {
        return;
    }
    jstring str = (*env)->NewStringUTF(env, "静态类");
    (*env)->CallStaticVoidMethod(env, jclazz, jmethodId, str, 1000);
    (*env)->DeleteLocalRef(env, jclazz);
    (*env)->DeleteLocalRef(env, str);
}

4.3、JNI访问Java的非静态函数

非静态函数与上面的静态函数类似,只是获取Method时会用的时GetMethodID

5、引用类型

1、局部引用
通过NewLocalRef和各种JNI接口创建(FindClass、NewObject、GetObjectClass和NewCharArray等)。不能跨函数 跨线程使用。函数返回或者结束后VM会自动回收。
释放一个局部引用有两种方式
1、本地方法执行结束后VM会自动释放
2、通过DeleteLocalRef手动释放
之所以手动释放是因为局部引用会影响它所创建的对象被GC回收
2、全局引用
调用NewGlobalRef创建,可以跨方法,跨线程使用。JVM不会自动释放,必须通过DeleteGlobalRef释放
3、弱全局引用
调用NewWeakGlobalRef基于局部引用或者全局引用创建,不会阻止GC回收所引用的对象,可以跨方法,跨线程。引用不会自动释放,当JVM认为回收的时候(比如内存紧张时),进而回收而被释放,或调用DeleteWeakGlobalRef手动释放

你可能感兴趣的:(Android-JNI的简单使用)