目 录CONTENT

文章目录

Android NDK开发入门详解

小王同学
2024-10-10 / 0 评论 / 1 点赞 / 83 阅读 / 0 字

Android NDK开发入门详解

打工人打工魂打工都是人上人

打工一時餓不死不打馬上會餓死

大家好这里是小王同学,最近在搞图片压缩,兜兜转转还得是c++底层压缩牛。所以想入门一下NDK开发,之前一直没了解过,今天我们一起来学一下吧!

入门 Android NDK 开发需要掌握以下知识和技能:

  1. 基础知识

    • C/C++ 编程语言:
    • 掌握 C/C++ 的语法和内存管理,理解指针、引用、动态内存分配和数组等基本概念。
    • 熟悉 C++ 的面向对象编程(OOP)特性,如类、继承、多态、模板等。
    • Java 和 JNI(Java Native Interface):
    • 了解 Java 编程语言和 Android Java API。Android NDK 中,通常会使用 JNI 进行 Java 与 C/C++ 代码的互相调用,了解 JNI 是关键。
    • 掌握 JNI 基础知识,包括 Java 与 C/C++ 数据类型的转换、调用本地方法、异常处理、访问 Java 对象和数组等。

  2. Android NDK 基础

    • 安装和配置 NDK:
    • 学习如何在 Android Studio 中安装和配置 NDK 环境。Android Studio 自带 NDK 支持,可以通过 SDK Manager 直接安装。
    • 熟悉 Android NDK 的基本目录结构和工具,如 ndk-build、CMake 等构建工具。
    • CMake 和 ndk-build:
    • 了解如何使用 CMakeLists.txt 来定义项目构建配置,并将其集成到 Android Studio 中。
    • 掌握 NDK 的另一构建工具 ndk-build 的基础用法和 Android.mk 文件的编写。
    • 学习如何编写 CMakeLists.txt 文件,配置编译选项、链接库、编译源文件等。
    • 编写简单的 JNI 示例:
    • 从一个简单的 JNI 示例开始,学习如何使用 NDK 编写一个基本的 C/C++ 函数,并通过 JNI 将其暴露给 Java 层调用。
    • 理解 System.loadLibrary()、native-lib.cpp 的使用以及与 Java 方法的互操作。

NDK 常用功能

• 性能优化:
• 掌握如何使用 NDK 提升性能,特别是在需要处理高性能计算、图形处理、多媒体处理等情况下。
• 学习如何将 C/C++ 代码与 Android 应用无缝集成,尤其是针对 CPU、内存密集型任务。
• 调用 C++ 库:
• 学习如何在 NDK 项目中使用外部的 C/C++ 库,掌握如何在 CMakeLists.txt 中链接外部库(如 OpenCV、FFmpeg)。
• 理解静态库和动态库的区别,学习如何在 NDK 项目中使用预编译的库或自行编译第三方库。
• NDK 调试与错误排查:
• 了解如何使用 Android Studio 和 LLDB 进行 NDK 项目的调试,学习如何设置断点、查看内存、检查线程等。
• 掌握 NDK 中常见的调试工具,如 ndk-stack,用于分析 native 崩溃时生成的堆栈跟踪。
• 内存管理与优化:
• 理解 Android NDK 中内存管理的重要性,尤其是与 Java 的垃圾回收机制(GC)的配合。学习如何避免内存泄漏、使用本地内存分配函数(如 malloc/free 或 new/delete)。
• 学习 C++ 智能指针(std::shared_ptr, std::unique_ptr)的使用来管理内存。

NDK与JNI的关系

说到NDK开发,我们不得不提JNI,那么他俩到底是什么关系呢?

下面让我们一探究竟!

NDK (Native Development Kit)

NDK 其实就是一个工具集,帮助我们在 Android 项目中使用原生代码(主要是 C 和 C++)。NDK 提供了编译器、构建工具和一些用于与 Android 系统交互的原生库(如 OpenGL ES、音频处理、图像处理等)。

主要特点如下:

用于编写高性能的代码,尤其适用于需要大量计算的任务,例如游戏开发、视频处理、音频处理等。
编程语言主要是 C/C++,NDK 提供了很多与 Android 系统交互的原生库,如处理音频、视频,进行硬件访问等。
通过 NDK 工具链,将 C/C++ 源代码编译为 Android 支持的 .so 动态库。

JNI (Java Native Interface)

JNI 是一个接口,允许 Java 和原生代码之间相互调用。通过 JNI,开发者可以从 Java 调用本地(C/C++)代码,或者从本地代码调用 Java 代码。

主要特点如下:

用于桥接 Java 与本地代码的通信。NDK 开发通常依赖 JNI 来让 Java 代码与 C/C++ 代码交互。
JNI 本身是一个接口,使用 Java 编写的代码可以通过 JNI 调用 C/C++(或其他原生语言)的代码。
例如在 Android 中,你可能需要在 Java 层进行界面操作,而在 C/C++ 层执行复杂的数学计算或处理多媒体数据,通过 JNI 可以在 Java 和 C/C++ 之间传递数据。

到这里大家都懂了吧,NDK就是底层开发提供的库,JNI相当于原生和C/C++沟通的桥梁!

下面我们一起创建一个NDK项目吧!

构建一个NDK项目

1.首先我们新建Project,选择Native c++

image-phzi.png

2.接着就是项目的各种配置,这个大家自己起名字就好,还有项目地址。

image-peiz.png

3.点击Finish

image-ymez.png

4.哈哈,报错了,那我们来看一下是什么错误。说是找不到我的NDK版本,那我们就去下载一下吧!

image-ndbc.png

Caused by: java.lang.RuntimeException: org.gradle.api.InvalidUserDataException: NDK not configured. Download it with SDK manager. Preferred NDK version is '25.1.8937393'.

5.打开setting,找到Android SDK---> SDK Tools --->Show package Details(这个不打对勾看不见)

image-bosy.png

6.找到对应的版本进行下载就可以啦

image-juix.png

7.编译成功!!✅是不是很简单

image-ksfn.png

下面我们来介绍一下项目中的CMAKE文件的具体作用

为什么要用CMAKE

1. 跨平台构建支持

CMake 是一种跨平台的构建系统生成工具,可以生成适用于多种平台的构建文件,如 Unix Makefiles、Ninja、Visual Studio 项目等。它不局限于 Android(不是只有Android才有的文件),可以轻松移植到其他平台(例如 Linux、Windows、macOS),减少了编写针对不同平台的构建脚本的复杂度。

对于 Android NDK 项目,CMake 文件简化了对不同架构(如 ARM、x86)的支持,能够自动生成合适的构建配置。

2. 便捷的依赖管理

CMake 提供了强大的依赖管理功能,它能够自动找到并链接需要的库。例如,在 Android 中可以通过 target_link_libraries 命令轻松链接 Android 系统提供的库(如 logandroid 等),同时也可以配置第三方库的链接。相比手动编写复杂的编译命令,CMake 更加简洁和安全。

3. 自动处理复杂的构建配置

C/C++ 的构建过程通常会涉及很多复杂的细节,包括:

  • 不同编译器和架构的支持:Android 设备有不同的 CPU 架构(如 ARM、x86),每个架构都需要不同的编译器配置,CMake 可以自动根据配置文件生成适合每个架构的编译选项。
  • 编译选项和优化:如设置 -std=c++11、编译器优化选项、警告处理等。CMake 能够灵活设置并应用到构建过程中。
  • 源文件管理:CMake 能够轻松处理大型项目中的多个源文件和目录,自动生成依赖关系并管理构建流程。
4. 与 Android Studio 集成良好

自 Android Studio 2.2 以来,CMake 已成为官方推荐的构建原生代码的工具之一。Android Studio 提供了对 CMake 的原生支持,允许开发者通过简单的配置在 Gradle 脚本中调用 CMake,从而无缝集成原生代码的编译过程。

Android Studio 的 CMake 支持简化了原生代码与 Java/Kotlin 代码的互操作,例如:

  • .so 文件集成到 APK 中。
  • 使用 JNI(Java Native Interface)与 Java/Kotlin 代码进行交互。
5. 维护性和可扩展性

使用 CMake 可以使项目构建更加模块化和易于维护。例如,如果项目未来需要添加新的源文件、库或目标架构,只需要简单修改 CMakeLists.txt 文件,而不需要手动修改大量的构建脚本。CMake 通过抽象编译过程的复杂性,使项目的可扩展性变得更高。

6. 方便调试和测试

CMake 支持多种调试和测试配置。通过配置选项,开发者可以生成带有调试信息的构建,并通过 Android Studio 或其他 IDE 进行调试。同时,CMake 还支持多种测试工具的集成,可以为项目添加自动化测试。

C++ 库的完整编译和链接过程

了解 C++ 库的完整编译和链接过程对于理解 CMake 在其中扮演的角色非常有帮助。

学过C和C++的都知道,C++ 库的编译和链接过程涉及多个步骤,从源代码的编译到生成最终可执行文件或库。这个过程可以分为两个主要阶段:编译阶段链接阶段。下面是完整的 C++ 库编译和链接过程的详细解释:

1. 预处理阶段(Preprocessing)

预处理是编译的第一个阶段,负责处理代码中的预处理指令,如 #include#define#ifdef 等。

  • 主要任务
    • 将所有的头文件通过 #include 指令展开,替换为其实际内容。
    • 宏替换:将所有的宏定义用相应的值替换。
    • 条件编译:根据 #ifdef 等指令,确定哪些部分代码会被编译。
  • 生成结果:经过预处理的 C++ 源文件,不再包含任何预处理指令,扩展名通常是 .i。

命令:

g++ -E source.cpp -o source.i

2. 编译阶段(Compilation)

编译阶段将预处理后的代码转换为汇编代码,这是将高级语言转换为机器语言的中间步骤。

  • 主要任务
    • 语法分析、语义分析:检查代码的语法是否正确,语义是否合理。
    • 生成汇编代码:将合法的 C++ 代码转为汇编代码。
  • 生成结果:汇编代码文件,通常扩展名为 .s。

命令:

g++ -S source.i -o source.s

3. 汇编阶段(Assembly)

汇编阶段将汇编代码文件转换为机器代码,也就是目标文件(object file)。

  • 主要任务
    • 汇编器将汇编代码转换为机器指令。
    • 将数据部分和代码部分分别处理,生成对应的二进制格式。
  • 生成结果:目标文件(object file),扩展名通常为 .o 或 .obj。

命令:

g++ -c source.s -o source.o

4. 链接阶段(Linking)

链接阶段是生成最终可执行文件或库的关键步骤。目标文件中包含了编译后的代码,但可能仍包含未定义的符号,如外部库或其他模块的函数和变量。

  • 主要任务
    • 符号解析:将所有目标文件中的符号(如函数、全局变量)与外部库或其他目标文件中的定义进行匹配。
    • 重定位:确定所有代码和数据在最终可执行文件中的实际地址。
    • 生成可执行文件或库:根据链接选项,生成最终的 .exe 文件或 .so/.dll 动态库或 .a/.lib 静态库。

命令:

g++ source.o -o executable

在链接过程中,有两种主要的库可以链接:

  • 静态库(Static Library)
    • 静态库在链接时将整个库的代码嵌入到可执行文件中,因此生成的可执行文件不依赖外部库。

    • 扩展名.a(Linux/Unix)、.lib(Windows)。

    • 命令

       g++ source.o -L/path/to/library -lname_of_library -o executable
      
      
    • 其中,-L 指定库的路径,-l 后面跟库的名字(去掉前缀 lib 和后缀 .a)。

  • 动态库(Shared Library / Dynamic Library)
    • 动态库的代码不嵌入可执行文件中,而是在运行时由操作系统加载。可执行文件只包含对库中符号的引用。

    • 扩展名.so(Linux/Unix)、.dll(Windows)。

    • 命令

      g++ source.o -L/path/to/library -lname_of_library -o executable
      
      
    • 运行时动态库会在程序启动时被加载。

5. 静态库和动态库的生成

5.1 生成静态库

静态库是多个 .o 文件的集合,可以使用 ar(archive)工具将这些目标文件打包成静态库。

  • 生成静态库的命令
ar rcs libmylib.a source1.o source2.o

5.2 生成动态库

动态库生成需要使用特定的编译选项 -shared 来生成 .so 文件(Linux)或 .dll 文件(Windows)。

  • 生成动态库的命令

    g++ -shared -o libmylib.so source1.o source2.o
    
    

6. 可选的优化步骤

在编译和链接阶段,编译器会根据给定的优化选项对代码进行优化:

  • -O1-O2-O3:优化级别,从低到高。
  • -g:生成调试信息,以便后续调试。
  • -march=native:为本地 CPU 架构优化。

命令示例:

g++ -O2 -march=native source.cpp -o optimized_executable

完整编译与链接流程

假设你有多个 C++ 源文件,并想要编译成一个动态库 libmylib.so,流程如下:

# Step 1: 编译各个源文件
g++ -c source1.cpp -o source1.o
g++ -c source2.cpp -o source2.o

# Step 2: 链接并生成动态库
g++ -shared -o libmylib.so source1.o source2.o

g++:GCC 编译器的 C++ 前端,用来编译 C++ 程序。

