xDocxDoc
AI
前端
后端
iOS
Android
Flutter
AI
前端
后端
iOS
Android
Flutter
  • Android NDK 开发入门

Android NDK 开发入门

本文将深入探讨如何在 Android 项目中集成和构建 C++ 代码,使用 Android NDK(Native Development Kit)和 JNI(Java Native Interface)实现高性能计算和现有 C++ 库的重用。内容涵盖环境配置、构建系统详解、常见问题解决方案及性能优化建议。

1. Android NDK 开发基础

Android NDK 是一套工具集合,允许您在 Android 应用中使用 C 和 C++ 代码。NDK 提供了平台库,用于管理原生 Activity 和访问物理设备组件(如传感器和触摸输入)。通过 NDK,开发者可以将 C/C++ 代码编译成原生库,Gradle 可将其打包到 APK 中。Java 或 Kotlin 代码随后可通过 JNI 框架调用原生库中的函数。

1.1 NDK 的优势与适用场景

使用 NDK 开发的主要优势包括:

  • 性能提升:对于计算密集型任务(如图像处理、物理模拟、数学计算),C++ 通常能提供比 Java 或 Kotlin 更好的性能。
  • 代码复用:重用现有的 C/C++ 库,避免重复实现功能。
  • 底层硬件访问:直接访问设备硬件特性,如传感器、GPU 等。

NDK 特别适用于游戏引擎、音频/视频处理、AR/VR 应用等高性能应用场景。

1.2 开发环境要求

要编译和调试应用的原生代码,您需要以下组件:

  • Android Native Development Kit (NDK):使您能在 Android 中使用 C 和 C++ 代码的工具集。
  • CMake:一个与 Gradle 配合使用来构建您原生库的外部构建工具。如果您只计划使用 ndk-build,则不需要此组件。
  • LLDB:Android Studio 中用于调试原生代码的调试器。

2. 环境配置与项目设置

2.1 安装 NDK 和构建工具

在 Android Studio 中安装 NDK 和 CMake:

  1. 打开 Android Studio,选择 File > Settings(在 macOS 上为 Android Studio > Preferences)。
  2. 导航到 Appearance & Behavior > System Settings > Android SDK。
  3. 选择 SDK Tools 选项卡。
  4. 勾选 NDK (Side by side) 和 CMake 复选框。
  5. 点击 Apply 并等待安装完成。

2.2 创建支持 C/C++ 的新项目

创建一个新的支持原生代码的项目的过程类似于创建任何其他 Android Studio 项目,但有一个额外的步骤:

  1. 在向导的 Choose your project 部分,选择 Native C++ 项目类型。
  2. 点击 Next。
  3. 填写向导下一部分中的所有其他字段。
  4. 点击 Next。
  5. 在向导的 Customize C++ Support 部分,您可以使用 C++ Standard 字段自定义项目。
  6. 使用下拉列表选择要使用的 C++ 标准化版本。选择 Toolchain Default 会使用默认的 CMake 设置。
  7. 点击 Finish。

Android Studio 完成新项目的创建后,从 IDE 左侧打开 Project 窗格,并从菜单中选择 Android 视图。如图 1 所示,Android Studio 添加了 cpp 组。

注意:此视图并不反映磁盘上实际的文件层次结构,而是将相似的文件分组以简化项目导航。

cpp 组是您可以找到所有原生源代码文件、头文件、用于 CMake 或 ndk-build 的构建脚本以及作为项目一部分的预构建库的地方。对于新项目,Android Studio 会创建一个示例 C++ 源文件 native-lib.cpp,并将其放在应用模块的 src/main/cpp/ 目录中。此示例代码提供了一个简单的 C++ 函数 stringFromJNI(),它返回字符串 "Hello from C++"。

2.3 向现有项目添加 C/C++ 支持

