C/C++语言基础 –> JNI编程 –> NDK编程 –> 音视频处理 –> 图像处理等等 ,从零到一 , 零基础学习NDK开发 。学习本专题 , 你需要具备一定的Android开发知识 C/C++基础, 知道如何创建一个Android工程 , 有一定的开发经验更佳 。

JNI可以做什么

JNI是Java平台的一个非常有用的特性,它让同时使用JAVA和C/C++协作开发应用程序成为可能。

JNI (Java Native Interface ,Java本地接口) 是一种编程框架,可以使Java虚拟机或Java程序调用本地应用或库。

你的第一个JNI程序

新建一个支持C++的项目

添加如下代码:

static {
        System.loadLibrary("native-lib");
    }
 //传递基本类型
public native void testArray(String[] strs, int[] ints);

然后在native-lib.cpp中编写代码

对于不熟悉JNI的人可以使用快捷键自动生成C++的代码模版

ALT + ENTER/OPTION + ENTER(mac)

或者通过命令生成

javah -o [输出文件名] [全限定名]

会给你生成如下代码模版:

extern "C"
JNIEXPORT void JNICALL
Java_com_jnimode1_MainActivity_testArray(JNIEnv *env, jobject instance, jobjectArray strs,
                                         jintArray ints_) {
                                         }

这个相当easy了,多敲几遍就搞定了。下面来理解这些关键字的含义。

先看这一段代码:

Java_com_jnimode1_MainActivity_testArray

其实就是方法的全类名:

com.jnimode1.ManiActivity.testArray  

最开始加了一个Java 表示是Java中的方法。

然后再看一下两个宏(JNIEXPORT JNICALL)是什么意思,这只是固定的写法,简单了解一下即可。

JNIEXPORT
Windows 中,定义为__declspec(dllexport)。因为Windows编译 dll 动态库规定,如果动态库中的函数要被外部调用,需要在函数声明中添加此标识,表示将该函数导出在外部可以调用。

​ 在 Linux/Unix/Mac os/Android 这种 Like Unix系统中,定义为__attribute__ ((visibility ("default")))

​ GCC 有个visibility属性, 启用这个属性gcc -fvisibility=xx:

当-fvisibility=hidden时

动态库中的函数默认是被隐藏的即 hidden. 除非显示声明为__attribute__((visibility("default"))).

当-fvisibility=default时

动态库中的函数默认是可见的.除非显示声明为__attribute__((visibility("hidden"))).

JNICALL:

​ 在类Unix中无定义,在Windows中定义为:_stdcall ,一种函数调用约定

类Unix系统中这两个宏可以省略不加。

JNIEnv:由Jvm传入与线程相关的变量。定义了JNI系统操作、Java交互等方法。

jobject:表示当前调用对象,即this,如果是静态的native方法,则获得jclass

至于 extern “C” 代码使用C++写的,要以C的方式编译.(PS:这是C语言的基础了)

我们先写一个简单的例子,C++ 获取Java传递的int 和 string,编码如下

JNIEXPORT jstring JNICALL Java_com_jni_ExampleUnitTest_test(JNIEnv *env,jobject o,jint i,jstring s){
    //获得c字符串
    //开闭内存x,拷贝Java字符串到x中 返回指向x的指针
    //提供一个boolean(int) 指针,用于接收jvm传给我们到字符串是否是拷贝
    const char *k = env->GetStringUTFChars(s, JNI_FALSE);//获得 C 字符串
    printf("C 获得 Java传递过来的值:%d,%s",i,k);
    char returnStr[100];
    //格式化字符串
    sprintf(returnStr, "C++ string:%d,%s",i,k);
    //释放掉内存x
    env->ReleaseStringUTFChars(s, k);
    //返回Java字符串
    return env->NewStringUTF(returnStr);
}

需要注意的地方:jstring
const char *k = env->GetStringUTFChars(s, JNI_FALSE); 需要将Java中的String转换为C的字符串,才能在C中打印以及使用。
因为转换的时候开辟了内存,不用就需要释放掉
env->ReleaseStringUTFChars(s, k);

