Android NDK 完整入门教程
目录
- 什么是 NDK?为什么需要它?
- NDK 的核心概念
- JNI (Java Native Interface)
- C/C++ 代码
- 编译系统
- ABI (Application Binary Interface)
- 环境搭建
- 前提条件
- 安装与配置
- 实战:从零开始构建你的第一个 NDK 项目
- 步骤 1: 创建支持 C++ 的 Android Studio 项目
- 步骤 2: 编写 C/C++ 源代码
- 步骤 3: 配置 CMake (构建脚本)
- 步骤 4: 配置 Gradle (构建 NDK 模块)
- 步骤 5: 在 Java/Kotlin 代码中加载和调用 Native 方法
- 步骤 6: 同步、构建和运行
- 进阶主题
- 数据类型映射
- 异常处理
- 日志输出
- 调试 Native 代码
- NDK 的典型应用场景
- 总结与最佳实践
什么是 NDK?为什么需要它?
NDK (Native Development Kit) 是 Android SDK 的一个工具集,它允许开发者使用 C 和 C++ 语言为 Android 应用程序编写本地代码。
为什么需要 NDK?
- 性能优化:对于计算密集型任务,如图形处理、物理模拟、音视频编解码、游戏逻辑等,C/C++ 代码的执行效率远高于 Java/Kotlin,它们可以直接操作内存,没有虚拟机的开销。
- 代码复用:如果你有一个用 C/C++ 编写的成熟库(一些开源的音视频编解码库如 FFmpeg),你可以通过 NDK 将其集成到你的 Android 应用中,而无需用 Java/Kotlin 重写。
- 硬件访问:虽然不常见,但有时需要直接访问特定的硬件指令或内存映射,这在 Native 层更容易实现。
重要提示:NDK 不是用来提升 UI 渲染性能的工具,UI 操作必须在主线程上使用 Android SDK 的 Java/Kotlin API 完成,滥用 NDK 会导致应用更复杂、更难维护,并可能引入新的 Bug。“能用 Java/Kotlin 解决的,就不要用 NDK” 是一个重要的原则。
NDK 的核心概念
在开始之前,你需要理解几个核心概念:
a. JNI (Java Native Interface)
JNI 是 Java 平台的一个标准,它允许 Java 代码和其他语言(主要是 C/C++)进行交互,它是连接 Java 世界和 Native 世界的桥梁。
- 从 Java 调用 C/C++:Java 声明一个
native方法,C/C++ 实现这个方法。 - 从 C/C++ 调用 Java:Native 代码可以创建 Java 对象、调用 Java 方法、访问 Java 字段。
b. C/C++ 代码
这就是你用 NDK 编写的“原生”代码,通常放在一个专门的目录中(app/src/main/cpp)。
c. 编译系统
Android Studio 使用 CMake 作为默认的 NDK 项目编译系统,CMake 是一个跨平台的构建工具,它会读取一个名为 CMakeLists.txt 的配置文件,然后将你的 C/C++ 源代码编译成库文件(.so 文件)。
d. ABI (Application Binary Interface)
ABI 定义了二进制代码如何在特定硬件和操作系统上运行,不同的 CPU 架构有不同的 ABI。
- 常见 ABI:
armeabi-v7a: 32 位 ARM 架构,兼容绝大多数旧设备。arm64-v8a: 64 位 ARM 架构,现在的主流设备。x86: 32 位 x86 架构,主要用于模拟器。x86_64: 64 位 x86 架构,用于新的模拟器和部分平板电脑。
当你构建 NDK 项目时,Gradle 会根据你的配置为指定的 ABI 架构编译对应的 .so 库,并将它们打包到 APK 中,这样,当应用在设备上运行时,系统会自动加载与设备 CPU 架构匹配的库。
环境搭建
a. 前提条件
- Android Studio: 最新版本的 Android Studio (推荐 2025.1 或更高版本)。
- SDK Tools: 在 Android Studio 的 SDK Manager 中,确保你已经安装了:
- NDK (Side by side): NDK 的版本,你可以选择一个稳定的版本(如 r21e, r23b 等)。
- CMake: 构建工具。
- LLDB: 用于调试 Native 代码的调试器。
b. 安装与配置
- 打开 Android Studio,进入
Tools->SDK Manager。 - 切换到
SDK Tools标签页。 - 勾选 NDK (Side by side) 和 CMake,然后点击
Apply或OK进行安装。 - 安装完成后,Android Studio 会自动配置好路径,你无需手动设置
NDK_HOME等环境变量。
实战:从零开始构建你的第一个 NDK 项目
我们将创建一个简单的应用,它调用一个 C++ 函数来计算两个整数的和,并显示在界面上。
步骤 1: 创建支持 C++ 的 Android Studio 项目
这是最简单、最推荐的方式。
- 打开 Android Studio,选择
File->New->New Project...。 - 选择
Native C++模板,然后点击Next。 - 配置你的应用名称、包名、保存位置等。
- 在 "C++ Standard" 下拉菜单中,选择你熟悉的 C++ 标准(如
C++17)。 - 点击
Finish,Android Studio 会为你创建一个包含 NDK 配置的完整项目。
步骤 2: 编写 C/C++ 源代码
Android Studio 会自动创建一些示例文件,我们先来看一下它们,然后修改它们。
-
在
app/src/main/cpp/目录下,你会看到一个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()); }代码解析:
extern "C": 告诉 C++ 编译器使用 C 语言的链接方式,这非常重要,因为 JNI 函数名需要遵循特定的 C 风格命名规则。JNIEXPORT/JNICALL: JNI 关键字,用于标记 JNI 函数。jstring: JNI 的字符串类型,对应 Java 的String。JNIEnv* env: 指向 JNI 环境的指针,通过它可以调用 Java 的方法。jobject /* this */: 指向调用该 Native 方法的 Java 对象(这里是MainActivity的实例)。Java_com_example_myapplication_MainActivity_stringFromJNI: 这是 JNI 函数的命名规则,格式为Java_包名_类名_方法名。注意包名中的点 () 要用下划线 (_) 代替。env->NewStringUTF(hello.c_str()): 将 C++ 的std::string转换为 Java 的String并返回。
-
修改
native-lib.cpp,添加我们自己的求和函数。#include <jni.h> #include <string> // 新增的求和函数 extern "C" JNIEXPORT jint JNICALL Java_com_example_myapplication_MainActivity_addNumbers( JNIEnv* env, jobject /* this */, jint a, jint b) { return a + b; } // 原有的示例函数 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()); }我们新增了一个
addNumbers函数,它接收两个jint(对应 Java 的int) 参数,返回一个jint。
步骤 3: 配置 CMake (CMakeLists.txt)
在 app/src/main/cpp/ 目录下,你会找到 CMakeLists.txt 文件,它是 CMake 的配置文件,告诉 CMake 如何编译你的代码。
# 设置 CMake 最低版本要求
cmake_minimum_required(VERSION 3.18.1)
# 定义项目名称
project("myapplication")
# 添加 C++ 标准
add_library( # 库名称
native-lib # 我们将创建的库的名称
SHARED # 共享库 (.so 文件)
# 源文件列表
native-lib.cpp)
# 找到 log 库
find_library( # 设置一个变量来存储路径
log-lib
# 指定你要找的库名称
log)
# 将 log 库链接到我们的 native-lib 库
target_link_libraries( # 目标库
native-lib
# 链接的库列表
${log-lib})
解析:
add_library: 定义我们要构建一个名为native-lib的共享库,源文件是native-lib.cpp。find_library: 找到 Android 系统的log库,我们将用它来在 Native 代码中打印日志。target_link_libraries: 将log库链接到native-lib,这样我们就可以在native-lib.cpp中使用log相关的函数了。
步骤 4: 配置 Gradle (build.gradle.kts 或 build.gradle)
现在需要告诉 Android 的构建系统(Gradle)去使用 CMake 来编译我们的 Native 代码。
打开 app/build.gradle.kts (如果使用 Kotlin DSL) 或 app/build.gradle (如果使用 Groovy DSL) 文件,你会看到一个 android { ... } 代码块。
确保其中有 externalNativeBuild 配置:
// Groovy DSL (build.gradle)
android {
// ... 其他配置如 compileSdk, defaultConfig 等
defaultConfig {
// ...
externalNativeBuild {
cmake {
cppFlags ""
// 可以在这里指定 ABI,
// abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86', 'x86_64'
}
}
}
// ... 其他配置如 buildTypes 等
// 关键部分:告诉 Gradle 去哪里找 CMakeLists.txt
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.18.1" // 使用你安装的 CMake 版本
}
}
}
解析:
externalNativeBuild: 这是连接 Gradle 和 CMake 的桥梁。path: 指向你的CMakeLists.txt文件的路径。version: 指定 CMake 的版本,应与 SDK Manager 中安装的版本一致。
步骤 5: 在 Java/Kotlin 代码中加载和调用 Native 方法
-
打开你的主 Activity,
MainActivity.java或MainActivity.kt。 -
声明 Native 方法:在类中声明
native方法,方法名必须与 C++ 代码中的 JNI 函数名完全匹配(去掉Java_和后面的__,并按包名_类名_方法名的格式)。// Java (MainActivity.java) package com.example.myapplication; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.widget.TextView; import android.widget.Button; import android.widget.Toast; public class MainActivity extends AppCompatActivity { // 加载包含 Native 方法的库 static { System.loadLibrary("native-lib"); } // 声明 Native 方法 public native String stringFromJNI(); public native int addNumbers(int a, int b); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); TextView tv = findViewById(R.id.sample_text); Button button = findViewById(R.id.calculate_button); // 调用 Native 方法并显示结果 tv.setText(stringFromJNI()); button.setOnClickListener(v -> { int result = addNumbers(10, 20); Toast.makeText(this, "10 + 20 = " + result, Toast.LENGTH_SHORT).show(); }); } }关键点:
static { System.loadLibrary("native-lib"); }: 这行代码至关重要,它会在类加载时加载我们之前用 CMake 编译的native-lib.so库。库名是CMakeLists.txt中add_library定义的库名。public native String stringFromJNI();:native关键字告诉编译器这个方法的实现在别处(在 Native 代码中)。
-
更新布局文件 (
activity_main.xml),添加一个TextView和一个Button用于测试。<LinearLayout ...> <TextView android:id="@+id/sample_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <Button android:id="@+id/calculate_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Calculate Sum" android:layout_marginTop="20dp"/> </LinearLayout>
步骤 6: 同步、构建和运行
- 点击 Android Studio 顶部出现的 "Sync Now" 按钮,让 Gradle 读取新的
build.gradle配置。 - 点击绿色的 "Run" 按钮 (▶) 来构建并运行你的应用。
- 应用启动后,你会看到 "Hello from C++" 的文本,点击 "Calculate Sum" 按钮,会弹出一个 Toast 提示,显示 "10 + 20 = 30"。
恭喜!你已经成功创建并运行了你的第一个 NDK 应用!
进阶主题
a. 数据类型映射
Java 和 C/C++ 的数据类型需要通过 JNI 进行映射。
| Java 类型 | JNI 类型 | C++ 类型 | 描述 |
|---|---|---|---|
byte |
jbyte |
int8_t / signed char |
8位有符号整数 |
short |
jshort |
int16_t / short |
16位有符号整数 |
int |
jint |
int32_t / int |
32位有符号整数 |
long |
jlong |
int64_t / long long |
64位有符号整数 |
float |
jfloat |
float |
32位浮点数 |
double |
jdouble |
double |
64位浮点数 |
char |
jchar |
uint16_t / unsigned short |
16位 Unicode 字符 |
boolean |
jboolean |
uint8_t / bool |
8位布尔值 (true=1, false=0) |
String |
jstring |
- | Java 字符串 |
Object |
jobject |
- | 任何 Java 对象 |
Class |
jclass |
- | Java 类对象 |
b. 异常处理
在 Native 代码中,Java 的异常不会立即中断 C/C++ 的执行,你需要手动检查和处理。
#include <jni.h>
#include <stdexcept>
extern "C" JNIEXPORT void JNICALL
Java_com_example_myapp_MyClass_riskyMethod(JNIEnv* env, jobject thiz) {
jclass cls = env->FindClass("java/lang/IllegalArgumentException");
if (cls == nullptr) {
// 找不到类,可能已经抛出了其他异常
return;
}
// 检查是否有待处理的异常
if (env->ExceptionCheck()) {
// 清理异常,否则它会一直存在
env->ExceptionDescribe(); // 打印异常信息到 logcat
env->ExceptionClear(); // 清除异常
// 或者直接重新抛出一个更合适的异常
env->ThrowNew(cls, "Something went wrong!");
return;
}
// ... 正常逻辑 ...
}
c. 日志输出
在 Native 代码中,我们可以使用 android/log.h 来打印日志,这些日志会显示在 Android Studio 的 Logcat 中。
-
在
native-lib.cpp顶部添加头文件:#include <android/log.h>
-
使用
__android_log_print函数打印日志:#define LOG_TAG "MyNativeTag" #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) // ... LOGI("Native function called with a=%d, b=%d", a, b); LOGE("An error occurred!"); -
在 Logcat 中,你可以通过设置 "Tag" 为
MyNativeTag来筛选你的 Native 日志。
d. 调试 Native 代码
- 在你的 C/C++ 代码行号旁边设置断点(就像调试 Java 代码一样)。
- 在运行应用时,选择 "Debug" 模式 (🐭 图标)。
- 当应用执行到断点时,Android Studio 会自动切换到 C++ 的调试视图,你可以查看变量、单步执行等。
NDK 的典型应用场景
- 游戏引擎: Unity, Unreal Engine 等重度依赖 NDK 进行高性能渲染和物理计算。
- 音视频处理: FFmpeg, OpenCV 等库的集成,用于编解码、滤镜、图像处理。
- 物理模拟: 复杂的 3D 物理引擎。
- 加密算法: 对性能要求极高的加密解密操作。
- 部分高性能计算: 如科学计算、数据分析等。
总结与最佳实践
- 不要滥用 NDK: 优先使用 Java/Kotlin 解决问题。
- 学习 JNI: 它是连接两个世界的核心,必须理解其工作原理和数据类型映射。
- 熟悉 CMake: 学会编写和修改
CMakeLists.txt是管理 NDK 项目的关键。 - 模块化设计: 将 Native 代码封装成独立的模块或库,方便复用和维护。
- 错误处理: 在 Native 代码中妥善处理异常和错误,并通过日志或回调机制通知 Java 层。
- 内存管理: C/C++ 需要手动管理内存,要特别注意内存泄漏和野指针问题,可以考虑使用智能指针(如
std::unique_ptr,std::shared_ptr)来简化管理。 - 保持 ABI 兼容性: 一旦编译了
.so文件,就不要轻易改变其导出函数的签名(参数、返回值),否则已安装的旧版本应用将崩溃。
这份教程涵盖了从入门到实践的完整流程,希望它能帮助你顺利开启 Android NDK 的开发之旅!