如果您想向现有项目添加原生代码,请遵循以下步骤:

  1. 创建新的原生源文件并将其添加到您的 Android Studio 项目。
    • 如果您应用的 main 源集中还没有 cpp/ 目录,请按如下方式创建一个:
      • 在 IDE 左侧打开 Project 窗格,并从菜单中选择 Project 视图。
      • 导航到 your-module > src。
      • 右键点击 main 目录并选择 New > Directory。
      • 输入 cpp 作为目录名,然后点击 OK。
    • 右键点击 cpp/ 目录并选择 New > C/C++ Source File。
    • 为您的源文件输入一个名称,例如 native-lib。
    • 从 Type 菜单中,选择源文件的文件扩展名,例如 .cpp。
    • 点击 Edit File Types 可向菜单添加其他文件类型,例如 .cxx 或 .hxx。在弹出的 New File Extensions 对话框中,从 Source Extension 和 Header Extension 菜单中选择其他文件扩展名,然后点击 OK。
    • 要创建头文件,请选中 Create an associated header 复选框。
    • 点击 OK。
  2. 跳过此步骤(如果您已有原生代码或想要导入预构建的原生库)。
  3. 配置 CMake 以将您的原生源代码构建到库中。如果您要导入预构建库或平台库并与之链接,则需要此构建脚本。
    • 如果您已有的原生库已经有 CMakeLists.txt 构建脚本,或使用 ndk-build 并包含 Android.mk 构建脚本,请跳过此步骤。
  4. 通过提供 CMake 或 ndk-build 脚本文件的路径来配置 Gradle。Gradle 使用构建脚本将源代码导入到您的 Android Studio 项目并将您的原生库打包到应用中。

将新的 C/C++ 文件添加到项目后,您仍然需要配置 CMake 以将这些文件包含在您的原生库中。

3. 构建系统:CMake 与 ndk-build

Android Studio 支持两种主要的原生库构建系统:CMake 和 ndk-build。

3.1 CMake 配置

CMake 是一个跨平台的构建系统,被推荐用于大多数新项目。与 build.gradle 文件指示 Gradle 如何构建您的应用类似,CMake 和 ndk-build 需要一个构建脚本才能知道如何构建您的原生库。对于新项目,Android Studio 会创建一个 CMake 构建脚本 CMakeLists.txt,并将其放在您模块的根目录中。

一个基本的 CMakeLists.txt 文件如下所示:

cmake_minimum_required(VERSION 3.10.2) # 设置CMake的最低版本要求

project("ndkhelloworld") # 设置项目名称

add_library( # 声明要添加的库
             native-lib # 库的名称
             SHARED # 指定库为共享库(动态链接库)
             native-lib.cpp ) # 库的源文件

find_library( # 查找NDK提供的预编译库
              log-lib # 用于引用log库的变量名
              log ) # log库的名称

target_link_libraries( # 指定链接到目标库的其他库
                       native-lib # 目标库
                       ${log-lib} ) # 要链接的库,这里链接Android的log库

代码解析:

  • cmake_minimum_required(VERSION 3.10.2): 指定构建本项目所需 CMake 的最低版本。
  • project("ndkhelloworld"): 定义项目名称。
  • add_library(native-lib SHARED native-lib.cpp): 指示 CMake 从源文件 native-lib.cpp 构建一个名为 native-lib 的共享库 (SHARED)。
  • find_library(log-lib log): 定位 Android 特定的日志库,并将其路径存储在变量 log-lib 中。
  • target_link_libraries(native-lib ${log-lib}): 告诉 CMake 将原生库 native-lib 与日志库链接起来。这对于在原生代码中使用 __android_log_write 等日志函数是必需的。

3.2 ndk-build 简介

Android Studio 也支持 ndk-build,它可能比 CMake 更快,但仅支持 Android。目前不支持在同一模块中同时使用 CMake 和 ndk-build。ndk-build 使用 Android.mk 和 Application.mk 文件来配置构建。

3.3 配置 Gradle

您需要通过提供 CMake 或 ndk-build 脚本文件的路径来配置 Gradle。Gradle 使用构建脚本将源代码导入到您的 Android Studio 项目并将您的原生库打包到应用中。

在模块的 build.gradle 文件中进行配置:

android {
    ...
    defaultConfig {
        ...
        externalNativeBuild {
            cmake {
                cppFlags "" // 可以在这里传递CMake参数
            }
        }
    }
    externalNativeBuild {
        cmake {
            path "src/main/cpp/CMakeLists.txt" // CMake构建脚本的路径
        }
    }
}

4. JNI 编程基础

