Android 开发:CMake 使用

CMake简介

CMake
CMake是一个跨平台的安装(编译)工具,可以用简单的语句来描述所有平台的安装(编译过程)。他能够输出各种各样的makefile或者project文件, gcc(GNU Compiler Collection)即为GNU编译器套件,也可以简单认为是编译器(Android NDK从r13起,默认使用Clang进行编译),它可以编译很多种编程语言,包括C、C++、Objective-C、Fortran、Java等。当程序只有一个源文件时,直接就可以用gcc命令编译它。但如果源文件太多,一个一个编译就会显得非常繁琐,于是研发人员想到,为什么不设计一种类似批处理的程序,来批处理编译源文件呢,于是就有了make工具。make是一个自动化编译工具,可以使用一条命令实现完全编译。前提是需要编写一个规则文件,make依据该规则文件来批处理执行编译,这个文件就是makefile。对于一个大工程,编写makefile是件极其复杂的事,手动编写不仅耗时且容易出错,于是研发人员又想到,为什么不设计一个工具,读入所有源文件之后,自动生成makefile呢,于是就出现了CMake工具,它能够输出各种各样的makefile或者project文件,从而帮助开发人员减轻负担。但是随之而来也就是编写cmakelists文件,它是CMake所依据的规则。值得注意的是,相对于编写makefile文件,编写cmakelists文件要简单的多
_

CMake 的特点

  1. 开放源代码,使用类 BSD 许可发布
  2. 能够管理大型项目
  3. 简化编译构建过程和编译过程。Cmake 的工具链非常简单:cmake+make
  4. 可扩展,可以为 cmake 编写特定功能的模块,扩充 cmake 功能
  5. 跨平台,并可生成 native 编译配置文件,在 Linux/Unix 平台,生成 makefile,在Mac 平台,可以生成 xcode,在 Windows 平台,可以生成 MSVC 的工程文件

CMakeLists.txt和Makefile的区别

CMakeLists.txt : CMake是一个跨平台的软件,在很多平台可以使用。一般在windows下,我们会直接使用VS生成项目,在Linux下面,我们也可以使用QT Creater生成项目,但是两个不同平台上面的项目不能相互移植。这就有了CMake的用武之地,我们可以先编写一个CMakeLists.txt文件,将需要的.h和.cpp文件包含进来,然后在不同的平台使用CMake调用各自的编译器生成各自的工程
Makefile: Makefile是linux下面的文件,对于一个包含很多文件的工程,如果直接编译,那么我们就需要使用一些命令将所有的文件都包括进来。如果我们对其中的一些文件稍做修改,那么我们需要重新输入这些命令。Makefile文件就可以很好的解决这个问题,它将所需要的命令都包含在这个Makefile文件中,然后简单的make一下就完成了所有的步骤。

  1. 编译流程图
    图文是网上找的
    (网上找的图片如有雷同纯属巧合请见谅)

CMake环境搭建

  1. Android Studio 搭建
    在这里插入图片描述
  2. 配置 Gradle
    在这里插入图片描述
  3. arguments
    arguments 配置参数后和系统参数可以在 build_command.txt查看
    在这里插入图片描述

