学习 Android(十四)NDK基础
Android NDK 是一个工具集,可让我们使用 C 和 C++ 等语言以原生代码实现应用的各个部分。对于特定类型的应用,这可以帮助我们重复使用以这些语言编写的代码库。
接下来,我们将按照以下步骤进行讲解
- NDK 是什么,作用和原理
- Android Studio 中配置 NDK 与 CMake
- 创建简单 Native 库(C/C++),Java 调用 Native 方法
- 了解 JNI 基本概念,基本数据类型映射,Java 和 C++ 函数签名
- 学习如何传递 Java 字符串、数组到 Native ,反之亦然
1. NDK 是什么?作用和原理
1.1 NDK 是什么?
原生开发套件 (NDK) 是一套工具,能够让我们在 Android 应用中使用 C 和 C++ 代码,并提供众多平台库,我们可使用这些平台库管理原生 activity 和访问实体设备组件,例如传感器和触控输入。NDK 可能不适合大多数 Android 编程初学者(例如作者我),初学者只需使用 Java 代码和框架 API 开发应用。然而,我们需要实现以下一个或多个目标,那么 NDK 就能派上用场:
-
进一步提升设备性能,以降低延迟或运行游戏或物理模拟等计算密集型应用。
-
重复使用您自己或其他开发者的 C 或 C++ 库。
我们可以在 Android Studio 2.2 或更高版本中使用 NDK 将 C 和 C++ 代码编译到原生库中,然后使用 Android Studio 的集成构建系统 Gradle 将原生库打包到 APK 中。Java 代码随后可以通过 Java 原生接口 (JNI) 框架调用原生库中的函数。
Android Studio 编译原生库的默认构建工具是 CMake。由于很多现有项目都使用 ndk-build 构建工具包,因此 Android Studio 也支持 ndk-build。不过,如果要创建新的原生库,则应使用 CMake。
1.2 NDK 的工作原理
NDK 的本质是通过 JNI(Java Native Interface)桥接 Java/Kotlin 和 C/C++ 本地代码,从而实现跨语言通信与调用,并在 Android 系统中生成 .so
动态链接库供运行时加载。
- 整体架构流程图如下所示
Java/Kotlin 层|| 调用 native 方法v
JNI (Java Native Interface)|| 负责参数类型转换、方法注册v
C/C++ 层代码(通过 NDK 编译)|| 编译为 .so 动态库v
libnative-lib.so 被 Android 加载并运行
-
Java 层声明 native 方法
我们首先要在 Java 或 Kotlin 中用
native
关键字声明一个方法:public class NativeLib {static {System.loadLibrary("native-lib"); // 加载 C/C++ 编译生成的 .so 文件}public native int add(int a, int b); // native 方法,C/C++ 实现 }
-
C/C++ 层实现(通用JNI)
我们需要在 C/C++ 中用 JNI 方式实现这个方法,签名必须完全匹配
extern "C" JNIEXPORT jint JNICALL Java_com_example_NativeLib_add(JNIEnv *env, jobject thisz, jnit a, jint b) {return a + b; }
-
JNIEnv*
是 JNI 环境指针(用于访问 Java) -
jobject
是 Java 传进来的对象引用(即 this)
-
-
构建和变异位动态库(
.so
文件)使用
CMakeLists.txt
或Android.mk
构建规则,把你的 C++ 文件编译成.so
:-
输出目录:
app/build/intermediates/cmake/debug/obj/arm64-v8a/libnative-lib.so
-
会被打包进 APK,在运行时由
System.loadLibrary
加载
-
-
运行时调用流程
-
用户点击或代码调用
NativeLib.add()
-
JVM 会通过 JNI 找到
.so
文件中注册的Java_com_example_NativeLib_add()
方法 -
调用 C++ 实现,返回结果给 Java
-
2. Android Studio 中配置 NDK 与 CMake
2.1 在 Android Studio 中操作:
-
打开 Preferences(设置):
-
macOS:
Android Studio > Preferences
-
Windows/Linux:
File > Settings
-
-
导航到:
Appearance & Behavior > System Settings > Android SDK > SDK Tools
-
勾选并安装:
-
NDK (Side by side)
-
CMake
-
LLDB(可选,调试 C++ 用)
-
2.2 配置 build.gradle
文件
以下以 App 模块的 build.gradle
(Groovy 版) 为例说明配置方式:
-
defaultConfig
中添加:defaultConfig {...externalNativeBuild {cmake {cppFlags ""}}ndk {abiFilters 'armeabi-v7a', 'arm64-v8a' // 你可以根据需求精简架构} }
-
配置
externalNativeBuild
android {...externalNativeBuild {cmake {path "src/main/cpp/CMakeLists.txt" // 指向你的 CMake 配置文件version "3.22.1" // 根据你安装的版本写}} }
2.3 创建 C/C++ 文件和 CMake 配置
app/└── src/└── main/├── cpp/│ ├── native-lib.cpp│ └── CMakeLists.txt└── java/
native-lib.cpp
#include <jni.h>extern "C"
JNIEXPORT jint JNICALL
Java_com_example_ndkdemo_NativeLib_add(JNIEnv *env, jobject obj, jint a, jint b) {return a + b;
}
CMakeLists.txt
cmake_minimum_required(VERSION 3.10.2)project("ndkdemo")add_library( # 构建库名native-libSHAREDnative-lib.cpp
)find_library( # 找到 log 库log-liblog
)target_link_libraries( # 链接 log 库native-lib${log-lib}
)
2.4 Java 层调用 native 方法
public class NativeLib {static {System.loadLibrary("native-lib"); // 加载 .so}public native int add(int a, int b); // 声明 native 方法
}
2.5 构建与运行
-
点击 Build → Rebuild Project
-
.so 文件将生成在:
app/build/intermediates/cmake/debug/obj/arm64-v8a/libnative-lib.so
-
如果你运行到 ARM64 模拟器或真机,程序会自动加载对应
.so
并调用你的 native 方法。
3. 创建简单 Native 库(C/C++),Java 调用 Native 方法
3.1 步骤一:项目结构准备
在 Android Studio 中新建一个空项目(Empty Activity),选择 Java语言,API 21,然后按如下结构添加文件:
app/└── src/└── main/├── cpp/│ ├── native-lib.cpp C++ 实现文件│ └── CMakeLists.txt CMake 构建文件└── java/com/example/ndkdemo/└── NativeLib.java Java 调用封装类
3.2 步骤二:配置 build.gradle
(app 模块)
android {defaultConfig {...// 指定使用的 ABI 架构ndk {abiFilters 'armeabi-v7a', 'arm64-v8a'}// 配置 CMake 构建externalNativeBuild {cmake {cppFlags ""}}}// 指定 CMake 构建文件路径externalNativeBuild {cmake {path "src/main/cpp/CMakeLists.txt"}}
}
3.3 步骤三:创建 CMake 构建文件(CMakeLists.txt)
在 app/src/main/cpp/CMakeLists.txt
中实现
cmake_minimum_required(VERSION 3.10.2)
project("ndkdemo") // 记得这是填对应的名称add_library( # native 库名native-libSHAREDnative-lib.cpp
)find_library( # 引用 Android 日志库(可选)log-liblog
)target_link_libraries(native-lib${log-lib}
)
3.4 步骤四:实现 C++ 代码(native-lib.cpp)
在 app/src/main/cpp/native-lib.cpp
中实现
#include <jni.h>// 使用 extern "C" 避免 C++ 方法名被改写(mangling)
extern "C"
JNIEXPORT jint JNICALL
Java_com_example_ndkdemo_NativeLib_add(JNIEnv *env, jobject thiz, jint a, jint b) {return a + b;
}
3.5 步骤五:Java 封装 Native 调用
在 com/example/ndkdemo/NativeLib.java
中实现
package com.example.ndkdemo;public class NativeLib {static {System.loadLibrary("native-lib"); // 加载 native-lib.so 动态库}// native 方法声明,由 C++ 实现public native int add(int a, int b);
}
3.6 步骤六:在 Activity 中调用验证
在 com/example/ndkdemo/MainActivity.java
中实现
public class MainActivity extends AppCompatActivity {private final NativeLib nativeLib = new NativeLib();private TextView textView;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);textView = findViewById(R.id.tv_result);int result = nativeLib.add(3, 4); // 调用 native 方法textView.setText("3 + 4 = " + result);}
}
3.7 步骤七:构建运行
编译运行结果如下所示
4. 了解 JNI 基本概念,基本数据类型映射,Java 和 C++ 函数签名
接下来我们将针对 JNI 进行相关的学习和了解
4.1 基本数据类型
Java 类型 | JNI 类型 | 描述 |
---|---|---|
boolean | jboolean | 无符号 8 位(通常为 unsigned char ),值为 JNI_TRUE (1) 或 JNI_FALSE (0) |
byte | jbyte | 有符号 8 位 |
char | jchar | 无符号 16 位 |
short | jshort | 有符号 16 位 |
int | jint | 有符号 32 位 |
long | jlong | 有符号 64 位 |
float | jfloat | 32 位 IEEE 浮点数 |
double | jdouble | 64 位 IEEE 浮点数 |
void | void | 对应 void 类型 |
4.2 引用类型
Java 类型 | JNI 类型 | 说明 |
---|---|---|
java.lang.Object | jobject | 所有对象的基类 |
任意 Java 类 | jclass | Java 类的引用 |
java.lang.String | jstring | Java 字符串 |
T[] (Java 数组) | jarray | 所有数组的基类 |
原始类型数组 | jintArray 、jbyteArray 等 | 特定类型的数组 |
Java 对象数组 | jobjectArray | 包含对象引用的数组 |
异常 | jthrowable | 可被 throw 的对象 |
4.3 特殊辅助类型
JNI 类型 | 定义 | 用途 |
---|---|---|
jsize | typedef jint jsize; | 表示数组、字符串长度或大小等 |
jfieldID | 不透明指针类型 | 标识一个类的字段 |
jmethodID | 不透明指针类型 | 标识一个类的方法 |
4.4 本地方法接口类型
JNI 提供的所有函数都通过这两个结构体访问:
类型名 | 说明 |
---|---|
JNIEnv * | 每个线程独有,包含 JNI 所有函数指针 |
JavaVM * | JVM 实例指针,用于跨线程附加线程等操作 |
4.5 布尔常量
为兼容 C 语言布尔类型,定义了:
#define JNI_TRUE 1
#define JNI_FALSE 0
4.6 原始类型数组
Java 类型 | JNI 类型 |
---|---|
boolean[] | jbooleanArray |
byte[] | jbyteArray |
char[] | jcharArray |
short[] | jshortArray |
int[] | jintArray |
long[] | jlongArray |
float[] | jfloatArray |
double[] | jdoubleArray |
4.7 对象数组
Java 类型 | JNI 类型 | 说明 |
---|---|---|
String[] | jobjectArray | 指向一组 jstring 对象的数组 |
Object[] | jobjectArray | 可存放任意引用类型对象 |
SomeClass[] | jobjectArray | 存放 SomeClass 对象的数组 |
5 Java 和 C++ 函数签名
Java 和 C++ 函数签名是函数唯一身份的定义方式,但两者的表现形式和规则存在差异。
5.1 Java 的函数签名
Java 中函数签名包括:函数名 + 参数类型列表(不包括返回值)
public int add(int a, int b) { ... }
Java 中,下面两个方法签名相同,会报错
public void test(int x) { }
public int tes(int x) { } // 编译报错:签名冲突(返回值不算签名)
Java 方法签名示例(包括参数类型):
方法声明 | 签名(方法名 + 参数类型) |
---|---|
void foo(int x) | foo(I) |
void foo(String s) | foo(Ljava/lang/String;) |
void foo(int[] arr) | foo([I) |
void foo(int x, String s) | foo(ILjava/lang/String;) |
5.2 C++ 的函数签名
C++ 中函数签名包括:函数名 + 参数类型列表(返回值不计入签名)
int add(int a, int b);
double add(int a, int b); // 编译错误:重定义函数(签名冲突)
但和 Java 不同的是,C++ 支持函数重载:C++ 的重载机制在编译和链接层处理得很好,不需要额外区分。但 Java 的重载虽然语法上支持,但在调用 native 方法时,需要开发者显式编码函数签名,这让处理重载略显麻烦。并不是说 Java 不支持重载,而是说 Java 的重载不天然适用于 native binding,需要额外工作。
void print(int x);
void print(double x);
函数签名还包括是否为指针、引用、常量等修饰:
void func(int &x); // 引用
void func(const int x); // const 修饰不同参数,签名不同
5.3 Java 和 C++ 在 JNI 中的函数签名映射
JNI 中为了让 Java 调用 C/C++ 函数,会将 Java 方法 签名映射为 JNI 名字。
public class MyClass {public native void hello(String msg);
}
对应的 C 函数签名为:
JNIEXPORT void JNICALL Java_MyClass_hello(JNIENV *env, jobject obj, jstring msg);
规则如下:
-
包名和类名中的
.
替换为_
-
方法名拼接在类名后
-
参数类型在 JNI 中通过
jint
、jstring
、jbooleanArray
等类型传递
5.4 常见 JNI 签名编码表
Java 类型 | JNI 类型编码 |
---|---|
int | I |
boolean | Z |
byte | B |
char | C |
short | S |
long | J |
float | F |
double | D |
void | V |
Object | L类名; |
int[] | [I |
String | Ljava/lang/String; |
6. 学习如何传递 Java 字符串、数组到 Native ,反之亦然
6.1 Java 与 Native(C/C++)之间的数据传递总览
类型 | Java -> Native | Native -> Java |
---|---|---|
String | jstring → const char* (使用 GetStringUTFChars ) | 创建 jstring (用 NewStringUTF ) |
基本类型数组 | jintArray , jbyteArray 等 → jint* (使用 GetXxxArrayElements 或 GetXxxArrayRegion ) | 创建数组并填充(用 NewXxxArray + SetXxxArrayRegion ) |
对象数组 | jobjectArray → 单个元素用 GetObjectArrayElement 访问 | 创建 jobjectArray 并填充每一项 |
自定义对象 | 传入 jobject ,通过 JNI API 访问其字段或方法 | 构造 Java 对象并返回 |
6.2 Java 字符串与 Native 的相互转换:
-
Java -> Native :获取 C 字符串:
extern "C" JNIEXPORT void JNICALL Java_com_example_hello_NativeLib_print(JNIEnv* env, jobject thiz, jstring jStr) {const char* cStr = (*env).GetStringUTFChars(jStr, 0);printf("收到字符串: %s\n", cStr);(*env).ReleaseStringUTFChars(jStr, cStr); // 一定要释放 }
-
Native -> Java :创建 Java 字符串:
extern "C" JNIEXPORT jstring JNICALL Java_com_example_hello_NativeLib_stringFromJNI(JNIEnv* env,jobject thiz /* this */) {jstring result = (*env).NewStringUTF("你好 MainActivity");return result; }
6.3 Java 数组与 Native 的相互转换:
-
Java int[] -> Native
extern "C" JNIEXPORT void JNICALL Java_com_example_demo_NativeLib_sum(JNIEnv* env, jobject thiz, jintArray arr) {jsize len = (*env).GetArrayLength(arr);jint* elems = (*env).GetIntArrayElements(arr, NULL);int sum = 0;for (int i = 0; i < len; i++) {sum += elems[i];}printf("总和: %d\n", sum);(*env).ReleaseIntArrayElements(arr, elems, 0); // 0 表示更新 Java 数组 }
-
Native int[] -> Java int[]
extern "C" JNIEXPORT jintArray JNICALL Java_com_example_demo_NativeLib_getNumbers(JNIEnv *env, jobject) {jint nums[] = {1, 2, 3, 4, 5};jintArray arr = (*env).NewIntArray(5);(*env).SetIntArrayRegion(arr, 0, 5, nums);return arr; }
在 Native (C/C++) 中使用 printf()
打印日志时,它的输出位置取决于哪个平台运行,在 Android 中 printf()
输出不会自动出现在 Logcat,我们通常看不到它的输出。
为此我们需要使用 __android_log_print
在 native-lib.cpp
中添加
#include <android/log.h>#define LOG_TAG "NativeLog"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
将在 Native 中使用 print() 的方法替换成 LOGI() 或者 LOGE() 方法,我们就可以在 Logcat 查看日志了
jintArray arr = (*env).NewIntArray(5);
(*env).SetIntArrayRegion(arr, 0, 5, nums);
return arr;
}
在 Native (C/C++) 中使用 `printf()` 打印日志时,它的输出位置取决于哪个平台运行,**在 Android 中 `printf()` 输出不会自动出现在 Logcat**,我们通常看不到它的输出。为此我们需要使用 `__android_log_print`在 `native-lib.cpp` 中添加```cpp
#include <android/log.h>#define LOG_TAG "NativeLog"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
将在 Native 中使用 print() 的方法替换成 LOGI() 或者 LOGE() 方法,我们就可以在 Logcat 查看日志了