至于jint可以直接使用。

在Java代码中,首先声明这个方法:

native String test(int i, String value);

然后直接调用即可:

 String java = test(1, "java");

Run,可以看到C++中的日志输出:
image.png

但是这个日志是在android中是看不到的,那么能否在Android中看到这些log呢?

需要在C++代码中定义一个宏(宏其实就是一种文本替换)
同时要导入一个头文件

#include <android/log.h>

//log 宏定义
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "JNI_TEST", __VA_ARGS__);

这样使用即可

LOGE("获取String:%s", getStr)

C/C++ 获取Java中的数组

//注意如果用C++写到,需要在每个方法中加 extern "C"
extern "C"
//C/C++ 获取Java中的数组
JNIEXPORT jstring JNICALL Java_com_jni_ExampleUnitTest_array(JNIEnv *env,jobject instance,jobjectArray a_,jintArray b_){
    //1 获得字符串数组
    //获得数组的长度
    int32_t str_length = env->GetArrayLength(a_);
    printf("字符串数组长度:%d",str_length);
    //获得字符串数组的数据
    for (int i = 0; i < str_length; i++) {
        //获取jstring
        jstring str = static_cast<jstring>(env->GetObjectArrayElement(a_, i));
        //将jstring 转换为c字符串,获得c字符串
        const char *c_str = env->GetStringUTFChars(str, JNI_FALSE);
        printf("字符串有:%s",c_str);
        //使用完释放
        env->ReleaseStringUTFChars(str, c_str);
    }
    //2 获取基本数据类型数组
    //获取数组长度
    int32_t int_length = env->GetArrayLength(b_);
    printf("int 数组的长度:%d",int_length);
    //获取jint
    //true 进行拷贝一个新的数据(相当于新申请了一块内存空间);false 就是使用的Java的数组(地址)
    jint *b =  env->GetIntArrayElements(b_, JNI_FALSE);
    for (int i = 0; i < int_length; i++) {
        printf("int 数组有:%d",b[i]);
    }
    //使用完后释放
    env->ReleaseIntArrayElements(b_, b, 0);
    //返回Java字符串
    return env->NewStringUTF("returnStr");
}

其实代码非常简单,需要注意几点:

  1. 遍历字符串数组的时候,由于JNI没有提供jstringArray,只有jobjectArray需要先将,转换为jstring,然后将jstirng –> char ,使用完之后要注意释放,int数组就相当简单了,将Java数组转换为jint 指针。
  2. 在释放env->ReleaseIntArrayElements(b_, b, 0);

    最后一个参数的意思:(大家可以改变试一下)

    //0 刷新Java数组并释放 c/c++数组 如果在C++中改变数组的值 Java中数组的值也会改变;

    //JNI_COMMIT 1 只刷新Java数组 如果在C++中改变数组的值 Java中数组的值也会改变

//JNI_ABORT 2 只释放并没有刷新Java数组 如果在C++中改变数组的值 Java中数组的值不会改变

C/C++反射Java

反射Java类的方法

C++调用Java类中的方法

Java 类

public class Helper {
    private static final String TAG = "Helper";

    int a = 10;

    String s = "test";

    public Helper() {
        Log.e(TAG, "Helper: 构造方法被调用");
    }

    //C/C++ 反射创建的Java对象,调用Java方法
    public void instanceModth(String a, int c, boolean b) {
        Log.e(TAG, "instanceModth: a= " + a + " c= " + c + " b= " + b);
    }

