二、CMake构建详解

741 阅读23分钟

本篇文章主要围绕CMake构建项目来讲解。
上一章节已经基本掌握JNI这门技术了,所以接下来要详细的讲解 Android中CMake构建执行流程、手动执行CMake构建、CMake语法、库生成及它们之间的区别。

学习资料

📎Cmake语法指令.pdf📎CMake中文手册.pdf 📎学习文档.pdf

介绍

  • CMake是目前Android Studio中,唯一能编译FFmpeg的方式。
  • Android Studio中现在编译NDK都是通过CMake来编译的;在很早以前则是通过makefile来编译的。
  • CMake它不是直接执行编译过程,而是生成适用于不同构建系统ABI(例如Make、Ninja、Visual Studio等)的配置文件。CMake允许开发者在不同的平台上使用相同的构建配置,而不必手动编写和维护多个平台的构建文件。

GCC手动编译C语言生成可执文件

C语言手动编译可执行文件GCC指令:

在类Unix/Linux系统中,没有明确规定可执行文件的扩展名。可执行文件通常没有扩展名,或者可能使用一些约定的扩展名,如 .out、.elf(Executable and Linkable Format)等。例如,一个名为 myprogram.out 的文件可能是一个可执行文件。

一步到位编译:gcc main.c -o main

预处理 -E (生成.i文件) 编译 -S (生成.s文件) 汇编转换 -c (生成.o文件) 连接 -o (生成无后缀可执行文件)

  1. **预处理:
    **gcc -E main.c -o **main.i
    **-E **:仅执行编译预处理
    **-o :将结果输出并指定输出文件的文件名
  2. **编译为汇编代码:
    **gcc -S main.i -o **main.s
    **-S :将C代码转换为汇编代码
  3. **汇编转换:
    **gcc -c main.c -o **main.o
    **-c :仅执行编译操作,不进行连接操作
  4. **连接:
    **gcc main.o -o main

首先熟悉一下GCC编译C语言并执行的流程。

环境:

centos、g++

安装g++:

yum install gcc-c++

  1. 创建C语言文件

#cd /usr/software/makedemo

#vim main.c

#ls

main.c

#include <stdio.h>
int main() {
    printf("main, World!\n");
    return 0;
}
  1. 预处理 (仅执行编译预处理,比如替换宏代码):

#gcc -E main.c -o main.i

#ls

main.c main.i

  1. 编译 为汇编代码(将C代码转换为汇编代码):

#gcc -S main.i -o main.s

#ls

main.c main.i main.s

  1. 汇编转换 (将汇编代码转换为机器指令,仅执行编译操作,不进行连接操作):

gcc -c main.c -o main.o

#ls

main.c main.i main.s main.o

  1. 连接

#gcc main.o -o main(将汇编编译后的main.o文件与main.s进行连接,生成main)

#ls

main main.c main.i main.s main.o

  1. 执行main:

#./main

main, World!

注意:也可以选择一步到位编译" gcc main.c -o main ",该命令可以直接完成步骤2、3、4、5的操作。

MakeFile与CMake

了解完借助gcc工具手动编译C代码的流程后,我们明白了手动执行编译命令所带来的复杂程度太过复杂了。
所以后面推出了MakeFile作为辅助工具来帮助编译。(MakeFile是个很强大但比较复杂的工具。)

但是MakeFile的使用还是太繁琐了,所以后面又推出了CMake。

MakeFile和Cmake的区别:

  • MakeFIle只支持Linux。
  • MakeFIle的参数配置比CMake多很多,需要配置很多编译脚本。
  • **CMake几乎支持所有平台。
    **CMake可以想象成傻瓜式照相机,他比MakeFIle更容易使用,并且适用范围广,Android Studio 2.3以后版本都是使用CMake。

MakeFIle类似eclipse.exe执行文件,其非常笨拙;所以之后出现了CMake,相当于Android开发中,从一开始使用eclipse编译器,更换为安卓专用的Android Studio,这种概念。

CMake编译过程

1.1 ABI 是什么

Abi架构介绍

ABI(应用程序二进制接口)是 Application Binary Interface 的缩写。

不同Android手机使用不同的CPU,因此支持不同的指令集。CPU与指令集的每种组合都有其自己的应用二进制界面(或 ABI)。ABI可以非常精确地定义应用的机器代码在运行时如何与系统交互。您必须为应用要使用的每个 CPU 架构指定 ABI。

典型的 ABI 包含以下信息:

  • 机器代码应使用的 CPU 指令集。
  • 运行时内存存储和加载的字节顺序。
  • 可执行二进制文件(例如程序和共享库)的格式,以及它们支持的内容类型。
  • 用于解析内容与系统之间数据的各种约定。这些约定包括对齐限制,以及系统如何使用堆栈和在调用函数时注册。
  • 运行时可用于机器代码的函数符号列表 - 通常来自非常具体的库集。

不同架构之间的兼容性

不同的CPU与指令集的每种组合都有定义的ABI(应用程序二进制接口),一般程序只有遵循这个接口规范才能在该CPU上运行,所以同样的程序代码为了兼容多个不同的CPU,需要为不同的ABI构建不同的库文件。当然,对于CPU来说,不同的架构并不意味着一定互不兼容。

  • armeabi设备只兼容armeabi;
  • armeabi-v7a设备兼容armeabi-v7a、armeabi;
  • arm64-v8a设备兼容arm64-v8a、armeabi-v7a、armeabi;
  • X86设备兼容X86、armeabi;
  • X86_64设备花呗X86_64、X86、armeabi;
  • mips64设备兼容mips64、mips;
  • mips只兼容mips;

根据以上的兼容总结,我们可以得到一些规律:

  • armeabi的so文件基本上是万金油,它能运行在除了mips和mips64的设备上,但在非armeabi设备上运行性能还是有损耗。
  • 64位的CPU架构总能 向下兼容 其对应的32位指令集,如:x86_64兼容X86,arm64_v8a兼容armeabi_v7a,mips64兼容mips。

补充:

  • 安卓设备基本是arm-v7a比较常见,华为设备则是arm-v8a常见,arm-v8a向下兼容arm-v7a。 百分之九十以上设备都是arm-v7aarm-v8a的。
  • abi类型与CPU芯片有关。
  • x86、x86_64则是Windows模拟器。
  • so库的体积一般都比较大;为了减少打包so库的体积,所以打包时通常指定只打包arm-v7a。

1.2 如何在 gardle 中配置输出指定的ABI类型

将 Gradle 关联到您的原生库 | Android Studio | Android Developers

默认情况下,cmake 会输出 4 种 ABI("armeabi-v7a" , "arm64-v8a", "x86", "x86_64"),如下所示:

我们也可以通过 abiFilters 来指定我们需要的 ABI:(Gradle插件版本4.1.1以上是如此,不同gradle插件版本可能配置命令不一样,以官网的为准)

abiFilters "armeabi", "armeabi-v7a" , "arm64-v8a", "x86", "x86_64", "mips", "mips64"

注意:在配置完abiFilters指定abi类型后,AS是不会自动更新重新生成so库的,需要在工具栏找到 build-> refresh linked c++ project 才会重新编译C/C++代码(并 Make Projects **)。
**或者删除.app下的build和.cxx的文件夹后重新编译。

1.3 build_command.txt 详解

新版本AS(我这边是2022.3.1 path 1)CMake编译后不会在目录中生成build_command.txt文件了,亲测AS4.1.1有**build_command.txt。
**所以本内容请选择性查看。

在旧版的AS中,CMake编译后,会在项目根目录:app->.cxx->debug->armeabi-v7a 目录下生成CMake编译过程产生的文件,其中里面有一个名为“build_command.txt ****”的配置文件,里面配置了CMake编译相关的配置信息,如下:

(Android Studio4.1.1版本生成):

                cmake的可执行文件在哪个路径 (绝对路径)
                Executable : A:\Android\Android-SDK\cmake\3.10.2.4988404\bin\cmake.exe
执行cmake时 携带的参数
arguments :
编译的源码放在哪个文件夹
-HB:\AndroidProjects\MyFecDemo\app\src\main\cpp
无效路径,临时目录,CMake编译完后该文件夹也会被删除。
-DCMAKE_FIND_ROOT_PATH=B:\AndroidProjects\MyFecDemo\app.cxx\cmake\debug\prefab\armeabi-v7a\prefab
-DCMAKE_BUILD_TYPE=Debug

导入系统的库 如liblog.so libjnigraphics.so
-DCMAKE_TOOLCHAIN_FILE=A:\Android\Android-SDK\ndk\21.1.6352462\build\cmake\android.toolchain.cmake
编译平台
-DANDROID_ABI=armeabi-v7a
NDK绝对路径
-DANDROID_NDK=A:\Android\Android-SDK\ndk\21.1.6352462
Android最低平台版本
-DANDROID_PLATFORM=android-16
当前编译 生成so的版本
-DCMAKE_ANDROID_ARCH_ABI=armeabi-v7a
Debug下的NDK绝对路径
-DCMAKE_ANDROID_NDK=A:\Android\Android-SDK\ndk\21.1.6352462
Debug下打开Cmake命令输出
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON
指定临时中间文件
-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=B:\AndroidProjects\MyFecDemo\app\build\intermediates\cmake\debug\obj\armeabi-v7a
指定语法解释器 ninja
-DCMAKE_MAKE_PROGRAM=A:\Android\Android-SDK\cmake\3.10.2.4988404\bin\ninja.exe
android 系统名称 Android
-DCMAKE_SYSTEM_NAME=Android
系统版本
-DCMAKE_SYSTEM_VERSION=16
最终生成的so库所在的地方
-BB:\AndroidProjects\MyFecDemo\app.cxx\cmake\debug\armeabi-v7a
-GNinja
jvmArgs : 

Build command args:

(网络参考):

cmake的可执行文件在那个路径 (绝对路径)
Executable :
C:\Users\Administrator\AppData\Local\Android\Sdk\cmake\3.10.2.4988404\bin\cmake.exe

执行cmake时 携带的参数
arguments :
编译的源码放在哪个文件夹
-HE:\maniu\NativeTest\app\src\main\cpp
无效路径
-DCMAKE_FIND_ROOT_PATH=E:\maniu\NativeTest\app.cxx\cmake\debug\prefab\armeabiv7a\prefab
-DCMAKE_BUILD_TYPE=Debug
-
导入系统的库 如liblog.so libjnigraphics.so
DCMAKE_TOOLCHAIN_FILE=C:\Users\Administrator\AppData\Local\Android\Sdk\ndk\21.0.
6113669\build\cmake\android.toolchain.cmake
编译平台
-DANDROID_ABI=armeabi-v7a
NDK绝对路径
-DANDROID_NDK=C:\Users\Administrator\AppData\Local\Android\Sdk\ndk\21.0.6113669
Android最低平台版本
-DANDROID_PLATFORM=android-23
当前编译 生成so的版本
-DCMAKE_ANDROID_ARCH_ABI=armeabi-v7a
Debug下的NDK绝对路径
-
DCMAKE_ANDROID_NDK=C:\Users\Administrator\AppData\Local\Android\Sdk\ndk\21.0.611
3669
Debug下打开Cmake命令输出
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON
指定临时中间文件
-
DCMAKE_LIBRARY_OUTPUT_DIRECTORY=E:\maniu\NativeTest\app\build\intermediates\cmak
e\debug\obj\armeabi-v7a
指定语法解释器 ninja
-
DCMAKE_MAKE_PROGRAM=C:\Users\Administrator\AppData\Local\Android\Sdk\cmake\3.10.
2.4988404\bin\ninja.exe
android 系统名称 Android
-DCMAKE_SYSTEM_NAME=Android
系统版本
-DCMAKE_SYSTEM_VERSION=23
最终生成的so库所在的地方
-BE:\maniu\NativeTest\app.cxx\cmake\debug\armeabi-v7a
-GNinja
jvmArgs :
Build command args:

注意:build_command.txt 在每个abi平台对应的目录下都会生成一个。

修改build_command.txt的配置

build_command.txt文件中存储了CMake编译所需的相关信息,如果说我们想修改某项配置的信息,比如修改 -DCMAKE_SYSTEM_VERSION 的值,那么可以在build文件中使用 arguments 来修改:

  1. 使用arguments来修改build_command.txt中的配置
externalNativeBuild {
    cmake {
        cppFlags ""
        argments "-DCMAKE_SYSTEM_VERSION=24" //修改系统版本为24
    }
}
  1. 删除之前生成的.cxx目录,然后重新生成该目录。
  2. 重新生成后可以看见,buil_command.txt中的该配置项被修改了。

此处简单了解下 arguments 进行修改操作即可。后期使用ffmpeg时会使用到arguments修改很多参数去优化。

arguments 可以修改 与 新增参数,且支持创建宏。

externalNativeBuild {
    cmake {
        cppFlags ""
        //修改DCMAKE_SYSTEM_VERSION系统版本号为18,修改DANROID_TOOLCHAIN使用clang进行编译。
        arguments "-DCMAKE_SYSTEM_VERSION=18","-DANROID_TOOLCHAIN=clang"
    }
    ndk {
        //...
    }
}

DANROID_TOOLCHAIN

