Android-NDK/JNI

AndroidNDK——Cmake详解

2020-02-19  本文已影响0人  GitLqr

一、Cmake语法详解

1、什么是CMake

2、CMake源文件

3、CMake注释

# 单行注释

#[[多行注释
多行注释
多行注释]]

4、CMake变量

# 声明变量:set(变量名 变量值)
set(var 123)
# 引用变量:message 命令用来打印
message("var = ${var}")

5、CMake列表(lists)

# 声明列表:set(列表名 值1 值2 ... 值N)
# 或set(列表名 "值1;值2;...;值N")
set(list_var 1 2 3 4 5)
# 或者
set(list_var2 "1;2;3;4;5")

message("list_var = ${list_var}")

6、CMake流程控制-操作符

类型 名称
一元 EXIST,COMMAND,DEFINED
二元 EQUAL,LESS,LESS_EQUAL,GREATER,GREATER_EQUAL,STREQUAL,STRLESS,
STRLESS_EQUAL,STRGREATER,STRGREATER_EQUAL,VERSION_EQUAL,VERSION_LESS,
VERSION_LESS_EQUAL,VERSION_GREATER,VERSION_GREATER_EQUAL,MATCHES
逻辑 NOT,AND,OR

7、CMake流程控制-布尔常量值

类型
true 1,ON,YES,TRUE,Y,非0的值
false 0,OFF,NO,FALSE,N,IGNORE,NOTFOUND,空字符串,
以-NOTFOUND结尾的字符串

8、CMake流程控制-条件命令

set(if_tap OFF)
set(elseif_tap ON)

if(${if_tap})
    message("if")
elseif(${elseif_tap})
    message("elseif")
else(${if_tap})
    message("else")
endif(${if_tap})
if(表达式)
    COMMAND(ARGS...)
elseif(表达式)
    COMMAND(ARGS...)
else(表达式)
    COMMAND(ARGS...)
endif(表达式)

9、CMake流程控制-循环命令

set(a "")
while(NOT a STREQUAL "xxx")
    set(a "${a}x")
    message("a = ${a}")
endwhile()
while(表达式)
    COMMAND(ARGS...)
endwhile(表达式)

10、CMake流程控制-循环遍历

1)格式一

foreach(item 1 2 3)
    message("item = ${item}")
endforeach(item)
foreach(循环变量 参数1 参数2 ... 参数N)
    COMMAND(ARGS...)
endforeach(循环变量)

2)格式二

foreach(item RANGE 3)
    message("item = ${item}")
endforeach(item)
foreach(循环变量 RANGE total)
    COMMAND(ARGS...)
endforeach(循环变量)

3)格式三

foreach(item RANGE 1 5 2)
    message("item = ${item}")
endforeach(item)
foreach(循环变量 RANGE start stop step)
    COMMAND(ARGS...)
endforeach(循环变量)

4)格式四

set(list_var 1 2 3)

foreach(item IN LISTS list_var)
    message("item = ${item}")
endforeach(item)
foreach(循环变量 IN LISTS 列表)
    COMMAND(ARGS...)
endforeach(循环变量)

11、CMake自定义函数命令

function(func x y z)
    message("call function func")
    message("x = ${x}")
    message("y = ${y}")
    message("z = ${z}")
    message("ARGC = ${ARGC}")
    message("arg1 = ${ARGV0} arg2 = ${ARGV1} arg3 = ${ARGV2}")
    message("all args = ${ARGV}")
endfunction(func)

func(1 2 3)

ARGC:表示传入参数的个数。
ARGV:表示所有参数。
ARGV0:表示第一个参数,ARGV1ARGV2以此类推。

function(<name> [arg1[arg2[arg3...]]])
    COMMAND()
endfunction(<name>)

12、CMake自定义宏命令

macro(ma x y z)
    message("call macro ma")
    message("x = ${x}")
    message("y = ${y}")
    message("z = ${z}")
endmacro(ma)

ma(1 2 3)

函数命令有自己的作用域。
宏命令跟调用者的作用域一样。

macro(<name> [arg1[arg2[arg3...]]])
    COMMAND()
endmacro(<name>)

13、CMake变量的作用域

变量查找优先级:函数层 优于 目录层目录层 优于 全局层,即:函数层 --> 目录层 --> 全局层