-c:表示 编译(Compile源文件,但不进行链接操作。也就是说,这个命令会将 .cpp 文件编译成目标文件(.o 文件),但是不会生成最终的可执行文件或库。这个步骤的结果是一个 .o 文件,包含了机器码。

-o source1.o-o source2.o

  • -o 选项指定输出文件的名称。在这里,-o 后面跟的是生成的目标文件名称。
  • source1.o 和 source2.o 是输出的目标文件,分别对应 source1.cpp 和 source2.cpp。

总结:

  1. 预处理:展开头文件和宏替换。
  2. 编译:将 C++ 源代码转换为汇编代码。
  3. 汇编:将汇编代码转换为机器指令,生成目标文件。
  4. 链接:将目标文件和库链接在一起,生成最终的可执行文件或库。

CMakeLists.txt 配置文件说明

上面我们其实已经创建了一个Native Project,并且自动生成了CMakeLists文件。

下面我们一起来看一下配置文件都是干什么的。

image-fxbn.png

1. cmake_minimum_required(VERSION 3.22.1)

  • 作用:指定最低版本的 CMake,这个命令声明了当前 CMakeLists.txt 文件需要的最低 CMake 版本。我这里自动指定为 3.22.1。确保在使用的 CMake 版本过低时,能够及时报错,避免不支持的语法或功能。

2. project("mynativeproject")

  • 作用:定义当前项目的名称,这里设置项目名称为 mynativeproject。
  • 作用范围:项目名称主要用于标识和组织项目,CMake 会用到这个名称来为编译目标命名。虽然早期版本的 CMake 中不要求设置项目名称,但现在谷歌官方版本中推荐设置。

3. add_library($ SHARED native-lib.cpp)

  • 作用:定义一个名称为 ${CMAKE_PROJECT_NAME} 的库(这里为 mynativeproject),并指定该库的类型为 SHARED,即动态库。
    • SHARED 表示生成动态库(.so 文件)。
    • native-lib.cpp 是该库的源代码文件,编译时会将该 C++ 文件编译到动态库中。
  • 注意:${CMAKE_PROJECT_NAME} 是一个变量,它代表项目名称 mynativeproject,因此生成的动态库名称为 libmynativeproject.so。
  • 作用:将库 mynativeproject(即 native-lib 编译出的动态库)与系统库进行链接。

    • android:Android 平台的原生支持库,提供 Android 相关的原生功能。
    • log:Android 的日志库,用于调用 __android_log_print() 等函数进行日志记录。

这样,当你运行项目时,Android Studio 就会使用 CMake 构建你的 native-lib.cpp 代码,并将其集成到你的 Android 应用中。当你在 Java 代码中调用 stringFromJNI 函数时,它就会返回 "Hello from C++" 这个字符串。

image-mifq.png

Android 的 Gradle 和 CMake 集成

在 Android 项目中,构建系统使用的是 Gradle。Gradle 是一个开源的构建自动化系统,它能够处理多种语言的项目,如 Java、C++、Python 等。然而,作为一个通用的构建系统,Gradle 对于 C++ 的支持并不如 CMake 全面,所以我们在 Android 项目中通常会结合用 Gradle 和 CMake。

当我们在创建一个新的 Android 项目,选择了 "Include C++ support" 后,Android Studio 会为我们自动生成一份 build.gradle 文件。这份文件定义了 Android 项目的构建配置,比如项目的目录结构,依赖关系,以及构建脚本的位置等。

在 build.gradle 文件中,Android Studio 自动生成的 CMake 配置部分长这样:


externalNativeBuild {
        cmake {
            path = file("src/main/cpp/CMakeLists.txt")
            version = "3.22.1"
        }
    }

这里 externalNativeBuild 部分就是指定了我们构建 Native 代码的方式和配置。我们设置 cmake 块中的 path 为 "CMakeLists.txt",指定了 CMake 构建脚本的位置。version 指定了我们使用的 CMake 的版本。

于是,当我们构建 Android 项目时,Gradle 就会运行 CMake 来构建我们的 native 代码。

Gradle和CMake如何配合

上述配置告诉 Gradle 这个项目用 CMake 来构建本地代码,并指示 Gradle 如何找到 CMakeLists.txt 文件。

当你运行构建过程时,例如运行 ./gradlew assembleDebug,以下是发生的事情:

  1. Gradle 会调用 CMake 以编译你在 CMakeLists.txt 中定义的本地代码。
  2. CMake 产生的 .so 文件被放在 app/build/intermediates/cmake/debug/obj 目录中。这个目录下面,会根据不同的架构,有不同的子目录,例如 armeabi-v7a, arm64-v8a, x86, 等等。
  3. 然后,Android Gradle 插件会在最终创建 APK 时,查找这些目录,并自动把 .so 文件打包到 APK 的 lib/ 目录。

实际上,.so 文件并不需要你显示地放到 jniLibs 目录下,因为 Gradle 插件会自动把它们从 app/build/intermediates/cmake/debug/obj 目录中取出并打包到 APK。

处理不同的Android架构和平台版本

Android 支持的 ABI(应用二进制接口)

Android 支持的 ABI(架构)包括:

  • armeabi-v7a:32 位 ARM 架构
  • arm64-v8a:64 位 ARM 架构
  • x86:32 位 x86 架构
  • x86_64:64 位 x86 架构

不同设备会基于不同的处理器架构,因此,你需要为每种架构生成特定的库。

CMake 文件中指定 ABI 架构

CMakeLists.txt 中,你可以设置支持的 ABI 以确保本地代码针对不同的架构进行编译。

# 设置 ABI 架构的支持选项
set(CMAKE_ANDROID_ARCH_ABI arm64-v8a)  # 这里可以是 arm64-v8a、armeabi-v7a、x86 或 x86_64

Gradle 配置中处理不同架构

在 build.gradle 文件中,你可以通过 ndk.abiFilters 来控制支持的架构。例如:

android {
    defaultConfig {
        ndk {
            abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'  // 设置支持的架构
        }
    }
}

对于不同的 ABI,可能会需要不同的 CMake 构建设置。

这时,我们可以在 CMakeLists.txt 文件利用 ANDROID_ABI 这个变量来区别处理。例如:

if(ANDROID_ABI STREQUAL "armeabi-v7a")
    # 对于 armeabi-v7a 的特殊设置
endif()

if(ANDROID_ABI STREQUAL "x86_64")
    # 对于 x86_64 的特殊设置
endif()

上面的代码中,ANDROID_ABI 变量的值由 Gradle 在运行 CMake 命令时自动传入。变量的值就是当前正在构建的 ABI 类型。我们可以根据不同的 ANDROID_ABI 值进行不同的处理,例如添加不同的编译选项,链接不同的预编译的库等等。

如此,我们就能方便地在一个项目中管理和构建针对不同安卓设备的原生代码了。

C++ 标准和编译选项

在编写 C++ 代码时,我们通常要根据具体的项目需求来选择合适的 C++ 标准版本(如 C++11,C++14,C++17 等)。此外,我们也可能会需要添加一些特定的编译选项来控制编译器的行为。

在 CMake 中,我们可以通过 target_compile_features 和 target_compile_options 这两个命令来进行设置。

  1. target_compile_features 命令可以用来设置 C++ 标准。例如:
target_compile_features(your-lib PUBLIC cxx_std_14)

这行命令设定我们的库 your-lib 采用 C++14 标准。

  1. target_compile_options 命令可以用来添加编译选项。例如:
target_compile_options(your-lib PRIVATE -Wall)

这行命令添加了一个 -Wall 选项,它会让编译器输出所有类型的警告信息。

有时候,我们可能还想要根据不同的编译环境来设置不同的编译选项。我们可以这样:

if (CMAKE_BUILD_TYPE MATCHES Debug)
    target_compile_options(your-lib PRIVATE -Wall -g)
else()
    target_compile_options(your-lib PRIVATE -Wall -O3)
endif()

  1. if (CMAKE_BUILD_TYPE MATCHES Debug)
  • 这是一个条件判断语句,它检查当前构建类型是否为 Debug。
  • CMAKE_BUILD_TYPE 是一个预定义的 CMake 变量,用来指定构建类型。
  • MATCHES 表示在比较 CMAKE_BUILD_TYPE 变量的值和 Debug 字符串,如果它们匹配,则条件为真。
  1. target_compile_options(your-lib PRIVATE -Wall -g)
  • 这行代码在构建类型为 Debug 时执行。
  • target_compile_options 用来为目标库(your-lib)指定编译选项。
    • your-lib:这里是目标库的名字,可以替换为你实际的库名称。
    • PRIVATE:指的是这些编译选项仅对该目标库有效,而不会传递给依赖它的其他库或应用。
    • -Wall:这个编译选项开启所有常见的编译器警告,帮助你发现代码中的潜在问题。
    • -g:这个编译选项开启调试信息生成,通常用于 Debug 构建中,方便调试。

3.target_compile_options(your-lib PRIVATE -Wall -O3)

  • 这行代码在构建类型为非 Debug(通常为 Release)时执行。
  • 它同样为 your-lib 添加编译选项:
    • -Wall:依旧是开启所有常见的编译器警告。
    • -O3:这是一个优化选项,表示进行三级优化,最大化性能。这通常用于 Release 构建中,提升应用程序的运行速度。

有不懂的或者有疑问的欢迎留言~

1
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin

评论区