CMake编译通常使用gcc和clang,Linux主流是使用gcc。
安卓移动端则是:NDK18之前都是使用gcc,NDK18及之后则是使用的clang来编译的。安卓高版本的OpenCV和Ffmpeg都是用clang编译的。

clang和gcc类似于Java的javac,用于编译C源码,编译成可执行文件。

clang的出现是作为gcc的替代品。

1.4 CMake编译过程

  1. 首先编写我们的C语言项目源代码。
  2. build.gradle配置了ndk的支持及相关信息,Android Studio会从中读取相关信息,是否为ndk项目,特别是CmakeLists.txt所在的位置。
  3. AS把从build.gradle中配置的ndk相关信息打包成命令,放置在生成的 .cxx目录,该目录下存储了Cmake编译所使用到的相关参数和指令
  4. AS访问.cpp目录,分别对目录中生成的各平台ABI文件夹中build_command.txt进行解析后执行CMake构建。
    首先从build_command.txt文件中拿到Cmake的地址,及读取build_command.txt内的其他参数后,执行CMake.exe并传入相关参数。
  5. CMake工具根据命令将指定的c源代码进行编译与构建,最终生成各abi对应的so包。
    生成的so包路径为:build\intermediates\cmake\debug\obj\arm64-v8a debug:对应版本(debug/release) arm64-v8a:对应的abi平台
    每个abi平台目录下会有各自的.so库。
  6. AS将CMake编译后生成的.so库拷贝到 build\intermediates\merged_native_libs\debug\out\lib\arm64-v8a 目录下。
  7. AS使用自带工具将.SO与我们的Java语言代码.dex进行打包,最终生成.apk文件。

到此CMake的执行过程就结束了。

示例:手动使用CMake编译.c源代码生成可执行文件

CMake可以编译的内容:

  1. 可执行文件:在Linux、windows、Mac平台中,大部分都是编译为可执行文件。
  2. 动态库、静态库:在Android中编译实现的。

编译语法:

cd source (CMakeLists.txt 所在目录)

cmake .

make

ubuntu 22.04配置国内镜像源: 阿里云/清华大学/中科大_ubuntu国内镜像源-CSDN博客

环境:

unbuntu 22.04.3

cmake 2.8.12

gcc

安装CMake:

apt-get install cmake

apt-get install gcc

apt-get install vim

创建项目:

#cd /usr/software/newmakedemo

#ls

(无文件)

#vim main.c

#include <stdio.h>

int main() {
    printf("main, World!\n");
    return 0;
}

创建CMakeLists.txt:

#ls

main.c

#cmake -version

cmake version 3.22.1

#vim CMakeLists.txt

cmake_minimum_required(VERSION 3.22.1)
project(Lxunua C)

set(CMAKE_C_STANDARD 99)

add_executable(Lxunua main.c)

CMake构建:

完成C代码的编写和CMakeLists文件的配置后,就可以使用CMake进行构建了。

#cmake .

#ls

CMakeCache.txt CMakeFiles cmake_install.cmake CmakeLists.txt main.c Makefile

上述操作生成了编译当前平台可执行文件所需要的构建信息与配置文件。

make生成:

通过make指令对cmake生成的构建信息进行生成,得到可执行文件

#make

#ls

CMakeCache.txt CMakeFiles cmake_install.cmake CmakeLists.txt main.c Makefile Lxunua

执行:

#./Lxunua

"main, World!"

总结:

  1. 由此可知,我们手动编译CMake时,只需要提供.c源码和CMakelists.txt。
  2. CMkake会将源码编译后生成可执行文件。
  3. CMake编译后生产了MakeFile文件,所以可见CMake的底层原理还是MakeFile。
  4. CMake它不是直接执行编译过程,而是生成适用于不同构建系统(例如Make、Ninja、Visual Studio等)的配置文件。CMake允许开发者在不同的平台上使用相同的构建配置,而不必手动编写和维护多个平台的构建文件。
  5. make是一个实际执行编译过程的构建工具。它读取Makefile文件,其中包含了源代码文件、依赖关系和编译规则,然后执行这些规则以生成目标文件。

Linux中的make指令和CMake有什么关系吗?

make和CMake都是用于构建软件项目的工具,但它们有不同的作用和功能。

make是一个在Unix和类Unix系统上用于构建程序的标准工具。它通过读取一个名为Makefile的文件来确定如何构建项目。Makefile中包含了关于如何编译源文件、链接目标文件以及执行其他构建任务的规则和命令。

CMake是一个跨平台的构建系统生成工具,它可以生成适用于不同构建系统的配置文件,比如make、Ninja、Visual Studio等。它使用一个名为CMakeLists.txt的文件来描述项目的配置和构建过程,然后根据这个文件生成相应的构建系统所需的文件,比如Makefile。

因此,make和CMake之间的关系是:CMake可以生成Makefile文件,然后使用make命令来执行构建过程。CMake提供了更高级的抽象和灵活性,可以生成不同构建系统所需的配置文件,而make只是其中一个具体的构建系统。

常用的CMake命令

CMakeLists.txt脚本中常用的CMake命令

注:CMake语法中关键字不区分大小写