    public static void staticModth(String a, int c, boolean b) {
        Log.e(TAG, "instanceModth: a= " + a + " c= " + c + " b= " + b);
    }
}
extern "C"
JNIEXPORT void JNICALL
Java_com_jnimode1_MainActivity_invokeHelper(JNIEnv *env, jobject instance, jobject helper) {
    jclass helperClass;
    //判断传递的对象是否为空
    if (helper != NULL) {
        //获得Helper类
        LOGE("helper对象不为空")
        helperClass = env->GetObjectClass(helper);
    } else {
        LOGE("helper对象为空")
        helperClass = env->FindClass("com/jnimode1/Helper");
    }

    //构造方法 基本数据类型的签名采用一系列大写字母来表示
    jmethodID constructMethod = env->GetMethodID(helperClass, "<init>", "()V");
    if (helper == NULL) {
        //创建helper对象并调用构造方法
        helper = env->NewObject(helperClass, constructMethod);
    }
    //调用普通方法
    jmethodID instanceMethod = env->GetMethodID(helperClass, "instanceModth",
                                                "(Ljava/lang/String;IZ)V");
    LOGE("准备调用方法instanceModth")
    //将字符串转换为Java的调用方式
    jstring string = env->NewStringUTF("C++调用");
    LOGE("开始调用instanceModth")
    env->CallVoidMethod(helper, instanceMethod, string, 20, 0);
    //调用静态方法
    jmethodID staticMethod = env->GetStaticMethodID(helperClass, "staticModth",
                                                    "(Ljava/lang/String;IZ)V");
    jstring staticString = env->NewStringUTF("C++调用静态方法");
    env->CallStaticVoidMethod(helperClass, staticMethod, staticString, 30, 1);
    env->DeleteLocalRef(helperClass);
    env->DeleteLocalRef(staticString);

    //主要需要释放对象及引用
    env->DeleteLocalRef(helper);
    env->DeleteLocalRef(string);
}

上述代码,可能稍微优点复杂,其实就是对API的调用。

这一段代码大家可能不理解是什么意思(Ljava/lang/String;IZ)V
,其实相当于一个签名,翻译成Java就是void (String,int,boolean).

Run,结果如下:

image.png

确实调用了Java类中的方法。

Java类型 签名
boolean Z
short S
float F
byte B
int I
double D
char C
long J
void V
引用类型 L + 全限定名 + ;
数组 [+类型签名

可以使用javap来获取反射方法时的签名

#cd 进入 class所在的目录 执行: javap -s 全限定名,查看输出的 descriptor

反射Java类的属性

道理是一样的,熟悉相关的API,代码中都有注释。

C++ 来修改Java类属性的值,编码如下:

//反射Java中类的属性
extern "C"
JNIEXPORT void JNICALL
Java_com_jnimode1_MainActivity_fiedHelper(JNIEnv *env, jobject instance, jobject helper) {
    jclass helperClass;
    //判断传递的对象是否为空
    if (helper != NULL) {
        //获得Helper类
        LOGE("helper对象不为空")
        helperClass = env->GetObjectClass(helper);
    } else {
        LOGE("helper对象为空")
        helperClass = env->FindClass("com/jnimode1/Helper");
    }

    jfieldID aId = env->GetFieldID(helperClass, "a", "I");

    //获得属性值
    jint a = env->GetIntField(helper, aId);
    LOGE("a:%d", a);

    //修改属性值
    env->SetIntField(helper, aId, 100);

    //获得属性值
    jint a1 = env->GetIntField(helper, aId);
    LOGE("a1:%d", a1);

    jfieldID sId = env->GetFieldID(helperClass, "s", "Ljava/lang/String;");

    //获取字符串的值
    jstring s = static_cast<jstring>(env->GetObjectField(helper, sId));
    const char *as = env->GetStringUTFChars(s, 0);
    LOGE("获取s的值%s", as);
    //不用之后释放
    env->ReleaseStringUTFChars(s, as);

    //修改字符串的值
    jstring new_str = env->NewStringUTF("ssss");
    env->SetObjectField(helper, sId, new_str);
    //注意释放引用
    env->DeleteLocalRef(new_str);

    //获取字符串的值
    jstring s1 = static_cast<jstring>(env->GetObjectField(helper, sId));
    const char *as1 = env->GetStringUTFChars(s1, 0);
    LOGE("修改后s的值%s", as1);
    //不用之后释放
    env->ReleaseStringUTFChars(s1, as1);


    env->DeleteLocalRef(helperClass);
}