JNI (Java Native Interface) 是一个编程框架,它允许 Java 代码与用 C/C++ 编写的本地应用程序或库进行交互。通过 JNI,可以在 Java 中调用 C/C++ 代码,反之亦然。

4.1 编写原生函数

以下是一个简单的 JNI 函数示例,位于 native-lib.cpp 中:

#include <jni.h> // 提供JNI接口相关的类型和函数定义
#include <string> // 引入C++标准字符串库

// extern "C" 防止C++编译器对函数名进行修饰(mangling),确保C语言能正确链接。
// JNIEXPORT 和 JNICALL 是宏,用于指定函数的导出和调用约定,使其能被JVM识别和调用。
// jstring 是JNI中对应Java String的类型。
// 函数名格式必须为: Java_包名_类名_方法名
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_ndkhelloworld_MainActivity_stringFromJNI(
        JNIEnv* env, // JNIEnv指针,提供了大量JNI函数用于与Java环境交互
        jobject /* this */) { // 指向调用此原生方法的Java对象的引用。如果方法是static的,则这里是jclass。
    std::string hello = "Hello from C++!"; // 创建C++字符串
    return env->NewStringUTF(hello.c_str()); // 使用JNIEnv将C++字符串转换为Java字符串并返回
}

关键点说明:

  • 函数命名规则: JNI 函数名称必须遵循 Java_包名_类名_方法名 的格式(使用下划线代替包名和类名中的点)。这对于 JNI 正确链接 Java 原生方法和 C++ 实现至关重要。
  • extern "C": 指示 C++ 编译器以 C 语言的方式处理函数名(防止名称修饰),确保 JVM 能找到该函数。
  • JNIEnv*: 提供访问 JNI 环境的接口,包含所有用于与 Java 交互的函数(如创建 Java 对象、调用方法、抛出异常等)。
  • 参数类型: jstring, jobject 等是 JNI 定义的与 Java 类型对应的本地类型。

4.2 从 Java/Kotlin 调用原生函数

在您的 Java 或 Kotlin 代码中,您需要加载原生库并声明原生方法。

Kotlin 示例 (MainActivity.kt):

package com.example.ndkhelloworld

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.TextView

class MainActivity : AppCompatActivity() {

    companion object {
        // 在伴生对象的初始化块中加载原生库。
        // 库的名称应与CMakeLists.txt中add_library指定的名称一致(去掉lib前缀和.so后缀)。
        init {
            System.loadLibrary("native-lib")
        }
    }

    // 使用'external'关键字(Kotlin中对应Java的'native')声明一个原生方法。
    // 其实现由已加载的原生库提供。
    external fun stringFromJNI(): String

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 示例:调用原生方法并设置TextView的文本
        findViewById<TextView>(R.id.textView1).apply {
            text = stringFromJNI() // 调用原生方法并获取返回的字符串
        }
    }
}

Java 示例 (MainActivity.java):

public class MainActivity extends AppCompatActivity {

    // 加载原生库
    static {
        System.loadLibrary("native-lib");
    }

    // 声明原生方法
    public native String stringFromJNI();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TextView tv = findViewById(R.id.sample_text);
        tv.setText(stringFromJNI()); // 调用原生方法并设置文本
    }
}

5. 构建与运行

当您点击 Run 时,Android Studio 会构建并启动一个应用,在您的 Android 设备或模拟器上显示文本 "Hello from C++"。以下概述描述了构建和运行示例应用时发生的事件:

  1. Gradle 调用您的外部构建脚本 CMakeLists.txt。
  2. CMake 遵循构建脚本中的命令,将 C++ 源文件 native-lib.cpp 编译成一个共享对象库(在 Unix 系统中通常是 .so 文件),并将其命名为 libnative-lib.so。然后 Gradle 将其打包到应用中。
  3. 在运行时,应用的 MainActivity 使用 System.loadLibrary() 加载原生库。现在,库的原生函数 stringFromJNI() 对应用可用。
  4. MainActivity.onCreate() 调用 stringFromJNI(),它返回 "Hello from C++" 并用它来更新 TextView。

5.1 验证 APK 中的原生库