CMake 语法

  1. CMake 的基本语法规则

    1. 使用星号 # 作为注释;
    2. 变量使用 ${} 方式取值,但是在 IF 控制语句中是直接使用变量名;
    3. 指令名(参数1 参数2 …),其中参数之间使用空格或分号隔开;
    4. 指令与大小写无关,但参数和变量是大小写相关的;
    5. 注:指令与大小写无关,官方建议使用大写,不过 Android 的 CMake 指令是小写的
    6. 例子如下:
      在这里插入图片描述在这里插入图片描述
  2. project 指令
    语法:project( [CXX] [C] [Java])

    1. 这个指令是定义工程名称的,并且可以指定工程支持的语言(当然也可以忽略,默认情况表示支持所有语言),不是强制定义的。
      例如:project(cmaketest)
    2. 定义完这个指令会隐式定义了两个变量:
      _BINARY_DIR和_SOURCE_DIR
      由下图的例子也可以看到,MESSAGE 指令有用到这两个变量;
      另外 CMake 系统还会预定义了 PROJECT_BINARY_DIR 和 PROJECT_SOURCE_DIR 变量,它们的值和上面 两个的变量对应的值是一致的。不过为了统一起见,建议直接使用PROJECT_BINARY_DIR 和PROJECT_SOURCE_DIR,即使以后修改了工程名字,也不会影响两个变量的使用。
      在这里插入图片描述
      在这里插入图片描述
  3. set 指令
    语法:set(VAR [VALUE])

    1. 这个指令是用来显式地定义变量,多个变量用空格或分号隔开
      例如:set(MY_SRC_LIST main.c test.c)
      注意:当需要用到定义的 SRC_LIST 变量时,需要用 v a r 的 形 式 来 引 用 , 如 : {var}的形式来引用,如: var{MY_SRC_LIST}
      不过,在 IF 控制语句中可以直接使用变量名。
      如图一:设置自定义的so输出目录,不设置默认在图二
      在这里插入图片描述
      在这里插入图片描述
  4. message 指令
    语法:message([SEND_ERROR | STATUS | FATAL_ERROR] “message to display” … )

    1. 这个指令用于向终端输出用户定义的信息,包含了三种类型:
      SEND_ERROR,产生错误,生成过程被跳过;
      STATUS,输出前缀为—-的信息;(由上面例子也可以看到会在终端输出相关信息)
      FATAL_ERROR,立即终止所有 CMake 过程;
  5. add_executable指令
    语法:add_executable(executable_file_name [source])

    1. 将一组源文件 source 生成一个可执行文件。 source 可以是多个源文件,也可以是对应定义的变量
      如:add_executable(untitled main.cpp)
      在这里插入图片描述
  6. add_library 指令
    语法:add_library(libname [SHARED | STATIC | MODULE] [EXCLUDE_FROM_ALL] [source])

    1. 将一组源文件 source 编译出一个库文件,并保存为 libname.so (lib 前缀是生成文件时 CMake自动添上去的)。其中有三种库文件类型,不写的话,默认为 STATIC:
      SHARED: 表示动态库,可以在(Java)代码中使用 System.loadLibrary(name) 动态调用;(生成so库)
      STATIC: 表示静态库,集成到代码中会在编译时调用;(.a静态库)
      MODULE: 只有在使用 dyId 的系统有效(ios),如果不支持 dyId,则被当作 SHARED 对待;
      EXCLUDE_FROM_ALL: 表示这个库不被默认构建,除非其他组件依赖或手工构建
    2. 将第三方库导入,与创建库文件一样在add_library()中第一个参数为库的名字,第二个参数SHARE表示动 态库so,STATIC表示静态库.a,和set_target_properties导入库配合使用
      在这里插入图片描述
      在这里插入图片描述
  7. set_target_properties 指令
    语法: set_target_properties(target1 target2 … PROPERTIES prop1 value1 prop2 value2 …)这条指令可以用来设置输出的名称(设置构建同名的动态库和静态库,或者指定要导入的库文件的路径),对于动态库,还可以用来指定动态库版本和 API 版本。

    1. 导入指定路径的动态库,导入的库需要先用add_library添加
      如:set_target_properties(test1-lib PROPERTIES OUTPUT_NAME ${MY_SOURCE_DIR}/jniLibs/libtest1-lib.so)。

    2.设置输出文件的名称,用来设置同时输出动态库和静态库
    3. #VERSION 指代动态库版本,SOVERSION 指代 API 版本。set_target_properties(test1-lib PROPERTIES VERSION 1.2 SOVERSION 1)
    在这里插入图片描述

  8. add_subdirectory 指令
    语法:add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])

    1. 这个指令用于向当前工程添加存放源文件的子目录,并可以指定中间二进制和目标二进制存放的位置。 EXCLUDE_FROM_ALL参数含义是将这个目录从编译过程中排除。可以通过这个指令编译子目录的工程
      另外,也可以通过 set 指令重新定义 EXECUTABLE_OUTPUT_PATH 和 LIBRARY_OUTPUT_PATH 变量来指定最终的目标二进制的位置(指最终生成的 native-lib 或者最终的共享库,不包含编译生成的中间文件)
      set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/jniLibs)
      在这里插入图片描述
  9. find_library 指令
    语法:find_library( name1 path1 path2 …)VAR 变量表示找到的库全路径,包含库文件名 。例如:将ndk 原生的liblog.so的全路径并设置在变量 log-lib
    在这里插入图片描述

  10. include_directories指令
    语法:include_directories([AFTER | BEFORE] [SYSTEM] dir1 dir2…)

    1. 这个指令可以用来向工程添加多个特定的头文件搜索路径,路径之间用空格分割,如果路径中包含了空格,可以使用双引号将它括起来,默认的行为是追加到当前的头文件搜索路径的后面。
      如果需要调用第三方的so库,需要引用到头文件,一般将头文件统一一个文件夹管理
      在这里插入图片描述
      在这里插入图片描述
  11. target_link_libraries指令
    语法:target_link_libraries(target library <debug | optimized> library2…)

    1. 这个指令可以用来为 target 添加需要的链接的共享库,同样也可以用于为自己编写的共享库添加共享库链接。指令中的是指通过add_executable()和add_library()指令生成已经创建的目标文件。而[item]表示库文件没有后缀的名字。默认情况下,库依赖项是传递的。当这个目标链接到另一个目标时,链接到这个目标的库也会出现在另一个目标的连接线上
      同样,link_directories(directory1 directory2 …) 可以添加非标准的共享库搜索路径。
      还有其他 指令和控制指令等就不介绍了,详细可以查看手册。
      在这里插入图片描述

