本文为博主原创文章,转载请标明出处。AndroidStudio 进行 JNI / NDK 开发:初步配置及使用-CSDN博客
java代表java语言,
native代表当前程序运行的本地环境,一般指windows/linux,而这些操作系统都是通过C/C++实现的,所以native通常也指C/C++语言,
interface代表java跟native两者之间的通信接口,
Java Native Interface(Java 本地编程接口),一套编程规范,它提供了若干的 API 实现了 Java 和其他语言的通信(主要是 C/C++)。Java 可以通过 JNI 调用本地的 C/C++ 代码,本地的 C/C++ 代码也可以调用 java 代码。Java 通过 C/C++ 使用本地的代码的一个关键性原因在于 C/C++ 代码的高效性。
Native Development Kit(本地开发工具),一系列工具的集合,提供了一系列的工具,帮助开发者快速开发 C/C++,极大地减轻了开发人员的打包工作。这套工具集允许为 Android 使用 C 和 C++ 代码。
CMake一款外部构建工具,可与 Gradle 搭配使用来构建原生库。如果只计划使用 ndk-build,则不需要此组件。
LLDB一种调试程序,Android Studio 使用它来调试原生代码。
AS2.2之前的传统创建方式:(已淘汰)
AS2.2之后的ndk cMake创建方式:见 第三条目
在新建项目时勾上Include C++ support 这行:
在向导的 Customize C++ Support 部分,有下列自定义项目可供选择:
C++ Standard:使用下拉列表选择使用哪种 C++ 标准。选择 Toolchain Default 会使用默认的 CMake 设置。
Exceptions Support:如果希望启用对 C++ 异常处理的支持,请选中此复选框。如果启用此复选框,Android Studio 会将 -fexceptions 标志添加到模块级 build.gradle文件的 cppFlags中,Gradle 会将其传递到 CMake。
Runtime Type Information Support:如果希望支持 RTTI,请选中此复选框。如果启用此复选框,Android Studio 会将 -frtti 标志添加到模块级 build.gradle文件的 cppFlags中,Gradle 会将其传递到 CMake。
主要项目结构目录
cpp 文件夹:用于编写 C/C++代码
CMakeLists.txt:CMake 脚本配置文件
配置 build.gradle 文件
要在哪个类运用 JNI ,就得加载相应的动态库
快速生成代码:Alt + Enter
( 我在kotlin类中创建了一个external函数,如何在native-lib.cpp文件里生成对应关联的函数)
鼠标选中external fun getBytes( bytes:ByteArray):ByteArray ,点击快捷键 Alt+Enter 弹出提示,Create jni function getBytes 就会在native-lib.cpp文件里生成对应的jni 函数
在 native-lib.cpp 中声明后记得在方法名的上面一行添加 extern"C",否则执行后会找不到该方法。
新建 C/C++ 源代码文件fmf_jni.cpp fmf_jni2.cpp,要添加到 CMakeLists.txt 文件中
直接在 .cpp 文件里面加断点 ; debug 运行就好了 ;
第一步,将native-lib. cpp 文件夹下的文件名 修改 为 fmf_jni.cpp
第二步,将此句 System.loadLibrary("native-lib"); 的名字修改 fmf_jni
第三步,将 CMakeLists.txt 文件里面所有 "native-lib" 的字段修改
然后先 Clean Project ,再重新编译一下项目( Ctrl + F9)(点击锤子 不是 Rebuild Project )
找到so文件的位置直接导出来 (E:\Android\NdkDemo\myNativeDemo\app\build\intermediates\cmake\freeMinApi23Debug\obj\arm64-v8a\libfmfjni.so)
你的build.gradle 配置ndk配置几个so类型 就打几个so类型库
创建新源文件(.cpp / .c)
参考新建项目的方式在 src/main 路径下创建 cpp 文件夹 ; (也可在其他目录下创建,在第二点会配置这个路径);
在 cpp 文件夹下创建你的 .cpp / .c 源文件 ;
添加预构建库的步骤与为 CMake 指定其他要构建的原生库的步骤相似。不过,由于库已构建,因此您需要使用 IMPORTED 标志指示 CMake 您只想要将此库导入到您的项目中:
然后,您需要使用 set_target_properties() 命令指定库的路径,具体命令如下所示。
某些库会针对特定的 CPU 架构或应用二进制接口 (ABI) 提供单独的软件包,并将其整理到单独的目录中。此方法既有助于库充分利用特定的 CPU 架构,又能让您只使用所需的库版本。如需向 CMake 构建脚本添加库的多个 ABI 版本,而不必为库的每个版本编写多个命令,您可以使用 ANDROID_ABI
路径变量。此变量使用的是 NDK 支持的一组默认 ABI,或者您手动配置 Gradle 而让其使用的一组经过过滤的 ABI。例如:
- 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/${ANDROID_ABI}/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}
- )
具体流程步骤:
(1)新建资源文件夹 jniLibs(貌似在 libs中也行,只要在 CMakeLists.txt中添加路径指示)
(2)在 CMakeLists.txt中添加路径指示
Ps:这里要注意两个地方
① 在定义库的名字时,不要加前缀 lib 和后缀 .so,不然会报错:java.lang.UnsatisfiedLinkError: Couldn’t load xxx : findLibrary【findLibrary returned null错误: http://blog.csdn.net/treasure3334/article/details/17170927】
② ABI 文件夹上面不要再分层,直接用“jniLibs/${ANDROID_ABI}/”的格式,不然也会报错
见MyCMakeListStudyDemo/HELLO案例:
- # 引入第三方 .so文件,要添加到 CMakeLists.txt 文件中
- #添加预构建库的步骤与为 CMake 指定其他要构建的原生库的步骤相似。不过,由于库已构建,因此您需要使用 IMPORTED 标志指示 CMake 您只想要将此库导入到您的项目中:
- add_library(gperf # 在定义库的名字时,不要加前缀 lib 和后缀 .so
- SHARED
- IMPORTED
- )
-
- #使用 set_target_properties() 命令指定第三方libUVCCamera.so库的路径
- #如需向 CMake 构建脚本添加库的多个 ABI 版本,而不必为库的每个版本编写多个命令,您可以使用 ANDROID_ABI 路径变量
- set_target_properties(
- gperf
- PROPERTIES IMPORTED_LOCATION
- ${PROJECT_SOURCE_DIR}/libs/${ANDROID_ABI}/libgperf.so
- )
-
- #为了让 CMake 能够在编译时找到头文件,您需要使用 include_directories() 命令指出第三libgperf.so库 的头文件gperf.h的路径:
- #android 官方Developers 给出的案例
- include_directories( ${PROJECT_SOURCE_DIR}/libs/include )
- message("输出 头文件gperf.h的路径 =${PROJECT_SOURCE_DIR}/libs/include") #E:/Android/NdkDemo/MyCMakeListStudyDemo/HELLO/src/main/libs/include
-
-
-
-
-
-
-
-
- target_link_libraries(srcmain # 目标库文件(libsrcmain.so库)(与 上面的add_library的库名称 srcmain 一定要相同)
- PUBLIC
- #todo 给目标库文件(libsrcmain.so库)添加一个自定义依赖库(liblibhello.so库)
- # todo PUBLIC libsrcmain.so库里的源文件 main.cpp 可以直接调用 liblibhello.so库里的源文件 hello.cpp里的sayHello(const char* name)函数
- libhello
- libusb100 #todo 将导入的第三方库libusb100.so库 关联到我的目标库文件libsrcmain.so库
- gperf #todo 将导入的第三方库libgperf.so库(这个so库里只有一个gperf.c文件) 关联到我的目标库文件libsrcmain.so库
- ${log-lib} # todo 给目标库文件(libsrcmain.so库) 添加依赖库日志库liblog.so,就可以调用里面的log日志函数了
- )
如果想要构建多个 CMake 项目并在 Android 项目中包含这些 CMake 项目的输出,您可以使用一个 CMakeLists.txt
文件(即您关联到 Gradle 的那个文件)作为顶级 CMake build 脚本,并添加其他 CMake 项目作为此 build 脚本的依赖项。
以下顶级 CMake build 脚本会使用 add_subdirectory() 命令将另一个 CMakeLists.txt
文件指定为 build 依赖项,然后关联其输出,就像处理任何其他预构建库一样。
通常在CMakeLists.txt文件中,使用最多的是命令,譬如上例中的 cmake_minimum_required project 都 是命令;命令的使用方式有点类似于 C 语言中的函数,因为命令后面需要提供一对括号,并且通常需要我们提供参数,多个参数使用空格分隔而不是逗号“,”,这是与函数不同的地方。命令的语法格式如下所示:
command(参数 1 参数 2 参数 3 ...)
不同的命令所需的参数不同,需要注意的是,参数可以分为必要参数和可选参数(通常称为选项),很多命令都提供了这两类参数,必要参数使用表示,而可选参数使用[参数]表示,譬如 set 命令:
set 命令用于设置变量,第一个参数和第二个参数是必要参数,在参数列表(…表示参 数个数没有限制)的最后可以添加一个可选参数 PARENT_SCOPE(PARENT_SCOPE 选项),既然是可选的,那就不是必须的,根据实际使用情况确定是否需要添加。
set( ... [PARENT_SCOPE])
在CMakeLists.txt中,命令名不区分大小写,可以使用大小写字母书写命令名,
这俩的效果是相同的,指定的是同一个命令,并没区别;
- project(HELLO) #小写
- PROJECT(HELLO) #大写
使用 set 命令可以对变量进行设置,
通过 set 命令对变量 MY_VAL 进行设置,将其内容设置为"Hello World!";
那如何引用这个MY_VAL 变量呢?这与 Makefile 是相同的,通过${MY_VAL}方式来引用变量,
- #设置变量 MY_VAL
- set(MY_VAL "Hello World!")
-
- #引用变量 MY_VAL
- message(${MY_VAL}) # 输出Hello World!
LIBRARY_OUTPUT_PATH 和 EXECUTABLE_OUTPUT_PATH 变量则是 cmake 的内置变量,每一个内置变量都有自己的含义,像这样的内置变量还有很多,稍后向大家介绍。
- add_custom_command(TARGET gperf POST_BUILD
- COMMAND "${CMAKE_COMMAND}" -E
- copy "${CMAKE_CURRENT_SOURCE_DIR}/src/gperf.h"
- "${distribution_DIR}/gperf/include/gperf.h"
- COMMENT "Copying gperf to output directory")
add_executable 命令用于添加一个可执行程序目标,并设置目标所需的源文件,
add_executable( [WIN32] [MACOSX_BUNDLE] [EXCLUDE_FROM_ALL] source1 [source2 ...])
只需传入目标名和对应的源文件即可
定义了一个可执行程序目标 hello,生成该目标文件所需的源文件为 1.c、2.c 和 3.c。
需要注意的是,源文件路径既可以使用相对路径、也可以使用绝对路径,相对路径被解释为相对于当前源码路径(注意,这里源码指的是 CMakeLists.txt 文件,因为 CMakeLists.txt 被称为 cmake 的源码,若无特别说明,后续将沿用这个概念!
- #生成可执行文件 hello
- add_executable(hello 1.c 2.c 3.c)
add_library命令添加一个项目so库文件,就是你需要创建的目标so库声明处,并设置目标所需的源文件,该命令定义如下所示:
第一个参数 name 指定库文件目标的名字,
参数 source1…source2 对应源文件列表;
add_library 命令默认生成的库文件是静态库文件,通过 SHARED 选项可使其生成动态库文件
- add_library(
[STATIC | SHARED | MODULE] - [EXCLUDE_FROM_ALL]
- source1 [source2 ...])
- #生成静态库文件 libmylib.a
- add_library(mylib STATIC 1.c 2.c 3.c)
-
- #生成动态库文件 libmylib.so
- add_library(mylib SHARED 1.c 2.c 3.c)
- add_library( # Sets the name of the library.
- dy-register-lib
-
- # Sets the library as a shared library.
- SHARED
-
- # Provides a relative path to your source file(s).//这里可以使用多个文件
- dy_register.cpp
- people.cpp)
-
与 add_executable 命令相同,add_library 命令中源文件既可以使用相对路径指定、也可以使用绝对路径指定,相对路径被解释为相对于当前源码路径。
不管是 add_executable、还是 add_library,它们所定义的目标名在整个工程中必须是唯一的,不可出现两个目标名相同的目标。
find_library()查找第三方库,这个在某个库需要关联其他库进行连接的时候就需要查找到库
- find_library(
- log-lib
- log )
顶层 CMakeLists.txt 文件通过 add_subdirectory 加载子目录 src 下的 CMakeLists.txt文件,
add_subdirectory 命令告诉 cmake 去指定的目录中寻找源码并执行它,有点像 Makefile 的 include,其定义如下所示:
参数 source_dir 指定一个目录,告诉cmake 去该目录下寻找 CMakeLists.txt文件并执行它;
参数binary_dir 指定了一个路径,该路径作为子源码(调用 add_subdirectory 命令的源码称为当前源码或父源码,被执行的源码称为子源码)的输出文件(cmake 命令所产生的中间文件)目录,binary_dir 参数是一个可选参数,如果没有显式指定,则会使用一个默认的输出文件目录;为了后续便于表述,我们将输出文件目录称为 BINARY_DIR。
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
顶层 CMakeLists.txt 文件内容如下所示:
- # 顶层 CMakeLists.txt
- cmake_minimum_required("VERSION" "3.5")
- project("HELLO")
-
- # 告诉 cmake 去 src 目录下寻找子 CMakeLists.txt
- add_subdirectory(src)
src 目录下的 CMakeLists.txt 文件:
- # src 下的 CMakeLists.txt
- add_executable(hello main.c)
通过 add_subdirectory 命令加载、执行一个外部文件夹中的源码,既可以是当前源码路径的子目录、也可以是与当前源码路径平级的目录亦或者是当前源码路径上级目录等等;对于当前源码路径的子目录,不强制调用者显式指定子源码的 BINARY_DIR;如果不是当前源码路径的子目录,则需要调用者显式指定 BINARY_DIR,否则执行源码时会报错。接下来进行测试,譬如工程目录结构如下所示:
顶层 CMakeLists.txt 内容如下:
- # 顶层 CMakeLists.txt
- cmake_minimum_required("VERSION" "3.5")
- project("HELLO")
-
- # 加载 src 目录下的源码main.c
- add_subdirectory(src)
src 目录下的 CMakeLists.txt:
- # src 目录 CMakeLists.txt
- add_executable(hello main.c)
-
- # 加载平级目录 lib 中的源码
- add_subdirectory(../lib)
此时调用 add_subdirectory 加载 lib 目录的源码时并为显式指定 BINARY_DIR,进入到 build 目录下,执行 cmake 命令,发生了报错,而且提示我们 add_subdirectory 命令必须要指定 BINARY_DIR,那我们将 src 目录下的 CMakeLists.txt 进行修改,显式指定 BINARY_DIR,如下所示:
- # src 目录 CMakeLists.txt
- add_executable(hello main.c)
-
- # 加载平级目录 lib 中的源码
- add_subdirectory(../lib output)
从指定的目录dir中查找所有源文件,并将扫描到的源文件路径信息存放到变量variable中
其命令定义如下:
aux_source_directory( )
aux_source_directory 会将扫描到的每一个源文件添加到 SRC_LIST 变量中,组成一个字符串列表,使用分号“;”分隔。
aux_source_directory 既可以使用相对路径,也可以使用绝对路径,相对路径是相对于当前源码路径。
- # 顶层 CMakeLists.txt
- cmake_minimum_required("VERSION" "3.5")
- project("HELLO")
-
- # 查找 src 目录下的所有源文件
- aux_source_directory(src SRC_LIST)
- message("${SRC_LIST}") # 打印 SRC_LIST 变量
-
- # src/1.c;src/2.cpp;src/2.c;src/main.c
分别用于获取/设置目标的属性。
set_target_properties() 命令指定第三方libUVCCamera.so库的路径
- set_target_properties(
- gperf
- PROPERTIES IMPORTED_LOCATION
- ${PROJECT_SOURCE_DIR}/libs/${ANDROID_ABI}/libgperf.so
- )
-
-
- add_library(gperf SHARED IMPORTED)
- #使用 set_target_properties() 命令指定第三方动态库libgperf.so库的路径
- set_target_properties(gperf PROPERTIES IMPORTED_LOCATION ${distribution_DIR}/gperf/lib/${ANDROID_ABI}/libgperf.so )
include_directories:命令用于设置 添加目标库文件的头文件夹的路径路径
每一个 CMakeLists.txt 源码都有自己的头文 件搜索列表
默认情况下会将指定目录添加到头文件搜索列表的最后面,
可以通过设置 CMAKE_INCLUDE_DIRECTORIES_BEFORE 变量为 ON 来改变它默认行为,将目录添加到列表前面。
也可以在每次调用 include_directories 命令时使用 AFTER 或 BEFORE 选项来指定是添加到列表的前面或者后面。
如果使用 SYSTEM 选项,会把指定目录当成系统的搜索目录。
既可以使用绝对路径来指定头文件搜索目录、也可以使用相对路径来指定,相对路径被解释为当前源码路径的相对路径。
include_directories([AFTER|BEFORE] [SYSTEM] dir1 [dir2 ...])
源文件 main.c 中使用了 include 目录下的头文件 hello.h
- # 顶层 CMakeLists.txt
- cmake_minimum_required("VERSION" "3.5")
- project("HELLO")
-
- # 输出include 目录下的头文件 hello.h
- include_directories(include)
-
- add_executable(hello main.c)
- include_directories(
- ${CMAKE_CURRENT_SOURCE_DIR}
- ${CMAKE_CURRENT_BINARY_DIR}
- ${CMAKE_CURRENT_SOURCE_DIR}/include)
默认情况下,include 目录被添加到头文件搜索列表的最后面,通过 AFTER 或 BEFORE 选项可显式指定添加到列表后面或前面:
- # 添加到列表后面
- include_directories(AFTER include)
-
- # 添加到列表前面
- include_directories(BEFORE include)
每一个 CMakeLists.txt 源码都有自己的库文件搜索列表
①link_directories:用于设置库文件的搜索路径,会将指定目录添加到库文件搜索列表 中;
link_directories命令可以使用绝对路径或相对路径指定目录,相对路径被解释为当前源码路径的相对路径。
②link_libraries :用于 设置需要链接的第三方库文件,link_libraries 命令会将指定的第三方库文件添加到链接库列表。
- link_directories(directory1 directory2 ...)
- link_libraries([item1 [item2 [...]]]
- [[debug|optimized|general]
- ] ...)
在 lib 目录下有一个动态库文件 libusb100.so,编译链接 main.cpp源文件时需要链接 libusb100.so;CMakeLists.txt 文件内容如下所示:
src 目录下的 CMakeLists.txt
-
- # src 目录下的 CMakeLists.txt
-
-
-
- # include_directories 命令用来指明libhello目录下的头文件hello.h所在的路径,
- #并且使用到了 PROJECT_SOURCE_DIR 变量,该变量指向了一个路径,从命名上可知, 该变量表示工程源码的目录。
- #include_directories(${PROJECT_SOURCE_DIR}/libhello) # E:/Android/NdkDemo/MyCMakeListStudyDemo/HELLO/src/main/libhello
- # 添加到列表前面
- include_directories(BEFORE ${PROJECT_SOURCE_DIR}/libhello) # E:/Android/NdkDemo/MyCMakeListStudyDemo/HELLO/src/main/libhello
-
-
-
- message("输出 PROJECT_SOURCE_DIR 目录=${PROJECT_SOURCE_DIR}") #PROJECT_SOURCE_DIR=E:/Android/NdkDemo/MyCMakeListStudyDemo/HELLO/src/main
- # todo ${CMAKE_SOURCE_DIR}:表示 CMakeLists.txt的当前文件夹路径
- message("输出当前 CMakeLists的路径是=${CMAKE_SOURCE_DIR}") #CMAKE_SOURCE_DIR=E:/Android/NdkDemo/MyCMakeListStudyDemo/HELLO/src/main
- message("查找 libhello 目录下的头(.h)文件=${PROJECT_SOURCE_DIR}/libhello") # E:/Android/NdkDemo/MyCMakeListStudyDemo/HELLO/src/main/libhello
-
-
-
- ##todo 生成可以执行的文件 由于在android 里没有main.cpp执行的入口的mai(){}函数方法 所以 这行代码注释掉 android里使用add_library
- #add_executable(hello main.cpp)
-
-
- ## 加载平级目录 libhello 中的源码
- #此时调用 add_subdirectory 加载 lib 目录的源码时并为显式指定 BINARY_DIR,进入到 build 目录下,
- #执行 cmake 命令,发生了报错,而且提示我们 add_subdirectory 命令必须要指定 BINARY_DIR,
- #那我们将 src 目录下的 CMakeLists.txt 进行修改,显式指定 BINARY_DIR,如下所示:
-
- #add_subdirectory(${PROJECT_SOURCE_DIR}/libhello output) #todo ok 必须加上output 否则报错
- add_subdirectory(../libhello output) #todo ok 必须加上output 否则报错 # hello.cpp hello.h
-
-
- #todo 这里终于把libs/arm64-v8a/libusb100.so的so库加到cmake里去了
- # link_directories 会将指定目录添加到库文件搜索列表
- #link_directories 命令可以使用绝对路径或相对路径指定目录,相对路径被解释为当前源码路径的相对路径。
- link_directories(${PROJECT_SOURCE_DIR}/libs) # todo E:/Android/NdkDemo/MyCMakeListStudyDemo/HELLO/src/main/libs
- #link_libraries 命令会将指定库文件添加到链接库列表
- #在 lib 目录下有一个动态库文件 libusb100.so,编译链接 main.cpp 源文件时需要链接 libusb100.so;
- # link_libraries 命令也可以指定库文件的全路径(绝对路径 /开头)
- #todo ${ANDROID_ABI}:编译时会自动根据 CPU架构去选择相应的库
- link_libraries( ${PROJECT_SOURCE_DIR}/libs/${ANDROID_ABI}/libusb100.so) #todo E:/Android/NdkDemo/MyCMakeListStudyDemo/HELLO/src/main/libs/arm64-v8a/libusb100.so
-
- message("输出 libusb100.so 路径 =${PROJECT_SOURCE_DIR}/libs/${ANDROID_ABI}/libusb100.so") # todo E:/Android/NdkDemo/MyCMakeListStudyDemo/HELLO/src/main/libs/arm64-v8a/libusb100.so
-
-
-
- add_library(
- hello #库的名字 hello
- SHARED # 动态库(.so库)
- main.cpp
- )
-
- find_library(
- log-lib
- log)
-
-
-
- # target_link_libraries 命令为目标指定依赖库
- # 将 log-lib连接到hello.so库 将 本地的库 libhello 链接到hello.so库
- target_link_libraries(hello
- libhello
- ${log-lib}
- )
-
-
-
- # 查找 libhello 目录下的所有源文件
- aux_source_directory(libhello SRC_LIST)
- # 打印 SRC_LIST 变量
- message("查找 libhello 目录下的所有源文件=${SRC_LIST}")
-
-
-
-
list 命令是一个关于列表操作的命令,譬如获取列表的长度、从列表中返回由索引值指定的元素、将元素追加到列表中等等。
列表这个概念还没给大家介绍,列表其实就是字符串数组(或者叫字符串列表、字符串数组)
LENGTH 选项用于返回列表长度;
GET 选项从列表中返回由索引值指定的元素;
APPEND 选项将元素追加到列表后面;
FIND 选项将返回列表中指定元素的索引值,如果未找到,则返回-1。
INSERT 选项将向列表中的指定位置插入元素。
REMOVE_AT 和 REMOVE_ITEM 选项将从列表中删除元素,不同之处在于 REMOVE_ITEM 将删除给定的元素,而 REMOVE_AT 将删除给定索引值的元素。
REMOVE_DUPLICATES 选项将删除列表中的重复元素。
REVERSE 选项就地反转列表的内容。
SORT 选项按字母顺序对列表进行排序。
- list(LENGTH
- list(GET
[ ...]
-
- list(APPEND
[ ...])
- list(FIND
- list(INSERT
[ ...])
- list(REMOVE_ITEM
[ ...])
- list(REMOVE_AT
[ ...])
- list(REMOVE_DUPLICATES
)
- list(REVERSE
)
- list(SORT
)
message 命令用于打印、输出信息
CMake 如果要像用户展示消息需要可以使用 message() API,类似与 Android 中的 Log 输出。
message([] "message to display" ...)
可选的 mode 关键字用于确定消息的类型,如下:
mode | 说明 |
---|---|
none(无) | 重要信息、普通信息 |
STATUS | 附带信息 |
WARNING | CMake 警告,继续处理 |
AUTHOR_WARNING | CMake 警告(开发),继续处理 |
SEND_ERROR | CMake 错误,继续处理,但跳过生成 |
FATAL_ERROR | CMake 错误,停止处理和生成 |
DEPRECATION | 如果变量 CMAKE_ERROR_DEPRECATED 或 CMAKE_WARN_DEPRECATED 分别启用,则 CMake 弃用错误或警告,否则没有消息。 |
- message("CMAKE_SOURCE_DIR = ${CMAKE_SOURCE_DIR}")
- message(STATUS "PROJECT_SOURCE_DIR = ${PROJECT_SOURCE_DIR}")
- message(WARNING "CMAKE_BINARY_DIR = ${CMAKE_BINARY_DIR}")
- //添加日志打印出来
- message("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA>>>")
- message("当前CMake的路径是:${CMAKE_SOURCE_DIR}")
project命令用于设置工程名称:
- # 设置工程名称为 HELLO
- project(HELLO)
执行这个之后会引入两个变量:HELLO_SOURCE_DIR 和 HELLO_BINARY_DIR,注意这两个变量名的前缀就是工程名称,HELLO_SOURCE_DIR 变量指的是 HELLO 工程源码目录、HELLO_BINARY_DIR 变量指的是 HELLO 工程源码的输出文件目录;我们可以使用 message 命令打印变量,譬如 CMakeLists.txt 内容如下所示:
set 命令用于设置变量
设置变量的值,可选参数 PARENT_SCOPE 影响变量的作用域。
set( ... [PARENT_SCOPE])
- # set 命令
- set(VAR1 Hello) #设置变量 VAR1=Hello
- set(VAR2 World) #设置变量 VAR2=World
-
- # 打印变量
- message("VAR1=${VAR1}")
- message("VAR2=${VAR2}")
set(SRC_PATH ${CMAKE_CURRENT_SOURCE_DIR}/include/SerialPort.h)
字符串列表
通过 set 命令实现字符串列表,
- # 字符串列表
- set(SRC_LIST 1.c 2.c 3.c 4.c 5.c)
-
- # 打印变量SRC_LIST
- message("SRC_LIST=${SRC_LIST}") # SRC_LIST=1.c;2.c;3.c;4.c;5.c
-
-
- #获取列表SRC_LIST的长度
- list(LENGTH SRC_LIST L_LEN)
- message("列表SRC_LIST长度=${L_LEN}") # 列表SRC_LIST长度=5
-
- list(GET SRC_LIST 1 VAR1)
- message("获取列表SRC_LIST中 index=1 的元素=${VAR1}") # 获取列表SRC_LIST中 index=1 的元素= 2.c
-
-
- #列表追加元素
- list(APPEND SRC_LIST hello_world.c)
- message("SRC_LIST=${SRC_LIST}") # SRC_LIST=1.c;2.c;3.c;4.c;5.c;hello_world.c
-
- #列表排序
- list(SORT SRC_LIST)
- message("after sort SRC_LIST=${SRC_LIST}") # after sort SRC_LIST=1.c;2.c;3.c;4.c;5.c;hello_world.c
①target_include_directories 命令为指定目标库文件设置头文件搜索路径,
②target_link_libraries 给你的 目标库文件(.so库)(与 add_library的库名称一定要相同) 设置一个或多个第三方链接库文件(自定义的库,预构建的第三方库或ndk系统库(${log-lib}))
这听起来跟 include_directories 和 link_libraries 命令有着相同的作用,确实如此,它们的功能的确相同,但是在一些细节方面却有不同,关于它们之间的区别稍后再给大家进行解释!
target_include_directories 命令来说 ,SYSTEM、BEFORE 这两个 选项与 include_directories 命令中 SYSTEM、BEFORE 选项的意义相同,
我们重点关注的是 INTERFACE|PUBLIC|PRIVATE 这三个选项有何不同?
- target_include_directories(
[SYSTEM] [BEFORE] -
[items1...] - [
[items2...] ...]) -
- target_link_libraries(
-
- ...
- [
- ...]...)
#PRIVATE:私有的。main.cpp 程序调用了 libhello_world.so,而且 main.cpp 不会调用 hello.cpp world.cpp 中的函数(main.cpp 不知道 hello.cpp world.cpp 的存在),
#它只知道 libhello_world.so 的存在 他只会调用libhello_world.so 里的hello_world.cpp hello_world.h
#PUBLIC:公开的。PUBLIC = PRIVATE + INTERFACE。生成 libhello_world.so 时,在 hello_world.c 和 hello_world.h 中都包含了hello.h 。
# 当使用 PRIVATE 关键字修饰时,意味着包含目录列表仅用于当前目标;
# 当使用 INTERFACE 关键字修饰时,意味着包含目录列表不用于当前目标、只能用于依赖该目标的其它目标,也就是说 cmake 会将包含目录列表传递给当前目标的依赖目标;
# 当使用 PUBLIC 关键字修饰时,这就是以上两个的集合,包含目录列表既用于当前目标、也会传递给当前目标的依赖目标。
- target_include_directories(hello_world # libhello_world.so目标库名称
- PUBLIC //库类型
- hello # libhello.so库
- world # libworld.so库
- )
- target_include_directories(log-lib //目标库名称
- PUBLIC //库类型
- ${CMAKE_CURRENT_SOURCE_DIR}/include //目标include文件夹路径)
# 当使用 PRIVATE 关键字修饰时,意味着 链接库列表 仅用于当前目标;
# 当使用 INTERFACE 关键字修饰时,意味着 链接库列表 不用于当前目标、只能用于依赖该 链接库列表 的其它目标,也就是说 cmake 会将包含 链接库列表 传递给当前目标的依赖目标;
# 当使用 PUBLIC 关键字修饰时,这就是以上两个的集合,包含 链接库列表 既用于当前目标、也会传递给当前链接库列表的依赖目标。
- target_link_libraries(hello_world # libhello_world.so库
- PUBLIC
- hello # libhello.so库
- world # libworld.so库
- ${log-lib})
#表示目标 hello_world 不需要链接 hello 库,但是对于hello_world目标的依赖目标(依赖于hello_world的目标)需要链接 hello 库。
target_link_libraries(hello_world INTERFACE hello)
以上便是笔者对 INTERFACE、PUBLIC、PRIVATE 这三个关键字的概括性理解,所以整出这几个关键 字主要还是为了控制包含目录列表或链接库列表的使用范围,这就是 target_include_directories、 target_link_libraries 命令与 include_directories、link_libraries 命令的不同之处。
target_include_directories()、 target_link_libraries()的功能完全可以使用 include_directories()、link_libraries()来实现。但是笔者建议大家使用 target_include_directories()和 target_link_libraries()。
include_directories()、link_libraries()是针对当前源码中的所有目标,并且还会向下传递(譬如通过 add_subdirectory 加载子源码时,也会将其传递给子源码)。
在一个大的工程当中,这通常不规范、有时还会编译出现错误、混乱,所以我们应尽量使用 target_include_directories()和 target_link_libraries(),保持整个工程的目录清晰。
案例:见MyCMakeListStudyDemo的hello-world模块的几个CMakeList.txt文件的配置
(14)
变量也是 cmake 中的一个重头戏,cmake 提供了很多内置变量,每一个变量都有它自己的含义,通过这个链接地址cmake-variables(7) — CMake 3.5.2 Documentation可以查询到所有的内置变量及其相应的介绍,
在这一份文档中,对变量进行分类,分为:提供信息的变量、改变行为的变量、描述系统的变量、控制编译的变量等等,笔者也按照这个分类给大家介绍一些基本、常用的变量。
顾名思义,这种变量可以提供某种信息,既然如此,那么我们通常只需要读取变量即可,而不需要对变量进行修改:
PROJECT_SOURCE_DIR 变量表示工程的顶级目录,也就是顶层 CMakeLists.txt 文件所在目录;
PROJECT_BINARY_DIR 指的是我们执行 cmake 命令的所在目录,也是顶层 CMakeLists.txt 源码的 BINARY_DIR(输出文件目录)。
- # 输出打印资源目录,与HELLO_SOURCE_DIR 一样
- message("PROJECT_SOURCE_DIR=${PROJECT_SOURCE_DIR}")
- # PROJECT_SOURCE_DIR=E:/Android/NdkDemo/MyCMakeListStudyDemo/HELLO/src/main
-
- message("PROJECT_BINARY_DIR=${PROJECT_BINARY_DIR}")
- #PROJECT_BINARY_DIR=E:/Android/NdkDemo/MyCMakeListStudyDemo/HELLO/.cxx/Debug/264duq5q/arm64-v8a
- # 输出打印 CMake 资源目录,与 PROJECT_SOURCE_DIR 一样
- message("当前顶层 CMakeLists的路径是=${CMAKE_SOURCE_DIR}")
- #当前顶层 CMakeLists的路径是=E:/Android/NdkDemo/MyCMakeListStudyDemo/HELLO/src/main
-
- message("CMAKE_BINARY_DIR=${CMAKE_BINARY_DIR}")
- # CMAKE_BINARY_DIR=E:/Android/NdkDemo/MyCMakeListStudyDemo/HELLO/.cxx/Debug/264duq5q/arm64-v8a
指的是当前源码的路径以及当前源码的 BINARY_DIR,
-
- message("CMAKE_CURRENT_SOURCE_DIR=${CMAKE_CURRENT_SOURCE_DIR}")
- #CMAKE_CURRENT_SOURCE_DIR =E:/Android/NdkDemo/MyCMakeListStudyDemo/HELLO/src/main
-
- message("CMAKE_CURRENT_BINARY_DIR=${CMAKE_CURRENT_BINARY_DIR}")
- #CMAKE_CURRENT_BINARY_DIR=E:/Android/NdkDemo/MyCMakeListStudyDemo/HELLO/.cxx/Debug/264duq5q/arm64-v8a
-
-
记录 cmake 的版本号,如下
- # CMakeLists.txt
- message(${CMAKE_VERSION})
- message(${CMAKE_MAJOR_VERSION})
- message(${CMAKE_MINOR_VERSION})
记录工程的版本号,其实可以给工程设置一个版本号,通过 project()命令进行设置,如下:
- # CMakeLists.txt
- cmake_minimum_required("VERSION" "3.5")
- project(HELLO VERSION 1.1.0) #设置工程版本号为 1.1.0
-
- # 打印
- message(${PROJECT_VERSION})
- message(${PROJECT_VERSION_MAJOR})
- message(${PROJECT_VERSION_MINOR})
两者等价,记录工程的名字:
- # CMakeLists.txt
- cmake_minimum_required("VERSION" "3.5")
- project(HELLO VERSION 1.1.0) #设置工程版本号为 1.1.0
-
- # 打印工程名字
- message("CMAKE_PROJECT_NAME=${CMAKE_PROJECT_NAME}") #CMAKE_PROJECT_NAME=hello
- message(${PROJECT_NAME}) #PROJECT_NAME=hello
顾名思义,意味着这些变量可以改变某些行为,所以我们可以通过对这些变量进行设置以改变行为。
对于 add_library()命令,当没有显式指定生成动态库时(SHARED 选项),默认生成的是静态库;其实 们可以通过 BUILD_SHARED_LIBS 变量来控制 add_library()命令的行为,当将变量设置为 on 时表示使能动态库,则 add_library()默认生成的便是动态库文件;当变量设置为 off 或未设置时,add_library()默认生成的便是静态库文件。测试如下:
- # 顶层 CMakeLists.txt
- cmake_minimum_required("VERSION" "3.5")
- project(HELLO VERSION 1.1.0)
-
- #把源文件hello/hello.c添加到动态库libhello .so里去
- add_library(hello SHARED hello/hello.c)
-
- #把源文件world/world.c添加到动态库libworld .so里去
- add_library(world SHARED world/world.c)
-
-
-
- #todo 或者这样写 on 生成动态库 libhello .so libworld .so
- set(BUILD_SHARED_LIBS on)
- add_library(hello hello/hello.c)
- add_library(world world/world.c)
设置编译类型 Debug 或者 Release。debug 版会生成相关调试信息,可以使用 GDB 进行调试;release 不会生成调试信息:
cmake 会将该变量传递给编译器--sysroot 选项,通常在设置交叉编译时会使用到。
为 find_file()和 find_path()命令指定搜索路径的目录列表。它们分别用于查找文件、路径,我们需要传入一个文件名,find_file()命令会将该文件的全路径返回给我们;而 find_path() 命令则会将文件的所在目录返回给我们。
这两个命令去哪找文件呢?也就是通过CMAKE_INCLUDE_PATH 变量来进行指定 , CMAKE_INCLUDE_PATH 指定了一个目录列表,find_file()、find_path()会去这个目录列表中查找文件。
- # CMakeLists.txt
- cmake_minimum_required("VERSION" "3.5")
- project(HELLO VERSION 1.1.0) #设置工程版本号为 1.1.0
-
- # 设置 CMAKE_INCLUDE_PATH 变量
- set(CMAKE_INCLUDE_PATH ${PROJECT_SOURCE_DIR}/src)
-
- # 查找文件
- find_file(P_VAR 1.c)
- message(${P_VAR})
指定 find_library()命令的搜索路径的目录列表。find_library()命令用于搜索库文件,find_library()将会从 CMAKE_LIBRARY_PATH 变量设置的目录列表中进行搜索。
指定要由 include()或 find_package()命令加载的 CMake 模块的搜索路径的目录列表。
⑺CMAKE_INCLUDE_DIRECTORIES_BEFORE
这个变量在前面给大家提到过,它可以改变 include_directories()命令的行为。include_directories()命令默认情况下会将目录添加到列表的后面,如果将 CMAKE_INCLUDE_DIRECTORIES_BEFORE 设置为 on,则 include_directories()命令会将目录添加到列表前面;同理若将 CMAKE_INCLUDE_DIRECTORIES_BEFORE 设置为 off 或未设置该变量,include_directories()会将目录添加到列表后面。
被 find_program()、find_library()、find_file()和 find_path()命令忽略的目录列表。表示这些命令不会去 CMAKE_IGNORE_PATH 变量指定的目录列表中搜索。
这四个变量描述的是运行 cmake 的主机相关的信息,我们直接打印出来看看即可:
- # 打印信息
- message(${CMAKE_HOST_SYSTEM_NAME})
- message(${CMAKE_HOST_SYSTEM_PROCESSOR})
- message(${CMAKE_HOST_SYSTEM})
- message(${CMAKE_HOST_SYSTEM_VERSION})
这 4 个变量则是用于描述目标主机相关的信息,目标主机指的是可执行文件运行的主机,譬如我们的 ARM 开发板。
- # 打印信息
- message(${CMAKE_SYSTEM_NAME})
- message(${CMAKE_SYSTEM_PROCESSOR})
- message(${CMAKE_SYSTEM})
- message(${CMAKE_SYSTEM_VERSION})
这个变量可用于访问环境变量,用法很简单$ENV{VAR}
- # 访问环境变量
- message($ENV{XXX})
通过$ENV{XXX}访问 XXX 环境变量,我们来测试一下,首先在 Ubuntu 系统下使用 export 命令导出 XXX 环境变量:
- export XXX="Hello World!"
- cd build/
- cmake ..
EXECUTABLE_OUTPUT_PATH 设置可执行文件的输出目录
LIBRARY_OUTPUT_PATH 库文件的输出目录
CMake 中,双引号的作用我们可以从两个方面进行介绍,命令参数和引用变量。
调用命令时,参数可以使用双引号,
- project("HELLO")
-
- 也可以不使用双引号
- project(HELLO)
命令中多个参数之间使用空格进行分隔,而 cmake 会将双引号引起来的内容作为一个整体,当它当成一个参数,假如你的参数中有空格(空格是参数的一部分),那么就可以使用双引号,
- # 第一个 message 命令传入了两个参数
- #打印信息时,会将两个独立的字符串 Hello 和 World 都打印出来 HelloWorld
- message(Hello World)
- # 第二个 message 命令只传入一个参数
- # Hello World
- message("Hello World")
(2) 引用变量
- # CMakeLists.txt
- set(MY_LIST Hello World China)
- message(${MY_LIST}) # Hello;World;China
在 cmake 中可以使用条件判断,条件判断形式如下:
- if(expression)
- # then section.
- command1(args ...)
- command2(args ...)
- ...
- elseif(expression2)
- # elseif section.
- command1(args ...)
- command2(args ...)
- ...
- else(expression)
- # else section.
- command1(args ...)
- command2(args ...)
- ...
- endif(expression)
endforeach 括号中的可写可不写,如果写了,就必须和 foreach 中的一致。
- foreach(loop_var arg1 arg2 ...)
- command1(args ...)
- command2(args ...)
- ...
- endforeach(loop_var)
参数 loop_var 是一个循环变量,循环过程中会将参数列表中的变量依次赋值给他,类似于 C 语言 for 循环中经常使用的变量 i。
- # foreach 循环测试
- foreach(loop_var A B C D)
- message("${loop_var}") # 输出 A B C D
- endforeach()
使用 foreach 可以编译一个列表中的所有元素,
- # foreach 循环测试
- set(my_list hello world china)
- foreach(loop_var ${my_list})
- message("${loop_var}") # hello world china
- endforeach()
(2)foreach 循环之 RANGE 关键字
对于第一种方式,循环会从 0 到指定的数字 stop,包含 stop,stop 不能为负数。
第二种,循环从指定的数字 start 开始到 stop 结束,步长为 step,不过 step 参数是一个可选参数, 如果不指定,默认 step=1;三个参数都不能为负数,而且 stop 不能比 start 小。
- foreach(loop_var RANGE stop)
- foreach(loop_var RANGE start stop [step])
- # foreach 循环测试
- foreach(loop_var RANGE 4)
- message("${loop_var}") #0 1 2 3 4
- endforeach()
- # foreach 循环测试
- foreach(loop_var RANGE 1 4 1)
- message("${loop_var}") # 1 2 3 4
- endforeach()