要验证 Gradle 是否将原生库打包在应用中,请使用 APK Analyzer:

  1. 选择 Build > Build Bundle(s) / APK(s) > Build APK(s)。
  2. 选择 Build > Analyze APK。
  3. 从 app/build/outputs/ 目录中选择 APK 或 AAB,然后点击 OK。
  4. 如图 2 所示,您可以在 APK Analyzer 窗口中的 lib/<ABI>/ 下看到 libnative-lib.so。

6. 常见构建错误与解决方案

在构建 Android 原生接口时遇到错误,开发者通常在诊断和解决问题方面面临挑战。这些错误通常源于 NDK 路径配置错误、不兼容的 ABI 或错误的 CMake/ndk-build 配置。

6.1 NDK 路径与工具链错误

错误示例: "no NDK arm-linux-androideabi-gcc on $PATH at (eval 13)"

此错误源于开发环境中 Android NDK (Native Development Kit) 路径配置不当。当尝试为 Android 构建原生代码而系统 PATH 变量中无法访问必要的工具链二进制文件时,通常会发生此错误。

解决方案:

  1. 验证 NDK 安装:NDK 应通过 Android Studio 的 SDK Manager 下载或直接从 Google 的 NDK 存档下载。安装后,找到 NDK 目录,通常位于 $ANDROID_SDK_ROOT/ndk/。
  2. 传统 GCC 工具链支持:对于传统 GCC 工具链支持,请确保使用低于 r18 的 NDK 版本。
  3. 将工具链二进制文件添加到 PATH(Linux/macOS 示例):
    export NDK_HOME=$ANDROID_SDK_ROOT/ndk/
    export PATH=$PATH:$NDK_HOME/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/bin
  4. 如果使用 Gradle 构建,在 local.properties 中配置 NDK 位置:
    ndk.dir=/path/to/ndk
  5. 迁移到 Clang:现代 NDK 版本(r18+)弃用了 GCC,转而使用 Clang。官方文档指出:"NDK r18 是最后一个支持 GCC 的版本。使用 GCC 的项目必须迁移到 Clang 或使用旧的 NDK 版本"。推荐的方法是升级到使用 NDK r21+ 的 Clang,它提供了更好的性能和标准合规性。

6.2 NDK 版本冲突与 ABI 过滤

错误示例: Execution failed for task ':app:externalNativeBuildDebug'. > Build command failed. Error while executing process ...

这是因为较新的 Gradle 版本执行更严格的 NDK 验证。解决方案涉及同步 local.properties 和 build.gradle 中的 NDK 版本:

在 local.properties 中:

ndk.dir=/path/to/ndk/21.4.7075529

在 build.gradle 中:

android {
    ndkVersion "21.4.7075529" // 确保此版本与local.properties中的路径匹配
    externalNativeBuild {
        cmake {
            path "src/main/cpp/CMakeLists.txt"
        }
    }
}

另一个常见问题涉及丢失的 ABI 过滤器,这会导致 APK 膨胀或构建失败。在 build.gradle 中明确指定 ABI 可以防止此问题:

android {
    defaultConfig {
        ndk {
            // 只包含所需的ABI架构,减少APK大小
            abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
        }
    }
}

6.3 Gradle 与 Java 版本兼容性问题

错误示例: Your build is currently configured to use incompatible Java 21.0.4 and Gradle 8.2. Cannot sync

此错误表明当前使用的 Gradle 版本 (8.2) 和 Java 版本 (21.0.4) 不兼容。Gradle 8.2 不支持 Java 21,而是支持 Java 17 或更低版本。Gradle 8.5 及以上版本才支持 Java 21。

解决方案:

  1. 升级 Gradle 版本(推荐):
    • 在项目根目录下的 gradle-wrapper.properties 文件中,修改 distributionUrl:
      distributionUrl=https://services.gradle.org/distributions/gradle-8.9-bin.zip
    • 或者在 Android Studio 设置中修改 Gradle 版本(File > Settings > Build, Execution, Deployment > Build Tools > Gradle)。
  2. 降低 Java 版本:
    • 在 Android Studio 中修改项目的 JDK 设置(File > Project Structure > SDK Location),选择一个 Java 17 或更低版本的 JDK。
    • 或者在项目根目录下的 gradle.properties 文件中添加:
      org.gradle.java.home=/path/to/jdk-17

Gradle 与 Java 版本兼容性表:

Gradle 版本支持的 Java 版本
8.0 - 8.4Java 8 - 17
8.5 - 8.9Java 8 - 21
9.0+Java 17 - 21

6.4 插件依赖解析失败

错误示例: Gradle Refresh Failed - Could not find com.android.tools.build:gradle:2.2.0-alpha6

当 Gradle 在配置的存储库中找不到指定的插件版本时,通常会发生此错误。较旧的插件版本(如 2.2.0-alpha6)可能不再托管在 Google 的 Maven 存储库中,导致解析失败。

解决方案:

  1. 更新 Gradle 插件版本:在项目级的 build.gradle 文件中,引用受支持的版本:
    dependencies {
        classpath 'com.android.tools.build:gradle:8.2.2' // 更新为稳定的版本
    }
    (始终在 https://developer.android.com/studio/releases/gradle-plugin 上验证最新版本。)
  2. 验证存储库配置:确保 Google 的 Maven 存储库包含在 settings.gradle 或 build.gradle 文件中:
    dependencyResolutionManagement {
        repositories {
            google() // Android插件的主要仓库
            mavenCentral()
        }
    }

7. 高级主题与最佳实践

7.1 添加额外的 C/C++ 源文件

要向项目添加新的 C/C++ 源文件,请按以下步骤操作:

  1. 如果应用的 main 源集中还没有 cpp/ 目录,请按如下方式创建一个:
    • 在 IDE 左侧打开 Project 窗格,并从菜单中选择 Project 视图。
    • 导航到 your-module > src。
    • 右键点击 main 目录并选择 New > Directory。
    • 输入 cpp 作为目录名,然后点击 OK。
  2. 右键点击 cpp/ 目录并选择 New > C/C++ Source File。
  3. 输入源文件的名称,例如 my-new-lib。
  4. 从 Type 菜单中,选择源文件的文件扩展名,例如 .cpp。
  5. 点击 OK。

添加新的 C/C++ 文件后,您需要更新 CMakeLists.txt 文件以将其包含在原生库的构建中:

add_library( native-lib SHARED 
             native-lib.cpp 
             my-new-lib.cpp ) # 添加新的源文件

# 如果新文件需要额外的头文件搜索路径
include_directories(src/main/cpp/include/)

7.2 优化与调试

使用 NDK 调试工具:Android Studio 提供了 NDK 调试工具,帮助您在调试 C/C++ 代码时更有效地发现和解决问题。您可以在 Run > Debug 菜单中选择 Attach debugger to Android process 来启动调试。

性能优化建议:

  • 避免频繁的 JNI 调用:每次 JNI 调用都需要进行上下文切换,这可能会影响性能。尽量在原生侧完成批量操作,而不是多次往返于 Java 和原生代码之间。
  • 高效数据传递:对于大量数据,考虑使用直接字节缓冲区(ByteBuffer.allocateDirect())或使用 Parcelable 机制,而不是复制数据。
  • 内存管理:C/C++ 代码中的内存管理更为复杂。确保正确分配和释放内存,避免内存泄漏。使用 Android Studio 的内存分析器来帮助检测原生内存泄漏。

内存管理:C/C++ 代码中的内存管理更为复杂,确保您正确地分配和释放内存,以避免内存泄漏和其他问题。

8. 总结

通过本指南,您应该已经了解了如何在 Android Studio 项目中集成 C++ 代码,配置 CMake 构建脚本,编写 JNI 函数,并从 Java/Kotlin 调用它们,以及解决常见的构建问题。

🎯 核心步骤回顾:

  1. 环境配置:安装 NDK、CMake 和 LLDB。
  2. 项目设置:创建 cpp 目录,编写 C++ 源文件和 CMakeLists.txt。
  3. Gradle 配置:在 build.gradle 中指定 CMake 路径和 NDK 版本。
  4. JNI 编程:遵循命名规则编写原生函数,并在 Java/Kotlin 中加载库和声明原生方法。
  5. 构建与调试:使用 APK Analyzer 验证库是否打包,利用 LLDB 调试原生代码。
  6. 问题排查:注意 NDK 版本、Gradle 插件版本、Java 版本之间的兼容性,并正确配置 ABI 过滤器和存储库。
最后更新: 2025/9/18 18:20