前言
之前写了一篇文章简单的介绍了 Android NDK 的组件和结构,以及在 Android studio 中开发 NDK,NDK 是 Android 底层的 c /c++ 库,然而要在 java 中调用 c /c++ 的原生功能,则需要使用 JNI 来实现。
什么是 JNI
JNI(Java Native Interface)是 java 本地接口,它主要是为了实现 Java 调用 c、c++ 等本地代码所封装的一层接口。大家都知道 java 是跨平台开发语言,它的狂平台特性导致与本地交互的能力不够强大,一些和操作系统相关的特性 Java 无法完成,所以 Java 提供了 JNI 用于和 Native 代码进行交互。通过 JNI,Java 可以调用 c、c++,相反,c、c++ 也可以调用 Java 的相关代码。
创建 NDK 工程
开发环境
Mac
Android studio:3.3.2
新建工程
本地的 Android studio 版本为 3.3.2,当你创建项目的时候有一个选项是选择 Native C++ 的模板
点击 next,配置项目的信息
点击 next,选择使用哪种 C ++ 标准,选择 Toolchain Default 会使用默认的 CMake 设置即可。
点击 finish 即可完成工程的创建。
工程结构
这时候主工程目录下会有 cpp 文件夹和.externalNativeBuild 文件夹。
.externalNativeBuild 文件夹:用于存放 cmake 编译好的文件,包括支持的各种硬件等信息,有点类似于 build.gradle 文件明确 Gradle 如何编译 APP;cpp 文件夹:存放 C /C++ 代码文件,native-lib.cpp 文件默认生成的;
cpp 文件夹下有两个文件,一个是 native-lib.cpp 文件,一个是 CMakeLists.txt 文件。CMakeLists.txt 文件是 cmake 脚本配置文件,cmake 会根据该脚本文件中的指令去编译相关的 C /C++ 源文件,并将编译后产物生成共享库或静态块,然后 Gradle 将其打包到 APK 中。
CMakeLists.txt 的相关配置如下:
# 设置构建本地库所需的最小版本的 cbuild。
cmake_minimum_required(VERSION 3.4.1)
# 创建并命名一个库,将其设置为静态
# 或者共享,并提供其源代码的相对路径。
# 您可以定义多个库,而 cbuild 为您构建它们。
# Gradle 自动将共享库与你的 APK 打包。
add_library(native-lib #设置库的名称。即 SO 文件的名称,生产的 so 文件为“libnative-lib.so”, 在加载的时候“System.loadLibrary(“native-lib”);”
SHARED # 将库设置为共享库。
native-lib.cpp # 提供一个源文件的相对路径
helloJni.cpp # 提供同一个 SO 文件中的另一个源文件的相对路径
)
# 搜索指定的预构建库,并将该路径存储为一个变量。因为 cbuild 默认包含了搜索路径中的系统库,所以您只需要指定您想要添加的公共 NDK 库的名称。cbuild 在完成构建之前验证这个库是否存在。
find_library(log-lib # 设置 path 变量的名称。
log # 指定 NDK 库的名称 你想让 CMake 来定位。
)
#指定库的库应该链接到你的目标库。您可以链接多个库,比如在这个构建脚本中定义的库、预构建的第三方库或系统库。
target_link_libraries(native-lib # 指定目标库中。与 add_library 的库名称一定要相同
${log-lib} # 将目标库链接到日志库包含在 NDK。
)
#如果需要生产多个 SO 文件的话,写法如下
add_library(natave-lib # 设置库的名称。另一个 so 文件的名称
SHARED # 将库设置为共享库。
nataveJni.cpp # 提供一个源文件的相对路径
)
target_link_libraries(natave-lib #指定目标库中。与 add_library 的库名称一定要相同
${log-lib} # 将目标库链接到日志库包含在 NDK。
)
build.gradle 中有 CMake 的相关配置
代码结构
java 调用 c、c++ 代码分为三个步骤:
加载 so 库
编写 java 函数
编写 c 函数
在 MainActivity.java,static{}语句中使用了加载 so 库,此语句在类加载中只执行一次。
static {
System.loadLibrary(“native-lib”);
}
然后,编写了原生的函数,函数名中要带有 native。
public native String stringFromJNI();
最后,编写相对应的 c 函数,注意函数名的构成 Java_com_example_myapplication_MainActivity_stringFromJNI 为 Java_加上包名、类型、方法名的下划线连成一起。
native-lib.cpp 文件
#include <jni.h>
#include <string>
extern “C” JNIEXPORT jstring JNICALL
Java_com_example_myapplication_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = “Hello from C++”;
return env->NewStringUTF(hello.c_str());
}
这就是一个 JNI 方法调用示例。
虽然 Java 函数不带参数,但是原生方法却带了两个参数,第一个参数 JNIEnv 是指向可用 JNI 函数表的接口指针,第二个参数 jobject 是 Java 函数所在类的实例的 Java 对象引用。
JNIEnv 接口指针
原生代码 (c) 通过 JNIEnv 接口指针提供的各种函数来使用虚拟机的功能,JNIEnv 是一个指向线程 - 局部数据的指针,线程 - 局部数据中包含指向函数表的指针。
原生代码是 c 与原生代码是 c ++ 的调用 JNI 函数的语法不同,在 c 代码中,JNIEnv 是指向 JNINativeInterface 结构的指针,而在 c ++ 代码中,JNIEnv 是 c ++ 类实例,这两种方式调用函数的方式是不一样的。例如:
c 代码中:
(*env)->NewStringUTF(env,”Hello from JNI”);
c++ 代码中:
env->NewStringUTF(“Hello from JNI”);
实例方法与静态方法
Java 程序设计有两类方法,实例方法和静态方法。实例方法与类实例相关,只能在类实例中调用。静态方法不与类死里相关,它们可以在静态上下文中直接调用。在原生代码中可以获取 Java 类的实例引用和类引用。例如:
类实例引用
extern “C” JNIEXPORT void JNICALL
Java_com_example_myapplication_MainActivity_stringFromJNI(
JNIEnv *env,
jobject thiz) {
}
类引用
extern “C” JNIEXPORT void JNICALL
Java_com_example_myapplication_MainActivity_stringFromJNI(
JNIEnv *env,
jclass clazz) {
}
从函数中看出来,JNI 提供了自己的数据类型从而让原生代码了解 Java 数据类型。
JNI 数据类型
JNI 的数据类型包含两种:基本类型和引用类型。与 Java 数据类型的对应关系如下:
基本数据类型:
| JNI 类型 | Java 类型 || —— | —— | | jboolean | boolean || jbyte | byte | | jchar | char || jshort | short || jint | int || jlong | long || jfloat | float || jdouble | double || void | void |
引用类型:
| JNI 类型 | Java 类型 || —— | —— | | jobject | Object || jclass | Class | | jstring | String || jobjectArray | Object[] || jbooleanArray | boolean[] || jbyteArray | char[] || jshortArray | short[] || jintArray | int[] || jlongArray | long[] | | jfloatArray | float[] | | jdoubleArray | double[] | | jthrowable | Throwable |
引用数据类型的操作
JNI 提供了与引用类型密切相关的一组 API,这些 API 通过 JNIEnv 接口指针提供给原生函数。例如:
字符串
数组
NIO 缓冲区
字段
方法
字符串操作
JNI 把 Java 字符串当成引用类型处理,提供了 Java 与 c 字符串之间相互转换的必要函数,由于 Java 字符串对象是不可变得,所以 JNI 不提供修改现有 Java 字符串内容的函数。
创建字符串
可以在原生代码中使用 NewString 函数构建 Unicode 编码格式的字符串实例,也可以中 NewStringUTF 函数构建 UTF- 8 编码格式的字符串实例,这些函数以 C 字符串为参数,并返回一个 Java 字符串引用类型 jstring 值。例如:
jstring javaStr = (*env)->NewStringUTF(env,”Hello”);
把 Java 字符串转换成 C 字符串
为了在原生代码中使用 Java 字符串,需要将 Java 字符串转换成 C 字符串。用 GetStringChars 函数可以将 Unicode 格式的 Java 字符串转换成 C 字符串,用 GetStringUTFChars 函数可以将 UTF- 8 格式的 Java 字符串转换成 C 字符串。例如:
const jbyte* str
jboolean isCopy;
str = (*env)->GetStringUTFChars(env,javaString,&isCopy);
释放字符串
通过 JNI GetStringChars 函数和 GetStringUTFChars 函数获得的 C 字符串在原生代码中使用完后要释放,否则会引起内存泄漏。JNI 提供了 ReleaseStringChars 函数和 ReleaseStringUTFChars 函数来释放 Unicode 编码和 UTF- 8 编码格式的字符串。例如:
(*env)->ReleaseStringUTFChars(env,javaString,str);
数组操作
创建数组
用 New”Type”Array 函数在原生代码中创建数组实例,其中 ”Type” 可以是 Int、Char 等类型,例如:
jintArray javaArray = (*env)->NewIntArray(env,10);
访问数组元素
将数组的代码复制成 C 数组或者让 JNI 提供直接指向数组元素的指针方式来访问 Java 数组元素。
对副本的操作
Get”Type”ArrayRegion 函数将给定的基本 Java 数组复制到给定的 C 数组中,例如:
jint nativeArray[10];
(*env)->GetIntArrayRegion(env,javaArray,0,10,nativeArray);
原生代码可以使用和修改数组元素,使用 Set”Type”ArrayRegion 函数将 C 数组复制回 Java 数组中。例如:
(*env)->SetIntArrayRegion(env,javaArray,0,10,nativeArray);
NIO 操作
JNI 提供了在原生代码中使用 NIO 的函数,与数组操作相比,NIO 性能较好,更适合在原生代码和 Java 应用程序之间传送大量数据。
创建直接字节缓冲区
unsigned char* buffer = (unsigned char*) malloc(1024);
jobject directBuffer = (*env)->NewDirectByteBuffer(env,buffer,1024);
注意:原生函数应用通过释放未使用的内存分配以避免内存泄漏。
获取直接字节缓冲区
unsigned char* buffer;
buffer = (unsigned char*)(*env)->GetDirectBufferAddress(env,directBuffer);
访问域
Java 有两类域:实例域和静态域,这两个的区别就是有没有 static 声明静态。
获取域 ID
JNI 提供了用域 ID 访问两类域的方法,可以通过给定实例的 class 对象获取域 ID,用 GetObjectClass 函数来获取 class 对象。例如:
jclass clazz = (*env)->GetObjectClass(env,instance);
用 GetFieldId 函数来获取实例域。
jfieldId instanceFieldId = (*env)->GetFieldId(env,clazz,”instanceField”,”Ljava/lang/String”);
用 GetStaticFieldId 获取静态域 ID。
jfieldID staticFieldId = (*env)->GetStaticFieldID(env,clazz,”staticField”,”Ljava/lang/String”);
其中最后一个参数是 Java 中表示域类型的域描述符,”Ljava/lang/String” 表明域类型是 String。
获取域
获得域 ID 之后可以用 Get”Type”Field 函数获取实际的实例域。例如:
jstring instanceField = (*env)->GetObjectField(env,instance,instanceFieldId);
用 GetStatic”Type”Field 函数获得静态域。例如:
jstring staticField = (*env)->GetStaticObjectField(env,clazz,staticFieldId);
调用方法
与域类似,Java 中有两类方法:实例方法和静态方法。
获取方法 ID
JNI 提供了用方法 ID 访问两类方法的途径,可以用给定实例的 class 对象获取方法 ID,用 GetMethodID 函数获得实例方法的方法 ID。例如:
jmethodID instanceMethodId = (*env)->GetMethodID(env,clazz,”instanceMethod”,”()Ljava/lang/String;”);
用 GetStaticMethodID 函数获得静态域的方法 ID,例如:
jmethodID staticMethodId=(*env)->GetStaticMethodID(env,clazz,”staticMethod”,”()Ljava/lang/String;”);
调用方法
以方法 ID 为参数通过 Call”Type”Method 类函数调用实际的实例方法。例如:
jstring instanceMethodResult = (*env)->CallStringMetthod(env,instance,instanceMethodId);
用 CallStatic”Type”Field 类函数调用静态方法,例如:
jstring staticMethodResult = (*env)->CallStaticStringMethod(env,clazz,staticMethodId);
域和方法描述符
在上面获取域 ID 和方法 ID 均分别需要域描述符和方法描述符,域描述符和方法描述符可以通过下表 Java 类型签名映射获取:
| Java 类型 | 签名 || —— | —— | | Boolean | Z || Byte | B | | Char | C || Short | S || Long | J || Int | I || Float | F || Double | D || void | V || fully-qualified-class | Lfully-qualified-class | | type[] | [type | | method type | (arg-type)ret-type |
类的签名采用 ”L+ 包名 + 类名 +;” 的形式,将其中的. 替换为 / 即可,比如 java.lang.String,它的签名为 Ljava/lang/String; 数组的签名就是[+ 类型签名,比如 int 数组,签名就是[I,多维数组就是[[I。方法的签名为(参数类型签名)+ 返回值类型签名,例如:boolean fun1(int a,double b,int[] c),其中参数类型的签名为 ID[I,返回值类型的签名为 Z,所以这个方法的签名就是(ID[I)Z。