杰瑞科技汇

Android NDK教程怎么学?入门到上手指南

Android NDK 完整入门教程

目录

  1. 什么是 NDK?为什么需要它?
  2. NDK 的核心概念
    • JNI (Java Native Interface)
    • C/C++ 代码
    • 编译系统
    • ABI (Application Binary Interface)
  3. 环境搭建
    • 前提条件
    • 安装与配置
  4. 实战:从零开始构建你的第一个 NDK 项目
    • 步骤 1: 创建支持 C++ 的 Android Studio 项目
    • 步骤 2: 编写 C/C++ 源代码
    • 步骤 3: 配置 CMake (构建脚本)
    • 步骤 4: 配置 Gradle (构建 NDK 模块)
    • 步骤 5: 在 Java/Kotlin 代码中加载和调用 Native 方法
    • 步骤 6: 同步、构建和运行
  5. 进阶主题
    • 数据类型映射
    • 异常处理
    • 日志输出
    • 调试 Native 代码
  6. NDK 的典型应用场景
  7. 总结与最佳实践

什么是 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. 前提条件

  1. Android Studio: 最新版本的 Android Studio (推荐 2025.1 或更高版本)。
  2. SDK Tools: 在 Android Studio 的 SDK Manager 中,确保你已经安装了:
    • NDK (Side by side): NDK 的版本,你可以选择一个稳定的版本(如 r21e, r23b 等)。
    • CMake: 构建工具。
    • LLDB: 用于调试 Native 代码的调试器。

b. 安装与配置

  1. 打开 Android Studio,进入 Tools -> SDK Manager
  2. 切换到 SDK Tools 标签页。
  3. 勾选 NDK (Side by side)CMake,然后点击 ApplyOK 进行安装。
  4. 安装完成后,Android Studio 会自动配置好路径,你无需手动设置 NDK_HOME 等环境变量。

实战:从零开始构建你的第一个 NDK 项目

我们将创建一个简单的应用,它调用一个 C++ 函数来计算两个整数的和,并显示在界面上。

步骤 1: 创建支持 C++ 的 Android Studio 项目

这是最简单、最推荐的方式。

  1. 打开 Android Studio,选择 File -> New -> New Project...
  2. 选择 Native C++ 模板,然后点击 Next
  3. 配置你的应用名称、包名、保存位置等。
  4. 在 "C++ Standard" 下拉菜单中,选择你熟悉的 C++ 标准(如 C++17)。
  5. 点击 Finish,Android Studio 会为你创建一个包含 NDK 配置的完整项目。

步骤 2: 编写 C/C++ 源代码

Android Studio 会自动创建一些示例文件,我们先来看一下它们,然后修改它们。

  1. 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 并返回。
  2. 修改 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.ktsbuild.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 方法

  1. 打开你的主 Activity,MainActivity.javaMainActivity.kt

  2. 声明 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.txtadd_library 定义的库名
    • public native String stringFromJNI();: native 关键字告诉编译器这个方法的实现在别处(在 Native 代码中)。
  3. 更新布局文件 (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: 同步、构建和运行

  1. 点击 Android Studio 顶部出现的 "Sync Now" 按钮,让 Gradle 读取新的 build.gradle 配置。
  2. 点击绿色的 "Run" 按钮 (▶) 来构建并运行你的应用。
  3. 应用启动后,你会看到 "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 中。

  1. native-lib.cpp 顶部添加头文件:

    #include <android/log.h>
  2. 使用 __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!");
  3. 在 Logcat 中,你可以通过设置 "Tag" 为 MyNativeTag 来筛选你的 Native 日志。

d. 调试 Native 代码

  1. 在你的 C/C++ 代码行号旁边设置断点(就像调试 Java 代码一样)。
  2. 在运行应用时,选择 "Debug" 模式 (🐭 图标)。
  3. 当应用执行到断点时,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 的开发之旅!

分享:
扫描分享到社交APP
上一篇
下一篇