二、CMakeList.txt文件详解

1、CMakeLists.txt简析

使用AndroidStudio3.4创建一个C/C++Support的项目,默认在app/src/main目录下会生成cpp目录,里面包含CMakeLists.txt和native-lib.cpp。以下代码为CMakeLists.txt去掉英文注释格式化后的内容:

cmake_minimum_required(VERSION 3.4.1)

# 添加一个库,根据native-lib.cpp源文件编译一个native-lib的动态库
add_library(
    native-lib
    SHARED
    native-lib.cpp)

# 查找系统库,这里查找的是系统日志库,并赋值给变量log-lib
find_library(
    log-lib
    log)

# 设置依赖的库(第一个参数必须为目标模块,顺序不能换)
target_link_libraries(
    native-lib
    ${log-lib})

2、常用命令-add_library

1)添加一个库

add_library(<name> [STATIC | SHARED | MODULE]
 [EXCLUDE_FROM_ALL] source1 source2 ... sourceN)

2)导入预编译库

add_library(<name> <SHARED|STATIC|MODULE|UNKNOW> IMPORTED)

# 比如
add_library(test SHARED IMPORTED)
set_target_properties(
    test # 指明目标库名
    PROPERTIES IMPORTED_LOCATION # 指明要设置的参数
    库路径/${ANDROID_ABI}/libtest.so # 导入库的路径
)

3、常用命令-set

设置CMake变量:

# 设置可执行文件的输出路径(EXECUTABLE_OUTPUT_PATH是全局变量)
set(EXECUTABLE_OUTPUT_PATH [output_path])

# 设置库文件的输出路径(LIBRARY_OUTPUT_PATH是全局变量)
set(LIBRARY_OUTPUT_PATH [output_path])

# 设置C++编译参数(CMAKE_CXX_FLAGS是全局变量)
set(CMAKE_CXX_FLAGS "-Wall std=c++11")

# 设置源文件集合(SOURCE_FILES是本地变量即自定义变量)
set(SOURCE_FILES main.cpp test.cpp ...)

4、常用命令-include_directories

设置头文件目录。

# 可以用相对或绝对路径,也可以用自定义的变量值
include_directories(./include ${MY_INCLUDE})

相当于g++选项中的-l参数。

5、常用命令-add_executable

添加可执行文件:

add_executable(<name> ${SRC_LIST})

6、常用命令-target_link_libraries

target_link_libraries(<name> lib1 lib2 lib3)

# 如果出现互相依赖的静态库,CMake会允许依赖图中包含循环依赖,如:
add_library(A STATIC a.c)
add_library(B STATIC b.c)
target_link_libraries(A B)
target_link_libraries(B A)
add_executable(main main.c)
target_link_libraries(main A)

7、常用命令-add_definitions

为当前路径以及子目录的源文件加入由-D引入的define flag

add_definitions(-DF00 -DDEBUG ...)

通常用于添加编译参数

8、常用命令-add_subdirectory

用于添加子目录的CMake源文件:

# sub_dir指定包含CMakeLists.txt和源码文件的子目录位置
# binary_dir是输出路径,一般可以不指定
add_subdirectory(sub_dir [binary_dir])

如果当前目录下还有子目录时可以使用add_subdirectory,子目录中也需要包含有CMakeLists.txt。

9、常用命令-file

文件操作命令:

# 将message写入filename文件中,会覆盖文件原有内容
file(WRITE filename "message")
# 将message写入filename文件中,会追加在文件末尾
file(APPEND filename "message")
# 从filename文件中读取内容并存储到var变量中,如果指定了numBytes和offset,
# 则从offset处开始最多读numBytes个字节,另外如果指定了HEX参数,则内容会以十六进制形式存储在var变量中
file(READ filename var [LIMIT numbytes] [OFFSET offset] [HEX])
# 重命名文件
file(RENAME <oldname> <newname>)
# 删除文件,等于rm命令
file(REMOVE [file1 ...])
# 递归的执行删除文件命令,等于rm -r
file(REMOVE_RECURSE [file1...])
# 根据指定的url下载文件
# timeout超时时间;下载的状态会保存到status中;下载日志会被保存到log;sum指定所下载文件预期的MD5值,如果指定会自动进行比对,
# 如果不一致,则返回一个错误;SHOW_PROGRESS,进度信息会以状态信息的形式被打印出来
file(DOWNLOAD url file [TIMEOUT timeout] [STATUS status] [LOG log] [EXPECTED_MD5 sum] [SHOW_PROGRESS])
# 创建目录
file(MAKE_DIRECTORY [dir1 dir2 ...])
# 会把path转换为以unix的/开头的cmake风格路径,保存到result中
file(TO_CMAKE_PATH path result)
# 它会把cmake风格的路径转换为本地路径风格:windows下用"\",而unix下用"/"
file(TO_NATIVE_PATH path result)
# 将会为所有匹配查询表达式的文件生成一个文件list,并将该list存储进变量variable里,如果一个表达式指定了RELATIVE,返回的结果
# 将会是相对于定路径的相对路径,查询表达式例子:*.cxx,*.vt?
# NOTE:按照官方文档的说法,不建议使用file的GLOB指令来收集工程的源文件
file(GLOB variable [RELATIVE path] [globbing expressions]...)

使用这种方式来指定源文件的话,如果后面项目需要增加源文件,比如项目中原本有2个源文件a.c和b.c,后面新增一个c.c,如果我们直接增加c.c,然后进行编译是会报错的,因为CMakeLists文件没有改动,所以cmake不会重新生成makefile文件,所以我们需要简单的改动一下CMakeLists文件,可以在CMakeLists文件中添加一个空格,然后再编译,它就会生成makefile文件。

9、常用命令-set_directory_properties

设置某个路径的一种属性:

set_directory_properties(PROPERTIES prop1 value1 prop2 value2)

prop1,prop2代表属性,取值为:

10、常用命令-set_property

在给定的作用域内设置一个命名的属性:

set_property(<GLOBAL |
            DIRECTORY [dir] |
            TARGET [target ...] |
            SOURCE [src1 ...] |
            TEST [test1 ...] |
            CACHE [entry1 ...]>
            [APPEND]
            PROPERTY <name> [value ...])

PROPERTY 参数是必须的。第一个参数决定了属性可以影响的作用域:

11、多个源文件处理

cmake_minimum_required(VERSION 3.4.1)
# 查找当前目录所有源文件,并将名称保存到 DIR_SRCS 变量
# 不能查找子目录
aux_source_directory(. DIR_SRCS)
# 也可以使用
# file(GLOB DIR_SRCS *.c *.cpp)

add_library(
    native-lib
    SHARED
    ${DIR_SRCS})

如果源文件很多,把所有文件一个个加入很麻烦,可以使用aux_source_directory命令或file命令,会查找指定目录下的所有源文件,然后将结果存进指定变量名。

12、多目录多源文件处理

cmake_minimum_required(VERSION 3.4.1)
aux_source_directory(. DIR_SRCS)
# 添加child子目录下的cmakelist
add_subdirectory(child)

add_library(
    native_lib
    SHARED
    ${DIR_SRCS})
target_link_libraries(native-lib child)
----------------------------------------

# child目录下的CMakeLists.txt:
cmake_minimum_required(VERSION 3.4.1)
aux_source_directory(. DIR_LIB_SRCS)
add_library(
    child
    SHARED
    ${DIR_LIB_SRCS})

13、添加预编译库

1)Android6.0版本以前

假设我们本地项目引用了libimported-lib.so:

cmake_minimum_required(VERSION 3.4.1)
# 使用 IMPORTED 标志告知 CMake 只希望将库导入到项目中
# 如果是静态库则将shared改为static
add_library(imported-lib
            SHARED
            IMPORTED)

# 参数分别为:库、属性、导入地址、库所在地址
set_target_properties(
    imported-lib
    PROPERTIES
    IMPORTED_LOCATION
    <路径>/libimported-lib.so)

aux_source_directory(. DIR_SRCS)
add_library(
    native-lib
    SHARED
    ${DIR_SRCS})

target_link_libraries(native-lib imported-lib)

2)Android6.0版本以后

在Android6.0及以上版本,如果使用上节的方法添加预编译动态库的话,会有问题,我们可以使用另外一种方式来配置:

# set命令定义一个变量
# CMAKE_C_FLAGS:c的参数,会传递给编译器
# 如果是c++文件,需要用CMAKE_CXX_FLAGS
# -L:库的查找路径
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -L[so所在目录")

14、添加头文件目录

为了确保CMake可以在编译时定位头文件,使用include_directories,相当于g++选项中的-I参数。这样就可以使用#include <xx.h>,否则需要使用#include "path/xx.h"

cmake_minimum_required(VERSION 3.4.1)
# 设置头文件目录
include_directories(<文件目录>)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -L[so所在目录]")
aux_source_directory(. DIR_SRCS)
add_library(
    native-lib
    SHARED
    ${DIR_SRCS})
target_link_libraries(native-lib imported-lib)

15、build.gradle配置

可以在gradle中使用arguments设置一些配置:

android{
    defaultConfig{
        cmake{
            // 使用的编译器clang/gcc
            // cmake默认就是 gnustl_static
            arguments "-DANDROID_TOOLCHAIN=clang","-DANDROID_STL=gnustl_static"
            // 指定cflags和cppflags,效果和cmakelist使用一样
            cFlags ""
            cppFlags ""
            // 指定需要编译的cpu架构
            abiFilters "armeabi-v7a"
        }
    }
    externalNativeBuild{
        cmake{
            // 指定CMakeLists.txt文件相对当前build.gradle的路径
            path "xxx/CMakeLists.txt"
        }
    }
}

三、实操

官网:https://www.fmod.com/
SDK下载页面:https://www.fmod.com/download

需要先注册登录后,才能下载

1、库文件集成

在fmod官网下载好 fmodstudioapi20000android.tar.gz 后,解压,找到 api/core目录,其中inc是fmod的头文件,lib是fmod预编译好的so和jar文件。

1)集成so与jar

2)集成.h头文件

2、CMakeLists.txt文件配置:

fmod提供的so文件属于预编译库,需要在自己的工程中集成fmod并使用,大概可分为2步骤:

  1. 指定头文件目录。方便在native-lib.cpp中可以使用include <fmod.hpp>
  2. 指定预编译库目录。让native-lib在编译时,可以成功链接fmod。
# 指定cmake最小支持版本
cmake_minimum_required(VERSION 3.4.1)

# 设置头文件目录
include_directories(${CMAKE_SOURCE_DIR}/inc)

# 设置第三方so库路径(android6.0以后需要这样设置)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/../jniLibs/${CMAKE_ANDROID_ARCH_ABI}")

# 添加一个库,根据native-lib.cpp源文件编译一个native-lib的动态库
add_library(
        native-lib
        SHARED
        native-lib.cpp)

# 查找系统库,这里查找的是系统日志库,并赋值给变量log-lib
find_library(
        log-lib
        log)

# 设置以来的库(第一个参数必须为目标模块,顺序不能换)
target_link_libraries(
        native-lib
        fmod
        fmodL
        ${log-lib})

${CMAKE_SOURCE_DIR} 可以获取到CMakeLists.txt文件当前所在目录路径。

3、build.gradle配置

因为该工具只在模拟器上运行,所以只需要x86平台的so库文件,可以使用 abiFilters 进行过滤。

手机一般都是arm平台,可根据实际情况修改abiFilters。

android {
    defaultConfig {
        ...
        externalNativeBuild {
            cmake {
                cppFlags ""
                abiFilters "x86" // 指定本地库的cpu架构
            }
        }
        ndk {
            abiFilters "x86" // 指定第三方库的cpu架构
        }
    }
    externalNativeBuild {
        cmake {
            path "src/main/cpp/CMakeLists.txt"
            version "3.10.2"
        }
    }
}

4、编写native-lib.cpp

使用日志输出fmod版本号:

#include <jni.h>
#include <string>
#include <android/log.h>
#include <fmod.hpp>

using namespace FMOD;

extern "C" JNIEXPORT jstring JNICALL
Java_com_lqr_cmakefmod_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    System *system;
    System_Create(&system);
    unsigned int version;
    system->getVersion(&version);
    __android_log_print(ANDROID_LOG_ERROR, "TEST", "FMOD Version: %08x", version);

    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

最终,在触发stringFromJNI()方法后,就可以在控制台看到fmod版本号的输出了。

欢迎关注微信公众号:全栈行动
上一篇下一篇

猜你喜欢

热点阅读