基于AndroidStudio下的CMake使用

CMake简介

CMake 是一个跨平台的安装(编译)工具,可以用简单的语句来描述所有平台的安装(编译过程)。CMake是一套伪代码,执行编译的不是 CMake,可能是 gcc 也可能是 clang 等等,它让我们能更好的配置我们的工程的编译与配置、安装。CMake 可以帮助我们生成MakeFile,也可以帮助我们生成 build.ninja ,减小我们直接去编写这些编译参数的开发成本。而我们 Android 程序员在 ndk 中对 CMake 的使用,一般不涉及它的安装功能。

C与C++的编译

首先我们得大致了解下 C 以及 C++ 的编译原理。它们不像 Java,依赖于虚拟机,直接跨平台,也无需关心它的汇编二进制码的生成,它们是直接生成汇编语言的编程语言。在此只讲 LLVM 的编译。1、预处理阶段:将宏替换,删除注释展开头文件,生成.i文件 宏替换就是将宏拆箱成具体的一个常量值,删除注释就是把我们的注释从源文件中删除,展开头文件就是打开头文件里的各种 include 变成一个详细的 include 列表。2、词法分析:将代码切成一个个 token ,比如大小括号,等于号还有字符串等,是将字符序列转换为标记序列的过程。(也就是将代码按照符合切块)3、语法分析:验证语法是否正确,然后将所有节点组成抽象语法树 AST,也就是我们编译过程中验证我们输入的代码是否合法。4、静态分析:使用它来表示用于分析源代码以便自动发现错误。在 NDK 编译中,谷歌使用了 Ninja 编译器,它是用于取代 MakeFile 编译,在增量编译的时候速度优势很大。但即使是它,对于我们开发而言,使用也还是很复杂。(安卓的 ndk 编译使用的是 Cmake 结合 Ninja ,具体的编译步骤都会输出到 build.ninja 中通过 ninja 来使用ndk中的llvm编译)。Android 的 NDK 采用的是 Cmake+Ninja 的方式,以前是Android.mk(也就是makefile方案),而对 CMake 的调用则封装在 Gradle的 task 中( debug 版在 externalNativeBuildDebug 这个 task 中)。以上就是 C 与 C++的编译过程,而 CMake 的作用就是将这个过程通过我们的伪代码批量化、自动化执行,比如预处理阶段,我们就可以通过 CMake 去给宏定义不同的值来达到不同的编译方式的效果。当我们的项目比较庞大的时候,可以通过 CMake 去管理整个项目去指定生成我们想要的执行文件或者动态库或者静态库。 CMake 是一个预编译辅助工具。在NDK环境下,CMake 通过 android.toolchain.cmake 找到了对应 ANDROID_ABI 的工具连, CMake生成Ninja规则, 然后调用Ninja并行编译, Ninja调找到的clang执行编译,而其中一些重要但不怎么需要更改的参数,都交由gradle来生成并放在指定的配置文件里,比 build_command.txt。毕竟无论是 ninja 还是 makefile 或者是其他的编译工具,对我们来说直接去写他的配置都过于复杂,编写成本偏高。

Cmake的语法


# For more information about using CMake with Android Studio, read the

# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.10.2)

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11")

# Declares and names the project.

project("myapplication")

#添加第三方库的目录 ../就是上一级目录

set(jnilibs ${CMAKE_SOURCE_DIR}/../jniLibs)

include_directories(include)

#添加一个lib目录

link_directories(

${jnilibs}/${ANDROID_ABI})