命令含义
cmake_minimum_required指定需要CMAKE的最小版本
include_directories(var item,var item2...)指定 "native代码/so库" 的**头文件路径 **(比如Ffmpeg的导入则是导入整个Ffmpeg项目文件)
add_library- 用于定义生成库文件(library) 的CMake目标。 定义一个库文件,并向库文件中新增源码。
  • 库文件包含一组函数或对象,供其他程序使用,可以是静态库(.a或.lib)或共享库(.so或.dll)。

  • 语法:add_library(target_name [STATIC | SHARED | MODULE] source1 [source2 ...]) 创建一个库,名为target_name。向这个库中引入指定的c/c++代码文件。 STATIC|SHARED用于指定这个库的类型。STATIC是静态库,SHARED是动态库。示例:add_library(my_library STATIC math.cpp util.cpp) 在这个例子中,my_library是一个静态库,包含了 math.cpp 和 util.cpp 两个源文件。 | | add_executable | - 用于定义生成可执行文件(executable) 的CMake目标。

  • 可执行文件是可以直接运行的程序,通常是你的应用程序或测试程序。

  • 语法:add_executable(target_name source1 [source2 ...]) executable_name 是你要生成的可执行文件的名称。 source_file1, source_file2, 等是你的项目中的源代码文件列表,这些文件将被编译并链接到最终的可执行文件中。 | | set_target_properties(<> PROPERTIESIMPORTED_LOCATION) | 指定 导入库的路径 | | set_target_properties(<> PROPERTIESLIBRARY_OUTPUT_DIRECTORY) | 指定生成的目标库的导出路径 | | find_library | 查找NDK 库 | | target_link_libraries | 将预构建库关联到原生库 | | aux_source_directory | 查找在某个路径下的所有源文件 | | file(GLOB ...) | 使用 file(GLOB ...) 查找指定目录下的文件。如:file(GLOB sources ${MY_SOURCE_DIR}/*.cpp),查找指定目录下的.cpp后缀文件 | | message("") | 在CMakeLists.txt使用,代码执行时会生成日志。 .externalNativeBuild/cmake/debug/{abi}/cmake_build_output.txt 中查看 log。 |

cmake_minimum_required用于指定CMake的最低版本信息,不加入会收到警告。

cmake_minimum_required(VERSION 3.4.1)

实例:CMake指令配置CMakeLists.txt脚本

cmake_minimum_required(VERSION 3.22)
project(CMakeDemo02 C)

set(CMAKE_C_STANDARD 99)

#设置lib库的输出目录为   项目根目录的lib目录下。
#LIBRARY_OUTPUT_PATH为系统变量
#set既可以设置系统变量,也可以声明自己的变量
set(LIBRARY_OUTPUT_PATH  ${PROJECT_SOURCE_DIR}/lib)

#打印
message("LIBRARY_OUTPUT_PATH   " ${LIBRARY_OUTPUT_PATH})

#生成可执行文件
#add_executable(CMakeDemo02 main.c)

#生成 动态/静态 库
#add_library(CMakeDemo02 main.c)
#add_library(CMakeDemo02 SHARED main.c)

#使用set创建变量,来生成共享库。等效于  add_library(CMakeDemo02 SHARED main.c)
set(SRC_LIST main.c)
add_library(CMakeDemo02 SHARED ${SRC_LIST})

动态库生成:

输出:

LIBRARY_OUTPUT_PATH B:/CLionProjects/CMakeDemo02/lib

FFmpeg Cmake示例写法

FFmpeg中CMake语法,这里都是很常见的写法。非常有参考意义。

cmake_minimum_required(VERSION 3.4.1)

#add libavcodec
add_library(avcodec
        SHARED
        IMPORTED
        )

SET_TARGET_PROPERTIES(
        avcodec
        PROPERTIES IMPORTED_LOCATION
        ${PROJECT_SOURCE_DIR}/ffmpeg/prebuilt/${ANDROID_ABI}/libavcodec.so
)

#add libavfilter
add_library(avfilter
        SHARED
        IMPORTED
        )

SET_TARGET_PROPERTIES(
        avfilter
        PROPERTIES IMPORTED_LOCATION
        ${PROJECT_SOURCE_DIR}/ffmpeg/prebuilt/${ANDROID_ABI}/libavfilter.so
)


#add libavformat
add_library(avformat
        SHARED
        IMPORTED
        )

SET_TARGET_PROPERTIES(
        avformat
        PROPERTIES IMPORTED_LOCATION
        ${PROJECT_SOURCE_DIR}/ffmpeg/prebuilt/${ANDROID_ABI}/libavformat.so
)


#add libavutil
add_library(avutil
        SHARED
        IMPORTED
        )

SET_TARGET_PROPERTIES(
        avutil
        PROPERTIES IMPORTED_LOCATION
        ${PROJECT_SOURCE_DIR}/ffmpeg/prebuilt/${ANDROID_ABI}/libavutil.so
)


#add libpostproc
add_library(postproc
        SHARED
        IMPORTED
        )

SET_TARGET_PROPERTIES(
        postproc
        PROPERTIES IMPORTED_LOCATION
        ${PROJECT_SOURCE_DIR}/ffmpeg/prebuilt/${ANDROID_ABI}/libpostproc.so
)

#add libswresample
add_library(swresample
        SHARED
        IMPORTED
        )

SET_TARGET_PROPERTIES(
        swresample
        PROPERTIES IMPORTED_LOCATION
        ${PROJECT_SOURCE_DIR}/ffmpeg/prebuilt/${ANDROID_ABI}/libswresample.so
)

#add libswscale
add_library(swscale
        SHARED
        IMPORTED
        )

SET_TARGET_PROPERTIES(
        swscale
        PROPERTIES IMPORTED_LOCATION
        ${PROJECT_SOURCE_DIR}/ffmpeg/prebuilt/${ANDROID_ABI}/libswscale.so
)

include_directories(ffmpeg/)


find_library(
        log-lib
        log)

add_library(
        ffmpeg-cmd
        SHARED
        ffmpeg/ffmpeg-cmd.cpp ffmpeg/ffmpeg.c ffmpeg/cmdutils.c ffmpeg/ffmpeg_filter.c ffmpeg/ffmpeg_hw.c ffmpeg/ffmpeg_opt.c
)
target_link_libraries( # Specifies the target library.
        ffmpeg-cmd

        avcodec
        swscale
        swresample
        postproc
        avutil
        avformat
        avfilter
        ${log-lib})

gcc/clang编译器的编译命令

简单了解使用gcc/clang编译器去编译Ffmpeg

编译命令: gcc/clang -g -O2 -o log ffmpeg_log.c -I -L -l(第一竖线是大写的i,第三个竖线是小写的L)

示例: clang -g -O2 -o log ffmpeg_log.c -I …/ffmpeg -L …/ffmpeg/libavutil -lavutil

解析:

  • -g:生成调试信息,以便在调试时能够追踪到源代码。
  • -O2:启用优化级别 2,这会告诉编译器进行一些优化,以提高生成的代码的性能。(默认是-O1是不对指令进行优化,-O2编译器会按照自己的理解优化指令,让指令运行的更快)
  • -o log:指定输出文件的名称为 log
  • ffmpeg_log.c:要编译的 C 源代码文件的名称。
  • -I ../ffmpeg:(指定头文件的位置)添加头文件搜索路径,告诉编译器在 ../ffmpeg 目录中查找头文件。
  • -L ../ffmpeg/libavutil:(-L 指定库文件的位置)添加库文件搜索路径,告诉编译器在 ../ffmpeg/libavutil 目录中查找库文件。
  • -lavutil:指定要链接的库,这里是 libavutil 库。-l 选项(指定引用的库文件名字)表示链接一个库,后面是库的名称。

示例:使用ffmpeg的日志系统

#include <stdio.h>
#include <libavutil/log.h>
int main(int argc,char* argv[]){
    av_log_set_level(AV_LOG_DEBUG);
    av_log(NULL,AV_LOG_DEBUG,"Hello World\n");
    av_log(NULL,AV_LOG_INFO,"Hello World\n");
    av_log(NULL,AV_LOG_WARNING,"Hello World\n");
    av_log(NULL,AV_LOG_ERROR,"Hello World\n");
    return 0;wwwww
}

什么是库

库是写好的、成熟的、可以复用的代码,一般程序运行都需要依赖许多底层库文件。

本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行,库有两种:静态库(.a、.lib)和动态库(.so、.so)。

静态、动态是指链接方式,将一个程序编译成可执行程序步骤如下:

静态库与共享库(动态库)

  • 静态库: 以.a结尾。静态库在程序链接的时候使用链接器会将程序中使用到函数的代码从库文件中拷贝到应用程序中。一旦链接完成,在执行程序的时候就不需要静态库了。
    静态加载,在编译时就加载,进行静态库的代码拷贝。
    类似于组件化,编译时就完成链接。
  • 共享库: 以.so结尾。在程序的链接时候并不像静态库那样在拷贝使用函数的代码,而只是作些标记。然后在程序开始启动运行的时候,动态地加载所需模块
    动态加载,只有在执行System.loadLibrary时才会加载动态库。
    类似于插件化,使用时才加载。
add_library(
    #第一个参数,决定了最终生成的共享库的名字
    native-lib #最终生成的so文件libnative-lib.so
    #第二个参数,我们可以指定根据源文件编译出来的是静态库还是共享库,分别对应STATIC/SHARED关键字。不指定第二个参数时,默认生成静态库
    SHARED
    #指定源文件
    native-lib.cpp)

当我们开发自己的so库时,如果使用了其他静态库,那么在编译时,静态库的代码会被编译进我们的so中,如果使用的其他库是动态库,那么就会被编译到apk里,不会与我们开发的so库合并。

静态库

所谓静态库,是因为在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中,对应的链接方式成为静态链接。

静态库与汇编生成的目标文件一起链接成为可执行文件,那么可以得出,静态库的格式跟.o文件格式相似,其实一个静态库可以简单看成是一组目标文件(.o/.ojb文件)的集合,即很多目标文件经过压缩打包后形成的一个文件。

优点:

  • 程序在运行时与函数库没有关系,移植方便。
  • 静态库加载速度更快:在可执行文件编译时就链接好了,所以在使用时可以直接执行程序,不需要再次加载。

缺点:

  • 浪费空间与资源,所有相关的目标文件与牵涉函数库会被链接合成为一个可执行文件。

共享库(动态库)

为什么需要动态库?

  • 静态库会造成空间的浪费
  • 静态库对于程序更新、部署和发布会带来麻烦,如果静态库需要更新,那么所有使用它的应用程序都需要重新编译、发布给用户。
    (一个很小的改动,就可能导致整个程序需要重新下载)

动态库的优点

  • 节约内存,可以节省APK体积。
  • 可以实现进程之间资源共享(因此动态库也称为共享库)
  • 使程序升级变得简单

动态库的缺点

  • 速度慢,加载慢

各平台,动态库、静态库命名区别

平台静态库动态库
Windows.a.dll
linux.a.so
mac.a.so

Android项目中引入native库

在安卓项目中,我们使用native库的方式主要有:

  • 引入生成的库:安卓官方提供的Android NDK库。
  • 引入生成的库:第三方提供的静态/共享库。
  • 引入三方库的源代码,由自己引入编译。
  • 直接引入其他CMake项目源码来使用。

Android NDK库的引入

参考资料: 配置 CMake | Android Studio | Android Developers Android官方NDK使用文档 官方NDK使用Demo

Android NDK 提供了一套您可能会觉得非常实用的原生 API 和库。通过在项目的 CMakeLists.txt 脚本文件中包含 NDK 库,您可以使用其中任何 API。

Android 平台上已存在预构建的 NDK 库,因此您无需构建 NDK 库或将其打包到 APK 中。由于这些 NDK 库已位于 CMake 搜索路径中,因此您甚至无需指定本地安装的 NDK 库的位置,您只需为 CMake 提供您想要使用的库的名称,并将其与您自己的原生库相关联即可。

向 CMake 构建脚本添加 find_library() 命令以找到 NDK 库并将其路径存储为一个变量。您可以使用此变量在构建脚本的其他部分引用 NDK 库。

在Android系统当中,预置了一些标准的NDK库,这些库函数的目的就是让开发者能够在native方法中实现之前在Java层开发的一些功能。

android-ndk-r23\build\cmake\system_libs.cmake

在 CMakeLists.txt 引入 Android NDK 的 log 库:

  • and_library:将一个变量和Android NDK的某个库建立关联关系。该函数的第二个参数为Android NDK中对应的库名称,而调用该方法之后,它就被和第一个参数所指定的变量关联在一起。 在这种关联建立以后,我们就可以使用这个变量在构建脚本的其它部分引用该变量所关联的NDK库。
  • target_link_libraries:把NDK库和我们自己的原生库native-lib进行关联,这样,我们就可以调用该NDK库中的函数了。
find_library(
        # 定义路径变量的名称 并用这个变量存储 NDK库的位置。
        log-lib
        # 指定 CMake 需要定位的NDK库的名称
        log)
        
# 将一个或多个 其他本地库 链接到我们的本地库上。
target_link_libraries(
				# 指定目标库(native-lib是我们自己创建的原生库)
        native-lib
        # 将日志库链接到目标库
        ${log-lib})
  • NDK 还以源代码的形式包含一些库,您需要构建这些代码并将其关联到您的原生库。您可以使用 CMake 构建脚本中的 add_library() 命令将源代码编译到原生库中。如需提供本地 NDK 库的路径,您可以使用 Android Studio 自动为您定义的 ANDROID_NDK 路径变量。
    以下命令可以指示 CMake 将 android_native_app_glue.c(负责管理 NativeActivity 生命周期事件和触控输入)构建至静态库,并将其与 native-lib 关联:
#给android_native_app_glue创建为别名"app-glue"lib。
add_library( app-glue
             STATIC
             ${ANDROID_NDK}/sources/android/native_app_glue/android_native_app_glue.c )

# 您需要将 静态(STATIC)库 与 共享(SHARED )的本地库链接起来.
target_link_libraries( native-lib app-glue ${log-lib} ) #将app-glue 和 log-lib库,与本地库"native-lib"进行链接。
  • 在代码中引入头文件,并调用Log函数:
#include <android/log.h>

#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

void ...(...){
    // 打印信息到Logcat
    LOGI("Content of jstring: %s", nativeString);
}

引入三方so库

  1. 首先将 so 库和头文件拷贝到对应的目录。

/app/src/main/jniLibs/arm/libxxx.so

/app/src/main/cpp/include

  1. 修改 CMakeLists.txt 文件。
    添加预构建库的步骤与为 CMake 指定其他要构建的原生库的步骤相似。不过,由于库已构建,我们需要使用 IMPORTED 标志指示 CMake 您只想要将此库导入到您的项目中:
add_library( imported-lib
             SHARED
             IMPORTED )

然后,需要使用 set_target_properties() 命令指定库的路径,具体命令如下所示。

add_library( imported-lib
             SHARED
             IMPORTED )
             
set_target_properties( # Specifies the target library.
                       imported-lib

                       # Specifies the parameter you want to define.
                       PROPERTIES IMPORTED_LOCATION

                       # Provides the path to the library you want to import.
                       imported-lib/src/${CMAKE_ANDROID_API}/libimported-lib.so )

为了确保 CMake 能够在编译时找到我们的头文件,我们需要使用 include_directories() 命令并包含相应头文件的路径:

include_directories( imported-lib/include/ )

将预构建库关联到我们自己的原生库,将其添加到 CMake 构建脚本的 target_link_libraries() 命令中:

target_link_libraries( native-lib imported-lib app-glue ${log-lib} )

如需将预构建库打包到 APK 中,您需要使用 sourceSets 块手动配置 Gradle 以包含 .so 文件的路径。

android {
    ...
        defaultConfig {...}
    buildTypes {...}

    // Encapsulates your external native build configurations.
    externalNativeBuild {

        // Encapsulates your CMake build configurations.
        cmake {

            // Provides a relative path to your CMake build script.
            path "CMakeLists.txt"
        }
    }
}

构建 APK 后,您可以使用 APK 分析器验证 Gradle 会将哪些库打包到您的 APK 中。

在代码中引入第三方库的头文件,调用本地函数,完成。

示例:在CLion编译器中创建两个Cmake项目,项目A生成一个库,项目B去引入项目A生成的库并使用。

通过上面的过程,我们知道,CMake项目想使用其他库时,需要其他库的两个东西:①生成的库文件 ②对应的.h头文件

下面这个示例就来实现,在项目A生成一个库后,在项目B中去使用项目A定义的功能。

项目A:首先是项目A的创建与库生成,这里我们用项目A来定义库功能并生成库。

创建项目A,项目名为CMAKEDEMO02。

定义一个Add加法功能。

//
// Created by linxy on 2023/12/1.
//
#include "Add.h"


int Add(int i1,int i2){
    return i1+i2;
}
//
// Created by linxy on 2023/12/1.
//
#ifndef CMAKEDEMO02_ADD_H
#define CMAKEDEMO02_ADD_H

int Add(int i1,int i2);

#endif //CMAKEDEMO02_ADD_H

配置CMakeLists脚本,用来生成静态库

cmake_minimum_required(VERSION 3.22)
project(CMakeDemo02 C)

set(CMAKE_C_STANDARD 99)

#配置库的输出位置
set(LIBRARY_OUTPUT_PATH  ${PROJECT_SOURCE_DIR}/lib)

#生成库指令
#使用set创建变量,来生成库。等效于  add_library(CMakeDemo02 main.c)
set(SRC_LIST main.c Add.c Add.h)
add_library(CMakeDemo02 ${SRC_LIST})

对项目进行Build,从而生成库文件:

至此,项目A的职责就完成了。下面进行项目B的创建。

创建项目B:导入项目A的库并使用

项目B的项目名为:CMakeDemo03

首先导入项目A的静态库,及项目A的头文件:

在CMakeLists.txt脚本中引入静态库,并声明头文件所在目录:

6~13行是用来设置三方库地址,链接三方库到本地,且声明相关库的头文件的路径。

cmake_minimum_required(VERSION 3.22)
project(CMakeDemo03 C)

set(CMAKE_C_STANDARD 99)

#定义一个变量,staticFiled变量类型是STATIC静态的,说明是导入静态库。导入动态库则只需将STATIC修改为SHARED即可。
add_library(staticFiled STATIC IMPORTED)
#为目标staticFiled赋值,赋值为本地路径,项目的工程目录拼接.so的路径
set_property(TARGET staticFiled	#指定目标库变量
							PROPERTY IMPORTED_LOCATION #表示为属性为IMPORTED_LOCATION,即本地路径
              ${PROJECT_SOURCE_DIR}/lib/libCMakeDemo02.a	#库的地址
              )
#指定头文件目录地址
include_directories(${PROJECT_SOURCE_DIR}/include)
#将库staticFiled链接到我们的库中
## link_libraries(staticFiled)
#将库staticFiled链接到CMakeDemo03库中。
target_link_libraries(CMakeDemo03 staticFiled)
#target_link_libraries与link_libraries的区别是参数区别:target_link_libraries支持指定目标库链接到哪个指定库中;而link_libraries不支持精准指定,它会指定整个项目中所有目标(

add_executable(CMakeDemo03 main.c)

完成上述步骤后,库A就引入进来了,接下来进行功能的使用。

在main.c中,引入Add头文件,进行使用:

#include <stdio.h>

//#include "include/Add.h"

//在CMakeLists.txt配置: include_directories(${PROJECT_SOURCE_DIR}/include) 后,我们引入头文件时,就可以不用指定具体路径了。
#include "Add.h"
#include <Add.h>

int main() {
    printf("Hello, World! %d \n", Add(5,10));
    return 0;
}

运行项目B:

完事。

引入三方库的源码

我们自己开发的JNI,写的C代码是需要在add_library中将每一个文件都添加进去的。所以三方库的源码也是如此使用,但是三方库的源码如果比较多的情况就需要借助一些指令来查找每个目录下的所有文件。

aux_source_directory与file(GLOB ...)

如果要引入同文件夹下的大量源码文件,那么我们就得借助这两个指令来实现了。
他们的作用是查找指定目录下的所有源文件。

但是这两种方案都有一个缺陷,它们不会去查找子目录下的文件,所以还是要自己手动填入所有子目录。

方案一:使用 aux_source_directory 方法将路径列表全部放到一个变量中。

aux_source_directory只会读取当前目录下的文件,不会查找当前目录下子目录中的文件,所以需要手动多查找几处。

注: aux_source_directory只会查找.c/.cpp源码文件,不会查找.h头文件,通常add_library也不需要引入头文件,所以一般问题不大。

# 查找所有源码 并拼接到路径列表
aux_source_directory(${CMAKE_HOME_DIRECTORY}/src/api SRC_LIST)
aux_source_directory(${CMAKE_HOME_DIRECTORY}/src/core CORE_SRC_LIST)
list(APPEND SRC_LIST ${CORE_SRC_LIST})
add_library(native-lib SHARED ${SRC_LIST})

方案二:使用file(GLOB ...)指令

在 CMake 中,file(GLOB ...) 用于获取匹配某个模式的文件列表。然而,使用 file(GLOB ...) 来收集源文件并不是一个推荐的做法,因为它可能导致一些不可预测的行为。

通常,更好的做法是显式地列举你的源文件,以确保你有完全的控制,并且能够正确地管理依赖关系。

file(GLOB ...) 中,你可以指定一个或多个目录来执行文件匹配。你可以在模式参数中使用相对或绝对路径,以匹配特定目录下的文件。

cmake_minimum_required(VERSION 3.12)
project(MyProject)

# 指定要查找的目录
set(MY_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src)

# 使用 file(GLOB ...) 查找指定目录下的 .cpp 文件
file(GLOB sources ${MY_SOURCE_DIR}/*.cpp)
# 使用 file(GLOB ...) 查找指定目录下的 .c 文件
file(GLOB sources ${MY_SOURCE_DIR}/*.c)
# 使用 file(GLOB ...) 查找指定目录下的 .h 文件
file(GLOB sources ${MY_SOURCE_DIR}/*.h)

#链接到库中
add_library(${CMAKE_PROJECT_NAME} SHARED
        # List C/C++ source files with relative paths to this CMakeLists.txt.
        ${SRC_LIST} openfeclib.cpp)

## 添加可执行文件
##add_executable(MyExecutable ${sources})

引入三方库的源代码来编译

理解了aux_source_directory或file(GLOB ...)这两个查找函数后,我们就可以开始进行三方库的源码引入与编译了。

我们只需要按照平时开发JNI的方式来引入就好。只是麻烦点的是需要把所有源代码文件的路径给add_library中。

示例:

首先项目结构如下,include/openFec是我们引入的三方库源码(50个文件)。openfeclib.cpp、test_openfec.h、test_openfec.cpp是我们编写的调试代码。

引入三方库源码重点就是这步,CMakeLists中引入所有的源代码:

cmake_minimum_required(VERSION 3.10.2)


project("openfeclib")

## 查找所有源码 并拼接到路径列表,使用aux_source_directory
##aux_source_directory(${CMAKE_HOME_DIRECTORY}/include/openFec/lib_stable/reed-solomon_gf_2_8 SRC_LIST)
##aux_source_directory(${CMAKE_HOME_DIRECTORY}/include/openFec/lib_stable/reed-solomon_gf_2_m SRC_LIST_REED-SOLOMON_GF_2_M)
##aux_source_directory(${CMAKE_HOME_DIRECTORY}/include/openFec/lib_stable/reed-solomon_gf_2_m/galois_field_codes_utils SRC_LIST_GALOIS_FIELD_CODES_UTILS)
##aux_source_directory(${CMAKE_HOME_DIRECTORY}/include/openFec/lib_stable/2d_parity_matrix SRC_LIST_2D__PARITY_MATRIX)
##aux_source_directory(${CMAKE_HOME_DIRECTORY}/include/openFec/lib_stable/ldpc_staircase SRC_LIST_LDPC_STAIRCASE)
##aux_source_directory(${CMAKE_HOME_DIRECTORY}/include/openFec/lib_common/ SRC_LIST_LIB_COMMON)
##aux_source_directory(${CMAKE_HOME_DIRECTORY}/include/openFec/lib_common/statistics SRC_LIST_STATISTICS)
##aux_source_directory(${CMAKE_HOME_DIRECTORY}/include/openFec/lib_common/linear_binary_codes_utils SRC_LIST_STATISTICS_LINEAR_BINARY_CODES_UTILS)
##aux_source_directory(${CMAKE_HOME_DIRECTORY}/include/openFec/lib_common/linear_binary_codes_utils/binary_matrix SRC_LIST_BINARY_MATRIX)
##aux_source_directory(${CMAKE_HOME_DIRECTORY}/include/openFec/lib_common/linear_binary_codes_utils/it_decoding SRC_LIST_IT_DECODING)
##aux_source_directory(${CMAKE_HOME_DIRECTORY}/include/openFec/lib_common/linear_binary_codes_utils/ml_decoding SRC_LIST_ML_DECODING)#list(APPEND SRC_LIST ${SOLOMON_GF_2_M})
##list(APPEND SRC_LIST ${SRC_LIST_GALOIS_FIELD_CODES_UTILS})
##list(APPEND SRC_LIST ${SRC_LIST_2D_PARITY_MATRIX})
##list(APPEND SRC_LIST ${SRC_LIST_LDPC_STAIRCASE})
##list(APPEND SRC_LIST ${SRC_LIST_LIB_COMMON})
##list(APPEND SRC_LIST ${SRC_LIST_STATISTICS})
##list(APPEND SRC_LIST ${SRC_LIST_STATISTICS_LINEAR_BINARY_CODES_UTILS})
##list(APPEND SRC_LIST ${SRC_LIST_BINARY_MATRIX})
##list(APPEND SRC_LIST ${SRC_LIST_IT_DECODING})
##list(APPEND SRC_LIST ${SRC_LIST_ML_DECODING})
##message("print SRC_LIST: ${SRC_LIST}")

##搜索.c  .cpp后缀的文件
file(GLOB SRC_LIST
        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_stable/reed-solomon_gf_2_8/*.c
        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_stable/reed-solomon_gf_2_8/*.cpp
#        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_stable/reed-solomon_gf_2_8/*.h

        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_stable/reed-solomon_gf_2_m/*.c
        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_stable/reed-solomon_gf_2_m/*.cpp
#        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_stable/reed-solomon_gf_2_m/*.h

        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_stable/reed-solomon_gf_2_m/galois_field_codes_utils/*.c
        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_stable/reed-solomon_gf_2_m/galois_field_codes_utils/*.cpp
#        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_stable/reed-solomon_gf_2_m/galois_field_codes_utils/*.h

        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_stable/2d_parity_matrix/*.c
        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_stable/2d_parity_matrix/*.cpp
#        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_stable/2d_parity_matrix/*.h

        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_stable/ldpc_staircase/*.c
        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_stable/ldpc_staircase/*.cpp
#        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_stable/ldpc_staircase/*.h

        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_common//*.c
        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_common//*.cpp
#        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_common//*.h

        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_common/statistics/*.c
        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_common/statistics/*.cpp
#        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_common/statistics/*.h

        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_common/linear_binary_codes_utils/*.c
        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_common/linear_binary_codes_utils/*.cpp
#        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_common/linear_binary_codes_utils/*.h

        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_common/linear_binary_codes_utils/binary_matrix/*.c
        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_common/linear_binary_codes_utils/binary_matrix/*.cpp
#        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_common/linear_binary_codes_utils/binary_matrix/*.h

        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_common/linear_binary_codes_utils/it_decoding/*.c
        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_common/linear_binary_codes_utils/it_decoding/*.cpp
#        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_common/linear_binary_codes_utils/it_decoding/*.h

        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_common/linear_binary_codes_utils/ml_decoding/*.c
        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_common/linear_binary_codes_utils/ml_decoding/*.cpp
#        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_common/linear_binary_codes_utils/ml_decoding/*.h
        )
        
message("print SRC_LIST: ${SRC_LIST}")
message("print PROJECT_SOURCE_DIR: ${PROJECT_SOURCE_DIR}")
message("print CMAKE_SOURCE_DIR: ${CMAKE_SOURCE_DIR}")
message("print CMAKE_HOME_DIRECTORY: ${CMAKE_HOME_DIRECTORY}")
message("print CMAKE_CURRENT_SOURCE_DIR : ${CMAKE_CURRENT_SOURCE_DIR}")
message("print PROJECT_BINARY_DIR : ${PROJECT_BINARY_DIR}")
message("print include : ${CMAKE_CURRENT_SOURCE_DIR}/include")
message("print so : ${PROJECT_SOURCE_DIR}/libs/arm64-v8a")
message("print CMAKE_PROJECT_NAME : ${CMAKE_PROJECT_NAME}")
message("print lib_common : ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_common/")
#message打印的日志会在  .cxx/cmake/debug/armeabi-v7a/build_output.txt  中

#引入头文件,便于include
include_directories(${PROJECT_SOURCE_DIR}/include)
include_directories(
        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_stable/reed-solomon_gf_2_8
        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_stable/reed-solomon_gf_2_m
        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_stable/reed-solomon_gf_2_m/galois_field_codes_utils
        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_stable/2d_parity_matrix
        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_stable/ldpc_staircase
        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_common
        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_common/statistics
        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_common/linear_binary_codes_utils
        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_common/linear_binary_codes_utils/binary_matrix
        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_common/linear_binary_codes_utils/it_decoding
        ${CMAKE_HOME_DIRECTORY}/include/openFec/lib_common/linear_binary_codes_utils/ml_decoding
        )

//生成动态库,并添加我们的源代码
add_library(${CMAKE_PROJECT_NAME}
      SHARED
      # List C/C++ source files with relative paths to this CMakeLists.txt.
      ${SRC_LIST}
      openfeclib.cpp
      test_openfec.cpp
)

## Specifies libraries CMake should link to your target library. You
## can link libraries from various origins, such as libraries defined in this
## build script, prebuilt thir

包含其他 CMake 项目

如果想要构建多个 CMake 项目并在 Android 项目中包含这些 CMake 项目的输出,您可以使用一个 CMakeLists.txt 文件(即您关联到 Gradle 的那个文件)作为顶级 CMake build 脚本,并添加其他 CMake 项目作为此 build 脚本的依赖项。以下顶级 CMake build 脚本会使用 add_subdirectory() 命令将另一个 CMakeLists.txt 文件指定为 build 依赖项,然后关联其输出,就像处理任何其他预构建库一样。
将第三方CMake项目lib_gmath包含到当前Cmake项目native-lib中:

# 将lib_src_DIR设置为需要被导入的目标CMake项目的路径。
set( lib_src_DIR ../gmath )

# 将lib_build_DIR设置为输出目录的路径。
set( lib_build_DIR ../gmath/outputs )
#生成该lib_build_DIR路径的文件目录
file(MAKE_DIRECTORY ${lib_build_DIR})

# 添加位于指定目录中的CMakeLists.txt文件作为构建依赖项。
add_subdirectory( # 指定CMakeLists.txt文件的目录。
                  ${lib_src_DIR}

                  # 指定生成输出的目录。
                  ${lib_build_DIR} )

# 添加额外的CMake构建的输出作为预构建的静态库,并将其命名为lib_gmath。
add_library( lib_gmath STATIC IMPORTED )

#将lib_gmath.a静态库的路径作为本地三方库导入到lib_gmath
set_target_properties( lib_gmath #三方库导入后生成的命名
											  PROPERTIES IMPORTED_LOCATION	#导入本地库
                       ${lib_build_DIR}/${ANDROID_ABI}/lib_gmath.a )	#本地库地址

#引入lib_gmath库中的头文件
include_directories( ${lib_src_DIR}/include )

# 将lib_gmath构建输出的库 链接到 目标库native-lib
target_link_libraries( native-lib ... lib_gmath )

Question

Q1:怎么指定 C++标准?

A:在 build_gradle 中,配置 cppFlags -std

externalNativeBuild {
    cmake {
        cppFlags "-frtti -fexceptions -std=c++14"
        arguments '-DANDROID_STL=c++_shared'
    }
}

Q2:怎么调试 CMakeLists.txt 中的代码?

A:使用 message 方法

cmake_minimum_required(VERSION 3.4.1)
#PROJECT__SOURCE_DIR:当前项目的绝对路径  	也可使用:项目名_SOURCE_DIR,如navlib__SOURCE_DIR
message(STATUS,"execute CMakeLists",${PROJECT__SOURCE_DIR})
#获取CMake编译时使用的目录:#CMAKE_BINARY_DIR
message(STATUS,"execute CMakeLists",${CMAKE_BINARY_DIR})
#build.gradle修改的参数,build_command.txt最终生成的参数可以在CMakeLists使用message来打印参数。
#用法:${参数名},如获取-DCMAKE_ANDROID_ARCH_ABI的属性,那么:${CMAKE_ANDROID_ARCH_ABI}。(取属性时不需要加 -D)

Android Studio3.2.2及之后版本:接着执行后在 .cxx/cmake/debug/{abi}/build_output.txt 中查看 log。

Android Studio3.2.2之前的旧版本是在底部build栏中会打印message信息。

Q3:什么时候 CMakeLists.txt 里面会执行

A:测试了下,好像在 sync 的时候会执行。执行一次后会生成 makefile 的文件缓存之类的东西放在 externalNativeBuild 中。所以如果 CMakeLists.txt中没有修改的话再次同步好像是不会重新执行的。(或者删除.externalNativeBuild 目录)真正编译的时候好像只是读取.externalNativeBuild 目录中已经解析好的makefile 去编译。不会再去执行 CMakeLists.txt。

Q4:C++文件中引入C的头文件后,调用头文件函数,CMake编译不通过;提示error: undefined symbol怎么解决?

需要给引入的C头文件添加 Extern "C",即可正常编译。如:

#ifndef PUBLIC_TEST_OPENFEC_H
#define PUBLIC_TEST_OPENFEC_H

#include <iostream>

extern "C" {
		//声明为C代码,用C的编译指令去编译
    #include <of_openfec_api.h>
}
#endif