【Android】JNI与NDK编程
JNI简介
JNI(Java Native Interface),允许Java与本地语言C/C++进行交互。使用JNI的场景主要有:(1)对于某个需求,Java没有库而其他语言有(2)Java的性能不满足于需求,此时需要使用更接近于硬件层面的速度的语言,例如汇编、C/C++(3)为了与一些硬件或操作系统进行交互
JNI的缺点:(1)有可能使得程序丧失可移植性(2)由于程序会与硬件或操作系统进行交互,拥有读写内存等权限,有可能存在一些安全问题

NDK简介
NDK是Android提供的原生开发工具包,用于在Android项目中集成和编译C/C++代码,提高代码性能或复用底层库。
ndk-build是NDK提供的基于Make的构建系统,提供编写Android.mk和Application.mk来编译native代码。
CMake是一种跨平台构建系统,Android Studio编译器推荐使用CMake配置native编译流程,替代传统的ndk-build
JNI的开发流程
- 在Java中声明native方法以及加载so库的静态代码块
- 实现JNI方法
- 在mk文件中声明LOCAL_JNI_SHARED_LIBRARIES,将so库加入项目中
- 在Java文件中可以像调用java函数一样调用JNI方法
JNI的数据类型
JNI的基本数据类型
| JNI类型 | Java类型 | C/C++类型 | 大小(字节) |
|---|---|---|---|
| jboolean | boolean | unsigned char | 1 |
| jbyte | byte | sigend char | 1 |
| jchar | char | unsigned short | 2 |
| jint | int | int | 4 |
| jlong | long | long long | 8 |
| jfloat | float | float | 4 |
| jdouble | double | double | 8 |
| void | void | void | - |
JNI的引用数据类型
| JNI类型 | Java类型 | 说明 |
|---|---|---|
| jobject | Object | 任意对象 |
| jstring | String | 字符串 |
| jclass | Class | 类 |
| jthrowable | Throwable | 异常对象 |
| jobjectArray | Object[] | 任意类型数组 |
| jbooleanArray | boolean[] | 布尔类型数组 |
| jbyteArray | byte[] | 字节类型数组 |
| jcharArray | char[] | 字符类型数组 |
| jshortArray | short[] | 短整型数组 |
| jintArray | int[] | 整型数组 |
| jlongArray | long[] | 长整型数组 |
| jfloatArray | float[] | 单精度浮点数数组 |
| jdoubleArray | double[] | 双精度浮点数数组 |
JNI的基本数据类型签名(Type Signatures)
| Java 类型 | JNI 签名 |
|---|---|
| boolean | Z |
| byte | B |
| char | C |
| short | S |
| int | I |
| long | J |
| float | F |
| double | D |
| void | V |
// Java 方法
public native int add(int a, int b);
// 对应的 JNI 签名
(II)I
JNI的引用数据类型签名
Java 的引用类型(类、数组、字符串等)在 JNI 中使用 “L + 全限定类名 + ;” 表示:
| Java 类型 | JNI 签名 |
|---|---|
| String | Ljava/lang/String; |
| Object | Ljava/lang/Object; |
| int[] | [I |
| String[] | [Ljava/lang/String; |
| Class | Ljava/lang/Class; |
// Java 方法
public native String concat(String s1, String s2);
// 对应的 JNI 签名
(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
数组类型的签名
数组类型使用 [ 开头,后跟元素类型的签名:
| Java 类型 | JNI 签名 |
|---|---|
| int[] | [I |
| double[] | [D |
| String[] | [Ljava/lang/String; |
| int[][] | [[I |
// Java 方法
public native int sumArray(int[] arr);
// 对应的 JNI 签名
([I)I
方法签名
// 方法的签名格式:(参数类型1参数类型2...)返回类型
// Java 方法
public static boolean check(String name, int age, double score);
// 对应的 JNI 签名
(Ljava/lang/String;ID)Z
// 构造方法的返回类型始终是 V(void),但方法名必须写成 <init>
// Java 构造方法
public Person(String name, int age);
// 对应的 JNI 签名
(Ljava/lang/String;I)V
JNI.so编译配置
若是一个项目原本没有 cpp 代码,但是想集成进去,需要首先在 src/main 目录下创建一个名为 cpp 的文件夹,然后在 /cpp 下创建一个 CMakeLists.txt 和 cpp文件,同时,打开 build.gradle 配置如下信息:
android {
externalNativeBuild {
cmake {
path file('src/main/cpp/CMakeLists.txt')
version '3.31.6'
}
}
}
cpu架构
- arm64-v8a:这是64位ARM架构的指令集,适用于采用ARMv8的64位处理器,大部分中高端智能手机和平板电脑搭载的ARM处理器都支持该指令集,例如华为麒麟系列、高通骁龙系统等芯片
- armeabi-v7a:属于32位的ARM架构的指令集,适用于采用ARMv7架构的32位处理器,在早期的移动设备中广泛使用
- x86:32位的x86指令集,主要用于英特尔和AMD等公司生产的32位处理器,常见于早期的电脑和一些基于x86架构的安卓模拟器以及特定的嵌入式设备中
- x86_64:64位的x86指令集,适用于因特尔和AMD等公司生产的64位处理器,现在大多数桌面电脑、笔记本和部分服务器采用支持该指令集的处理器
运行完项目,在 Project 下的 /app/build/intermediates/merged_native_libs 目录下即可查看到生成的 .so 包。

gradle 可以指明我们想要声明的.so,因为 x86 和 x86-64 这两种用在电脑上的,因此集成到手机上的时候,考虑到应用的体积大小,我们可以这么声明:
defaultConfig {
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a'
}
}
若需要'x86', 'x86_64',就添加上去即可。若想要指明.so库的生成路径,可以在CMakeLists.txt添加以下命令:
#要加在cmake_minimum_required()命令之后,其他命令之前
#该命令意为:在 cmake 文件所在的目录的上级目录下创建文件夹 jniLibs,并将生成的 ABI 文件放在该目录下
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../jniLibs /${ANDROID_ABI}$)
若发现使用的cpp版本不对,可以在gradle中这么声明:
defaultConfig {
cmake {
cppFlags ‘-std=c++17’
}
}
编程实战
首先新建一个项目,选择 Native C++ 选项,然后点击“next”;

随后,输入项目名称等信息,并将 Build configuration language 配置为 Java 的,然后点击“next”;

然后选择 C++ 标准,建议选择 C++ 17 标准,因为在该标准之前会有较大的语法限制。然后点击“finish”;

项目创建以后,打开 /gradle/wrapper/gradle-wrapper.properties,然后将 distributionUrl 修改为国内的镜像路径(具体要根据Android Studio版本去决定下载的版本)
distributionUrl=https://mirrors.cloud.tencent.com/gradle/gradle-8.11.1-bin.zip
native-lib.cpp 就是实现 native 方法的 cpp 文件,可以在这里面实现我们需要的 native 方法
/*extern "C" 的作用:由于cpp有一套自己的编译规则,可以函数重载,有可能编译以后使得函数方法名 func 变为别的名字
从而使得 native 方法找不到,而extern "C"可以保证函数方法名不变,因此需要加上。
JNIEXPORT 的作用:是一个宏定义,相当于java的权限修饰符,可以使得方法或变量可以被外部访问到
JNICALL 的作用:是一个宏定义,一般是一个空定义,不写也可以,但是有可能不同平台定义不同,写上去比较好
JNIEnv 的作用:相当于一个上下文
*/
extern "C" JNIEXPORT jint JNICALL Java_包名_func(JNIEnv* env, jobject jobj) {
jint val = 10;
return val;
}
想要在 JNI 中打印日志,可以这么写:
//导入头文件
#include<android/log.h>
//为了方便使用,可以用一个宏定义封装打印日志的方法
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, "TAG", __VA_ARGS__)
//在方法里使用
LOGD("x的值为%d", x);
比如我们觉得在 Java 代码中给一个数组求和性能太低,想用 C++ 加速,可以先在 Java 类中声明 native 方法:
public native int intFromJNI(int[] arr);
public native int integerFromJNI(Integer[] arr);
随后,在 .cpp 文件中实现该方法:
extern "C" JNIEXPORT jint JNICALL
Java_com_example_jnidemo_MainActivity_intFromJNI(
JNIEnv* env,
jobject,
jintArray arr) {
// 统计总和
jint ans = 0;
// 获取数组长度
jsize len = env->GetArrayLength(arr);
// 获取数组的指针
jint *a = env->GetIntArrayElements(arr, NULL);
// 若数组为空,直接返回答案
if (a == NULL) {
return ans;
}
// 遍历数组,统计元素总和
for (jsize i = 0; i < len; ++ i) {
ans += a[i];
}
// 打印日志,输出元素总和
LOGD("sum = %d", ans);
// 释放数组及其指针
env->ReleaseIntArrayElements(arr, a, 0);
return ans;
}
extern "C" JNIEXPORT jint JNICALL
Java_com_example_jnidemo_MainActivity_integerFromJNI(
JNIEnv* env,
jobject,
jobjectArray arr) {
jint ans = 0;
jsize len = env->GetArrayLength(arr);
jclass integerClass = env->FindClass("java/lang/Integer");
jmethodID intValue = env->GetMethodID(integerClass, "intValue", "()I");
for (jsize i = 0; i < len; ++ i) {
jobject obj = env->GetObjectArrayElement(arr, i);
jint value = env->CallIntMethod(obj, intValue);
ans += value;
env->DeleteLocalRef(obj);
}
// 打印日志,输出元素总和
LOGD("sum1 = %d", ans);
return ans;
}
上述的使用方式,是传入局部变量的方式,有时候需求是不传入局部变量也要拿到 Java 的成员变量,而是通过 JNIEnv 和 jobject 拿到上下文中的成员变量。首先我们需要在 Java 中定义变量和 native 方法:
private String str1Val = "123123";
private int int1Val = 114514;
private static String str2Val = "456456";
private static int int2Val = 541263;
public native void modifyGlobalFields();
随后,需要在 .cpp 文件中声明方法
extern "C" JNIEXPORT void JNICALL
Java_com_example_jnidemo_MainActivity_modifyGlobalFields(
JNIEnv* env,
jobject thiz) {
// 通过 JNIEnv 和 jobject 构造出传入的上下文
jclass clazz = env->GetObjectClass(thiz);
// 通过指定变量名称和类型,从指定的上下文中拿到 jfieldID
jfieldID str1FieldID = env->GetFieldID(clazz, "str1Val", "Ljava/lang/String;");
jfieldID int1FieldID = env->GetFieldID(clazz, "int1Val", "I");
jfieldID str2FieldID = env->GetStaticFieldID(clazz, "str2Val", "Ljava/lang/String;");
jfieldID int2FieldID = env->GetStaticFieldID(clazz, "int2Val", "I");
// 引用数据类型需要通过 static_cast 转化
jstring str = static_cast<jstring>(env->GetObjectField(thiz, str1FieldID));
// 基本数据类型可以直接转化
jint i = env->GetIntField(thiz, int1FieldID);
// 由于 jstring 不能直接打印,可以转化为 char*
const char* s = env->GetStringUTFChars(str, nullptr);
// 打印出值
LOGD("strVal = %s", s);
LOGD("intVal = %d", i);
jstring newStr = env->NewStringUTF("123321");
jint newInt = 123;
// 将 newStr 赋值给 thiz 中的 str1FieldID
env->SetObjectField(thiz, str1FieldID, newStr);
// 将 newInt 赋值给 thiz 中的 int1FieldID
env->SetIntField(thiz, int1FieldID, newInt);
// 将 newStr 赋值给 clazz 中的 str2FieldID
env->SetStaticObjectField(clazz, str2FieldID, newStr);
// 将 newInt 赋值给 clazz 中的 int2FieldID
env->SetStaticIntField(clazz, int2FieldID, newInt);
}
浙公网安备 33010602011771号