file(GLOB src-files

${CMAKE_SOURCE_DIR}/player/*.cpp

${CMAKE_SOURCE_DIR}/decoder/*.cpp

${CMAKE_SOURCE_DIR}/bzlib/*.c

${CMAKE_SOURCE_DIR}/bspatch/bspatch.c

        )

#aux_source_directory(src sources)

# Creates and names a library, sets it as either STATIC

# or SHARED, and provides the relative paths to its source code.

# You can define multiple libraries, and CMake builds them for you.

# Gradle automatically packages shared libraries with your APK.

add_library(# Sets the name of the library.

            native-lib

            # Sets the library as a shared library.

            SHARED

${src-files}

            # Provides a relative path to your source file(s).

      native-lib.cpp)

# Searches for a specified prebuilt library and stores the path as a

# variable. Because CMake includes system libraries in the search path by

# default, you only need to specify the name of the public NDK library

# you want to add. CMake verifies that the library exists before

# completing its build.

find_library(# Sets the name of the path variable.

              log-lib

              # Specifies the name of the NDK library that

# you want CMake to locate.

              log )

MESSAGE(STATUS "This is log path "${log-lib})

add_library(avformat

        SHARED

        IMPORTED)

set_target_properties(avformat

        PROPERTIES IMPORTED_LOCATION

        ${jnilibs}/${ANDROID_ABI}/libavformat.so)

#支持 link_directories 到 jniLibs 目录 如果是IMPORTED_LOCATION方式会编译不通过

set(third-party-libs

avformat

avcodec

avfilter

swresample

swscale

avutil

        )

target_link_libraries(# Specifies the target library.

                      native-lib

${third-party-libs}

                      # Links the target library to the log library

# included in the NDK.

                      ${log-lib} )

一些一般不需要我们去修改的就不讲了。

project() 就是定义我们的项目名称,它和我们生成的库的名称没有关系。

add_library()我们安卓环境中常用就是两种。

Normal Libraries

这个模式就是最常见的 添加一个库,并指定这个库内部链接的源码 第一个参数是库名 第二个参数是库的类型 有 SHARED 和 STATIC 两种 第三个参数就是链接的源码路径。编译后会生成lib{库名}.so 的库文件。

Imported Libraries

这个模式是为当前工程引用一个编译好的库,第一个参数是库名,这个和库文件名字无关,但是最好对应上,第二个参数是库的类型 我们常用的就是 SHARE和 STATIC两种,第三个参数是 IMPORTED 这个是固定的,表示这是引入一个库的意思。第四个参数一般用不着,就不赘述。

include_directories([AFTER|BEFORE] [SYSTEM] dir1 [dir2 ...])

AFTER 或者 BEFORE 参数,可以控制是追加还是置前 后面直接放入我们的绝对路径或者相对路径即可,路径最前面不带/的话默认就是相对路径,就是 CMakeLists.txt所在路径,这一点在其他语法中一样。

link_directories([AFTER|BEFORE] directory1 [directory2 ...])

这个语句的用法和 include_directories 语句几乎是一样的,它的意义是为我们指定一个引用库的路径 可以用空格隔开来引入多个路径 也可以用分号来分隔 回车亦可,这是引入库,不是链接库,要注意,所以后续编译的时候我们还得去指定链接的库。

set( ... [PARENT_SCOPE])

这是一个传入传出方法,第一个参数就是我们想要赋值的变量名,后面紧跟着就是它的值,我们一般可以用来设置路径或者简单的数字或者布尔值,后面的参数可以引用当前CMake 环境固有的参数,比如ANDROID_ABI,CMAKE_SOURCE_DIR,这些都是我们的环境参数,通过${ANDROID_ABI}的方式引用,类似于 Kotlin 的语法,相信你不会陌生。ANDROID_ABI 其实是外部设置的一个 CMake 环境参数,至于它是怎么设置的,其实是万能的 Gradle 干的,后续会讲。这个语句容易和 file语句混淆,它不是操作文件的语句,它仅仅是为变量赋值的作用。

file()

file 语句,顾名思义,就是操作文件的语句。这个语句用法就非常复杂了,基本涵盖了所有的文件操作。第一个参数就决定了你是要以什么类型的文件操作来使用这个语句。它的操作大致有读、写、filesystem、路径转换、传输(上传下载)、锁、存档(我也不知道这个存档什么用,有知道的小伙伴欢迎交流)。

读的话语法就很简单,第一个参数决定你读取什么数据,是全文件呢还是读取哈希等等,第二个参数就是文件的路径,第三个参数就是存放你读取的文件的变量。写操作的话就是 write、touch、append 这几个我们可能用的多一些,第一个是命令参数,第二个是文件的路径,第三个是我们要写入的内容。一般情况,非常不建议在 CMake中做 IO操作。我们 Android 中对这个语句的用法更多的是 GLOB 命令,他是一个类似于搜索的命令,可以用来搜集文件 。第一个参数就是这个命令,第二个参数是用于存放这个文件集的变量,第三个参数是我们搜索的通配符。这个命令我们常用的操作就是当我们源码目录比较多的时候,可以用这个的通配符搜索,直接将所有的源码路径都放到一个变量里,直接使用这个变量来编译我们的库。比如

file(GLOB src_list *.cpp) ,千万不要和 C++ 中的 global 弄混了。

而 file 的路径转换操作,file(RELATIVE_PATH ) 对我们而言也是很有用的。

比如我们要引用一个不是标准安装的库或者是一个第三方的源码目录,完全不在CMakeLists.txt 的目录内,

我们要去引入它不知道要多少个源码文件,这个时候就可以用这个命令,把我们的绝对路径转化为一个相对路径并直接存入一个变量内,我们要引用的话直接使用这个变量即可,我们可以通过这个方式拿到这个变量后,再 file(GLOB third_source ${variable}/*.cpp)

set_target_properties(target1 target2 ... PROPERTIES prop1 value1 prop2 value2 ...)

这个语句是针对 library 的 target 即 library 可以同时对多个 library的字段进行操作,但是我不建议这么做,因为这样很容易写错。第一个参数 target 对应 library的名称,第二个参数是固定的 PROPERTIES 后面的参数就是键值对的形式 键 值 键 值 上面的例子中 我通过 add_library(avformat SHARED IMPORTED) 引入了 avformat 库,但是这个库没有路径的参数,所以我要紧跟着使用 set_target_properties 对这个库进行参数设置 set_target_properties(avformat PROPERTIES IMPORTED_LOCATION {ANDROID_ABI}/libavformat.so) IMPORTED_LOCATION 就是我们引入这个库的路径,后面的参数就是它的具体路径,这个要根据我们的不同的指令架构来获取不同的so文件。

option( variable "" [value])

条件语句 作用是设置一个变量,用来控制条件。value 有 ON 和 OFF 两种

find_library ( name1 [path1 path2 ...])

寻找安装目录下已安装的库,并为第一个变量赋值路径。这里的路径包含了 lib 和include,所以在当前编译环境下使用这个方式就能在项目里引入这个库的头文件来链接它了。

find_package( [version] [EXACT] [QUIET] [MODULE] [REQUIRED] [[COMPONENTS] [components...]] [OPTIONAL_COMPONENTS components...] [NO_POLICY_SCOPE])

用来调用预定义在 CMAKE_MODULE_PATH 下的 Find.cmake 模块。

也可以自己定义 Find模块,将其放入工程的某个目录中,通过 set(CMAKE_MODULE_PATH dir) 设置查找路径,供工程 find_package 使用

以上这两个语句都涉及到了安装的概念,对于安装的理解是很重要的。我们编译好一个工程,但是我们的环境并不知道,所以需要安装,这不是仅仅添加环境变量,安装往往会有 bin、lib、include 等目录,是有一套标准的文件存储方式。所以我们需要引入一个库的时候,最好的方式就是将它安装到我们的环境,好在强大的NDK已经有了许多的扩展库,一般情况下,我们使用 find_library 即可

CMake中的一些内置的变量和参数

PROJECT_BINARY_DIR,CMAKE_BINARY_DIR

这个变量指的是编译二进制文件的路径,如果是外部编译,他和源码目录不同,如果是内部编译,则相同

CMAKE_SOURCE_DIR PROJECT_SOURCE_DIR

这个变量指的是当前项目的源码路径,默认路径就说CMakelists.txt的所在路径

**CMAKE_CURRENT_SOURCE_DIR **

就是 CMakelists.txt的路径,一般我们不去更改它,除非我们加了一个子项目并且我们随意摆放子项目的 CMakelists.txt的路径时才用到它

EXECUTABLE_OUTPUT_PATH 和 LIBRARY_OUTPUT_PATH

前者是放置可执行文件的路径,后者是放置编译出来的库文件的路径

IMPORTED_LOCATION

引入的库的路径,由于我们经常需要把第三方库放到jniLibs目录下,而操作add_library来引入第三方库是不能引用jniLibs目录的,gradle 会报错 (AndroidStudio 4.0以上) ,所以现在最好还是使用 link_library 的方式来引入第三方库,只要我们正确的输入了库的名称,就一样可以正常引用并链接它。

CMake使用宏来区分平台编译

CMake支持宏定义。

在main方法中,我们判断了一个宏,TEST_IT_CMAKE,如果定义了它,就会命中if

image

如何使用CMake来定义它呢?

image

在这里例子中,我使用了 option 语句给变量 TEST_IT_CMAKE 添加了条件状态ON 并添加了解释说明(可以在log中输出), ON 就能够命中 if,OFF 命中 else,在 if 里,我使用了add_definitions(-DTEST_IT_CMAKE) 定义了一个叫 TEST_IT_CMAKE 的宏,这个语句可以只定义,也可以给宏传值。然后我们运行后,输出信息如下图

image

CMake 的 arguments

CMake 支持外部输入指令或者参数,其实就相当于宏,这些参数可以在 CMakeLists.txt中获得,在多平台编译的时候,这个参数就很有用,统一的格式和宏是一样的,也可以说他们就是宏。比如 -DANDROID_ABI=$arm_abi 这样用就可以选择交叉编译的时候选择对应的 clang来编译。

CMake引入FFmpeg

FFmpeg本身有一套完整的 MakeFile 构建参数,我们不需要使用 CMake 来编译它,编译的方法在我的个人博客里,编译完成后,我们只需要将 FFmpeg 的头文件统一放到我们用于存放头文件的地方,将编译完的共享库放到jniLibs对应的指令文件夹下,然后使用 include_directories 以及 link_directories 即可引入他们,在最后再选择链接它们,即使用 target_link_libraries 来链接它们。

image

要注意的是,最好不要使用 add_library 的方式,否则如果引入的库放到jniLibs目录下的话,在 AndroidPlugin4.0 以上会报如下错误:

image

CMake脱离Android Studio使用NDK交叉编译

NDK 是 android 的一套完整的工具,它自带了 ninja 编译工具,AndroidStudio 就是用它编译的 C 或者 C++ 库,但是有时候我们不想要用它,或者我们不想使用 AndroidStudio 来用 NDK 。比如 FFMpeg编译的时候,我们就需要给它的configure配置好 Android 相关的宏。那如果我们想用 CMake 的同时又不想用AndroidStudio来编译呢?也是可以的。经过我一段时间的踩坑,脱离 gradle 环境的话,我们想使用 ndk 里的 ninja 是很困难的,但是我们仍然可以使用我们电脑已安装的编译环境来编译,像 mac 安装了 CMake后,默认就会使用 XCode 自带的MakeFile 进行编译。以 arm64-v8a 为例,我们可以在包含了cpp 构建的 module 的 build 目录下找到相关的 CMake 参数配置。举个例子:project/app/build/.cxx/cmake/debug/arm64-v8a/build_command.txt 这个 txt 里缓存了每次调用cmake命令会带上的指令。

arguments :
-H/Users/edz/AndroidStudioProjects/MyApplication/app/src/main/cpp
-DCMAKE_CXX_FLAGS=-std=c++11 -frtti -fexceptions
-DCMAKE_FIND_ROOT_PATH=/Users/edz/AndroidStudioProjects/MyApplication/app/.cxx/cmake/debug/prefab/arm64-v8a/prefab
-DCMAKE_BUILD_TYPE=Debug
-DCMAKE_TOOLCHAIN_FILE=/Users/edz/Library/Android/sdk/ndk/21.1.6352462/build/cmake/android.toolchain.cmake
-DANDROID_ABI=arm64-v8a
-DANDROID_NDK=/Users/edz/Library/Android/sdk/ndk/21.1.6352462
-DANDROID_PLATFORM=android-16
-DCMAKE_ANDROID_ARCH_ABI=arm64-v8a
-DCMAKE_ANDROID_NDK=/Users/edz/Library/Android/sdk/ndk/21.1.6352462
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON
-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=/Users/edz/AndroidStudioProjects/MyApplication/app/build/intermediates/cmake/debug/obj/arm64-v8a
-DCMAKE_MAKE_PROGRAM=/Users/edz/Library/Android/sdk/cmake/3.10.2.4988404/bin/ninja
-DCMAKE_SYSTEM_NAME=Android
-DANDROID_TOOLCHAIN=clang
-DANDROID_ARM_MODE=arm
-DANDROID_STL=c++_static
-DCMAKE_SYSTEM_VERSION=21
-B/Users/edz/AndroidStudioProjects/MyApplication/app/.cxx/cmake/debug/arm64-v8a

-DCMAKE_MAKE_PROGRAM=/Users/edz/Library/Android/sdk/cmake/3.10.2.4988404/bin/ninja 这句命令就决定了编译程序的选用,所以我们在电脑已经带了编译环境的情况下,可以删掉这个命令 最后一行 -b 对于我们非 as 环境也是无用的,也应该去掉。像上面的library_output_directory 也可以去掉,也可以在我们的 CMakeLists.txt 中通过set 语句来设置这个变量来修改我们的编译输出 library的路径。
经过我的测试,以下的参数可以在外部环境使用 NDK 来交叉编译 Android 平台的库或者可执行文件
-frtti
-fexceptions
-DCMAKE_BUILD_TYPE=Debug
-DCMAKE_TOOLCHAIN_FILE=/Users/edz/Library/Android/sdk/ndk/21.1.6352462/build/cmake/android.toolchain.cmake
-DANDROID_ABI=arm64-v8a
-DANDROID_NDK=/Users/edz/Library/Android/sdk/ndk/21.1.6352462
-DANDROID_PLATFORM=android-16
-DCMAKE_ANDROID_ARCH_ABI=arm64-v8a
-DCMAKE_ANDROID_NDK=/Users/edz/Library/Android/sdk/ndk/21.1.6352462
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON
-DCMAKE_SYSTEM_NAME=Android
-DANDROID_TOOLCHAIN=clang
-DANDROID_ARM_MODE=arm
-DANDROID_STL=c++_static
-DCMAKE_SYSTEM_VERSION=21
这些-D开头的参数,都是对应的宏,是 NDK 的 预编译时会使用到的宏,告诉编译器去执行我们需要的编译逻辑。
另外引申一句,这些 command arguments 都可以在我们的gradle里去设置,设置方法如下
externalNativeBuild {
cmake {
arguments '-DANDROID_TOOLCHAIN=clang', '-DANDROID_ARM_MODE=arm', '-DANDROID_STL=c++_static', '-DCMAKE_SYSTEM_VERSION=21'
cppFlags "-std=c++11 -frtti -fexceptions"
}
}
在gradle的这个闭包里,arguments 后使用单引号以及逗号来隔开命令参数即可,cppFlags 是给 clang++ 的编译参数,表示使用 C++ 11 的标准的std 以及支持RTTI 和 支持异常处理 来编写。

你可能感兴趣的:(基于AndroidStudio下的CMake使用)