CMake常用变量

1)CMAKE_BINARY_DIR, PROJECT_BINARY_DIR, _BINARY_DIR
这三个变量指代的内容都是一样的,如果是 in-source 编译模式,指的是工程顶层目录,如果是 out-of-source 编译模式,指的是工程编译发生的目录。

2)CMAKE_SOURCE_DIR, PROJECT_SOURCE_DIR, _SOURCE_DIR
这三个变量指代的内容也是一样的,不论哪种编译方式,都是工程顶层目录。

3)CMAKE_CURRENT_SOURCE_DIR
当前处理的 CMakeLists.txt 所在的路径

4)CMAKE_CURRENT_BINARY_DIR
如果是 in-source 编译,它跟 CMAKE_CURRENT_SOURCE_DIR 一致,如果是 out-of-source 编译,指的是 target 编译目录
使用 ADD_SUBDIRECTORY(src bin)可以修改这个变量的值;
而使用 SET(EXECUTABLE_OUTPUT_PATH < 新路径>) 并不会对这个变量造成影响,它仅仅修改了最终目标文件存放的路径。
5)CMAKE_CURRENT_LIST_FILE
输出调用这个变量的 CMakeLists.txt 的完整路径
6)CMAKE_CURRENT_LIST_LINE
输出这个变量所在的行
7)CMAKE_MODULE_PATH
这个变量用来定义自己的 CMake 模块所在的路径。如果你的工程比较复杂,有可能会自己
编写一些 cmake 模块,这些 cmake 模块是随你的工程发布的,为了让 cmake 在处理
CMakeLists.txt 时找到这些模块,你需要通过 SET 指令,将自己的 cmake 模块路径设
置一下。
比如 SET(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)
这时候你就可以通过 INCLUDE 指令来调用自己的模块了。

8)EXECUTABLE_OUTPUT_PATH 和 LIBRARY_OUTPUT_PATH
分别用来重新定义最终结果的存放目录,前面我们已经提到了这两个变量。

9)PROJECT_NAME
返回通过 PROJECT 指令定义的项目名称。

JNI注册模式

静态注册原理
根据函数名来建立 java 方法与 JNI 函数的一一对应关系;在Java虚拟机加载so库时,如果发现含有上面两个宏定义的函数时就会链接到对应Java层的native方法,那么怎么知道对应Java中的哪个类的哪个native方法呢,我们仔细观察JNI函数名的构成其实是:以Java为前缀,并且用“_”下划线将包名、类名以及native方法名连接起来就是对应的JNI函数了。
其实就是:Java+包名+类名+方法名(native方法)
动态注册
在JNi层实现的,JAVA层不需关心,因为在system.load时就会去调用JNI_OnLoad,有就注册,没有就不注册。动态注册的原理要:JNI 允许我们提供一个函数映射表,注册给 JVM,这样 JVM 就可以用函数映射表来调用相应的函数, 而不必通过函数名来查找相关函数(这个查找效率很低,函数名超级长)流程更加清晰可控,效率更高(framework大多用这个方式)
静态注册
优点: 理解和使用方式简单,使用相关工具按流程操作就行, 出错率低
缺点: 当需要更改类名,包名或者方法时, 需要按照之前方法重新生成头文件, 灵活性不高
动态注册
优点: 灵活性高, 更改类名,包名或方法时, 只需对更改模块进行少量修改, 效率高
缺点: 会由于搞错签名, 方法, 导致注册失败
源码Activity的方法如下:
在这里插入图片描述
源码framework的jni方法如下:
在这里插入图片描述
JNI动态注册和静态注册的简单demo
如图:报红的为动态注册方法
在这里插入图片描述
在这里插入图片描述
动态加载的jni方法:
在这里插入图片描述
在这里插入图片描述
动态方法映射表:
在这里插入图片描述

本文以学习和做笔记为目的,如有不对,不足之处请帮忙指出,谢谢

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值