美文网首页JNI专题工作生活
CMAKE与Android NDK开发

CMAKE与Android NDK开发

作者: 蛋西 | 来源:发表于2019-07-31 19:16 被阅读0次

    在Android Studio 2.2开始,正式支持cmake编译,在与android studio结合之前,cmake就已经作为一个广泛使用的构建系统,应用在许多项目中。通过cmake与ndk,我们可以将c/c++源码编译成动/静态库、可执行程序等,非常的方便。

    认识CMake

    在使用cmake之前,我们需要先了解一下cmake,最直接的了解方式是官网。当然还有tutorial最好需要看一下,这样你就能大概理解cmake的一些用法。如果你还不了解c/c++的编译过程,请自行百度学习,不在本文叙述范围内。

    CMake的基本操作

    在看过官网的资料和tutorial之后,我们需要动手实操一下,如果使用cmake进行编译,这样我们才能更好的掌握cmake的一些用法。对于我们后续cmake与ndk的结合有莫大的帮助。我们这里通过clion作为ide进行demo的学习。

    创建可执行程序

    # 要求最低的cmake版本
    cmake_minimum_required(VERSION 3.14)
    
    # 工程名字 工程语言
    project(cmakedemo C)
    
    # 设置cmake c 的标准c99
    set(CMAKE_C_STANDARD 99)
    
    # 将main.c文件加入到可执行程序cmakedemo中
    add_executable(cmakedemo main.c)
    

    add_executable第一个参数是生成可执行程序名称,第二个参数是源码文件,如果有多个源码文件,依次加入,用空格隔开。最后会生成一个cmakedemo可执行程序。当然,我们日常使用过程中,更多的是将源码编译成动/静态库。
    目录结构如下:

    目录结构

    多源文件编译

    # 要求最低的cmake版本
    cmake_minimum_required(VERSION 3.14)
    
    # 工程名字 工程语言
    project(cmakedemo C)
    
    # 设置cmake c 的标准c99
    set(CMAKE_C_STANDARD 99)
    
    # 方法一:将多个文件加入到可执行程序中编译
    # 将main.c MathFunctions.c文件加入到可执行程序cmakedemo中
    #add_executable(cmakedemo2 main.c MathFunctions.c)
    
    # 方法二:采用目录形式
    # 将当前目录下的文件,都保存在DIR_SRCS变量中
    aux_source_directory(. DIR_SRCS)
    # 将变量代表的文件路径加入到可执行程序中编译
    add_executable(cmakedemo2 ${DIR_SRCS})
    

    CMakeLists.txt文件中,我们有两种方式将多个源码文件加入到编译,方法一将MathFunction.c文件放在add_executable最后,并用空格隔开。方法二中,我们用了一个aux_source_directory,第一个参数表示搜寻的目录,第二个参数DIR_SRCS表示将目录下的文件,表示成变量,并在下面应用。最后在add_executable中加入给变量${DIR_SRCS}
    目录结构如下:

    目录结构

    多级目录

    我们源码的目录结构,不会前两个例子中,都在同一级目录下,经常我们的源码有多级目录。这时候我们应该怎么编译呢?多级目录我们也有两种编译方式。

    # 要求最低的cmake版本
    cmake_minimum_required(VERSION 3.14)
    
    # 工程名字 工程语言
    project(cmakedemo C)
    
    # 设置cmake c 的标准c99
    set(CMAKE_C_STANDARD 99)
    
    ## 方法一:
    ## 将当前目录下的文件,都保存在DIR_SRCS变量中
    #aux_source_directory(. DIR_SRCS)
    ## 将math目录下的文件,都保存在DIR_MATH_SRCS变量中
    #aux_source_directory(./math DIR_MATH_SRCS)
    ## 将变量代表的文件路径加入到可执行程序中编译
    #add_executable(cmakedemo3 ${DIR_SRCS} ${DIR_MATH_SRCS})
    
    # 方法二:
    # 将当前目录下的文件,都保存在DIR_SRCS变量中
    aux_source_directory(. DIR_SRCS)
    # 将math目录加入编译
    add_subdirectory(math)
    # 将变量代表的文件路径加入到可执行程序中编译
    add_executable(cmakedemo3 ${DIR_SRCS})
    # 添加链接库
    target_link_libraries(cmakedemo3 MathFunctions)
    
    ## 方法三:
    ## 将当前目录下的源码,都保存在DIR_SRCS变量中
    #aux_source_directory(. DIR_SRCS)
    ## 将math目录下的文件,都保存在DIR_MATH_SRCS变量中
    #aux_source_directory(./math DIR_MATH_SRCS)
    ## 将DIR_MATH_SRCS保存的文件,都编译进入静态库libMathFunctions.a
    #add_library(MathFunctions ${DIR_MATH_SRCS})
    ## 将变量代表的文件路径加入到可执行程序中编译
    #add_executable(cmakedemo3 ${DIR_SRCS})
    ## 添加链接库
    #target_link_libraries(cmakedemo3 MathFunctions)
    

    方法一将来自目录中的源文件,保存成DIR_MATH_SRCS变量,然后在add_executable中应用即可,这样就把多级目录下的源码都加入到构建中。方法二使用add_subdirectorymath子目录加入编译,这时候math中的CMakeLists.txt文件和源码也将作为一个编译子目录进行处理。target_link_libraries指定cmakedemo3可执行程序将链接MathFunctions库,MathFunctions库将在math子目录中生成。
    math子目录CMakeLists.txt如下:

    aux_source_directory(. DIR_MATH_SRCS)
    add_library(MathFunctions ${DIR_MATH_SRCS})
    

    add_library表示将默认生成libMathFunctions.a静态库。
    目录结构如下:

    目录结构

    方法三是将子目录中的源码,都编译成libMathFuntions.a,最后同样的将静态库链接到目标可执行程序中,与方法二的区别是在通过一个CMakeLists.txt文件,就可以将静态库的编译包含在内,无需像方法二一样在./math目录下写一份CMakeLists.txt文件用于专门编译静态库。方法二和方法三各自有各自的好处,方法二更适合单模块编译,可以将某个目录下的源文件作为一个模块来编译,适合庞大的目录结构与模块层级编译。

    自定义编译选项

    # 要求最低的cmake版本
    cmake_minimum_required(VERSION 3.14)
    
    # 工程名字 工程语言
    project(cmakedemo C)
    
    # 设置cmake c 的标准c99
    set(CMAKE_C_STANDARD 99)
    
    # 加入一个配置头文件,用于处理 CMake 对源码的设置
    configure_file(
            "${PROJECT_SOURCE_DIR}/config.h.in"
            "${PROJECT_BINARY_DIR}/config.h"
    )
    
    # 设置USE_LOCALMATH打开
    option(USE_LOCALMATH "TRUE USE LOCAL MATH LIBRARY" OFF)
    
    if (USE_LOCALMATH)
        include_directories("${PROJECT_SOURCE_DIR}/math")
        add_subdirectory(math)
    
    endif (USE_LOCALMATH)
    
    aux_source_directory(. DIR_SRCS)
    
    # 将变量代表的文件路径加入到可执行程序中编译
    add_executable(cmakedemo4 ${DIR_SRCS})
    # 添加链接库
    if (USE_LOCALMATH)
        target_link_libraries(cmakedemo4 MathFunctions)
    endif (USE_LOCALMATH)
    

    在这里我们加入了一个config.h.in文件,这个文件,主要用来预定义宏,通过config.h.in,在编译之后可以生成config.h文件。config.h.in文件内容如下:

    #cmakedefine USE_LOCALMATH
    

    这里我们还使用到了option,主要是为了在进行编译时,在CMakeLists.txt同级目录下,通过ccmake .,来进行USE_LOCALMATH变量的选择,是否打开

    开关变量
    我们看到,最后有一个USE_LOCALMATH变量,可以用过enter键来选择ON或者OFF,如果是ON,那么在生成的config.h中,预定义宏被打开,如下:
    #define USE_LOCALMATH
    

    main.c中,我们就能够使用该宏定义了

    #include <stdio.h>
    #include <stdlib.h>
    #include <math.h>
    #include "config.h"
    #ifdef USE_LOCALMATH
    #include "math/MathFunctions.h"
    #endif
    
    int main(int argc, char *argv[]) {
        if (argc < 3) {
            printf("Usage: %s base exponent \n", argv[0]);
            return 1;
        }
        double base = atof(argv[1]);
        int exponent = atoi(argv[2]);
    #ifdef USE_LOCALMATH
        printf("Now we use our own Math library. \n");
        double result = power(base, exponent);
    #else
        printf("Now we use the standard library. \n");
        double result = pow(base, exponent);
    #endif
    
        printf("%g ^ %d is %g\n", base, exponent, result);
        return 0;
    }
    

    目录结构如下:


    目录结构

    环境检查

    我们有时候需要在编译过程中,检查系统的环境,是否支持某些函数,这个例子中,我们检查是否编译环境自带pow函数,如果自带pow函数,则使用pow函数,如果没有则使用自定义的power函数。

    # 要求最低的cmake版本
    cmake_minimum_required(VERSION 3.14)
    
    # 工程名字 工程语言
    project(cmakedemo C)
    
    # 设置cmake c 的标准c99
    set(CMAKE_C_STANDARD 99)
    
    # 检查系统是否支持 pow 函数
    include(${CMAKE_ROOT}/Modules/CheckFunctionExists.cmake)
    # 如果pow函数存在,则定义HAVE_POW宏,这个宏可以在下面的if条件中使用,也可以在config.h.in中预定义cmakedefine HAVE_POW
    check_function_exists(pow HAVE_POW)
    
    # 加入一个配置头文件,用于处理 CMake 对源码的设置
    configure_file(
            "${PROJECT_SOURCE_DIR}/config.h.in"
            "${PROJECT_BINARY_DIR}/config.h"
    )
    
    # 如果宏未定义,则引入自定义的power函数
    if (!HAVE_POW)
        include_directories("${PROJECT_SOURCE_DIR}/math")
        add_subdirectory(math)
    endif (!HAVE_POW)
    
    aux_source_directory(. DIR_SRCS)
    
    # 将变量代表的文件路径加入到可执行程序中编译
    add_executable(cmakedemo6 ${DIR_SRCS})
    # 添加链接库
    if (!HAVE_POW)
        target_link_libraries(cmakedemo4 MathFunctions)
    endif (!HAVE_POW)
    

    首先在顶层 CMakeLists.txt 文件中添加 CheckFunctionExists.cmake 宏,并调用 check_function_exists命令测试链接器是否能够在链接阶段找到 pow 函数。如果找到pow函数,则定义HAVE_POW宏,当然,在config.h.in中需要预定义HAVE_POW宏,如下:

    #cmakedefine USE_LOCALMATH
    #cmakedefine HAVE_POW
    #cmakedefine HAVE_LOCALPOWER
    

    随后在生成的config.h中,就会定义上该宏,如下:

    /* #undef USE_LOCALMATH */
    #define HAVE_POW
    /* #undef HAVE_LOCALPOWER */
    

    这时候,就可以在源码中使用宏了。

    #include <stdio.h>
    #include <stdlib.h>
    #include <math.h>
    #include "config.h"
    #ifdef USE_LOCALMATH
    #include "math/MathFunctions.h"
    #endif
    
    int main(int argc, char *argv[]) {
        if (argc < 3) {
            printf("Usage: %s base exponent \n", argv[0]);
            return 1;
        }
        double base = atof(argv[1]);
        int exponent = atoi(argv[2]);
    #ifndef HAVE_POW
        printf("Now we use our own Math library. \n");
        double result = power(base, exponent);
    #else
        printf("Now we use the standard library. \n");
        double result = pow(base, exponent);
    #endif
    
        printf("%g ^ %d is %g\n", base, exponent, result);
    
    #ifdef HAVE_LOCALPOWER
        printf("HAVE_LOCALPOWER . \n");
    #elif defined(HAVE_POW)
        printf("HAVE_POW . \n");
    #endif
        return 0;
    }
    

    这里有个注意点check_function_exists需要在configure_file定义config.h.in之前调用,否则对于config.h.in中预定义的宏无效。同样的,在CMakeLists.txt中也能够使用HAVE_POW宏来判断是否引入math目录,是否将math作为子目录加入编译,最后是否链接MathFunctions静态库。

    添加版本号

    在应用程序中,维护库或者可执行程序的版本号是一个好的习惯,配合changelog,能够很直观的看到库的更新迭代过程。在cmake中我们怎样添加版本号管理呢?

    # 要求最低的cmake版本
    cmake_minimum_required(VERSION 3.14)
    
    # 工程名字 工程语言
    project(cmakedemo C)
    
    # 设置cmake c 的标准c99
    set(CMAKE_C_STANDARD 99)
    
    set(VERSION_MAJOR 1)
    set(VERSION_MINOR 0)
    

    config.h.in文件中,添加预定义

    #define VERSION_MAJOR @VERSION_MAJOR@
    #define VERSION_MINOR @VERSION_MINOR@
    

    这样,在生成的config.h文件中就有VERSION_MAJORVERSION_MINOR的定义,在代码中使用如下:

    printf("major version %d , minor version %d \n", VERSION_MAJOR, VERSION_MINOR);
    

    编译动静态库

    上面我们都是生成可执行程序,如果我们想要生成动态库或者静态库应该怎么做呢?

    动态库

    aux_source_directory(. DIR_MATH_SRCS)
    add_library(MathFunctions SHARED ${DIR_MATH_SRCS})
    

    生成动态库如下:


    动态库

    静态库

    aux_source_directory(. DIR_MATH_SRCS)
    add_library(MathFunctions STATIC ${DIR_MATH_SRCS})
    

    生成静态库如下:

    静态库
    主要区别是在add_library时指定STATIC/SHARED参数即可。

    基本操作总结

    通过以上基本操作,我们了解了如何生成可执行程序,生成动/静态库,如何添加版本号、如何进行环境检查、如何预定义宏、如何对多级目录进行编译。对于cmake,我们已经有了一个大概的了解,后续继续讲一下在android中如何与cmake配合使用,来完成我们的目标。

    CMake与Android

    在android平台中,系统已经为我们内置了很多的原生api供我们链接调用,不同的系统api,android为我们提供了不同的库,具体可以参考Android NDK 原生 API。这些预构建的库,已经存在在android平台上了,我们无需将他们打包到apk中,因为NDK库已经是cmake搜索路径的一部分,所以找到提供库的名字,链接到所需库即可。那我们要怎么做才能使用这些库呢?

    find_library用法

    添加find_library()命令到你的cmake构建脚本用于定位ndk库路径,并且将路径存储变量中。你可以在脚本的其他地方使用这个变量,下面例子是查找android平台的log库,将路径存储在log-lib变量中。

    find_library( # Defines the name of the path variable that stores the
                  # location of the NDK library.
                  log-lib
    
                  # Specifies the name of the NDK library that
                  # CMake needs to locate.
                  log )
    

    接下来我们需要将ndk库,链接到我们的目标程序或者目标库中:

    # Links your native library against one or more other native libraries.
    target_link_libraries( # Specifies the target library.
                           native-lib
    
                           # Links the log library to the target library.
                           ${log-lib} )
    

    这里target_link_libraries含义是将${log-lib} 路径的ndk库,链接到native-lib.so中

    添加预构建的动态库

    添加一个预先构建的库,类似于为CMake指定另一个本地构建库。然而因为库已经预构建,你需要使用IMPORTED
    告诉cmake,你需要引入库到你的构建工程中。

    add_library( imported-lib
                 SHARED
                 IMPORTED )
    

    这里只是指定了引入一个动态库,并且动态库名称存储在本地变量imported-lib中。接着需要设置该动态库imported-lib的属性,首先指定具体库的位置

    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 )
    

    这里set_target_properties()定义了一个imported-lib库的属性IMPORTED_LOCATION,指定了该库的在本地操作系统中的位置,这样,结合上面add_library我们就完整的在cmake中引入了一个动态库,并且存储在imported-lib本地变量中,待后续使用。
    当然,我们引入了动态库还不够,编译时,经常还需要用到动态库的头文件,那么头文件该怎么引入呢?

    include_directories(imported-lib/include/)
    

    include_directories中是头文件在操作系统中的相对路径或者绝对路径,相对路径是对于当前CMakeLists.txt的位置而定。

    添加预构建的静态库

    添加预构建的静态库,与动态库类似,只是在add_libraryset_target_properties中有所不同

    add_library(imported-static-lib STATIC IMPORTED)
    set_target_properties(imported-static-lib PROPERTIES IMPORTED_LOCATION imported-lib/src/${ANDROID_ABI}/libimported-lib.a)
    

    主要区别是静态库在add_library中是STATIC,而动态库是SHARED,静态库会编译进目标动态库中,而动态库,最后编译完apk后,通过APK Analyzer查看,在apk的lib/${ANDROID_ABI}/目录下,有你所链接的动态库。

    编译过程构建静态库

    在编译过程中,可能会存在整个c工程会很庞大,例如笔者目前工作中的一个工程源码就很庞大,有多个不同的模块,组件,多级目录。那这种情况下我们可以将某些组件,先编译成静态库,然后将静态库参与最终目标动态库的编译。参考CMake基本操作->多级目录章节,有三种方法可以参考。

    多工程编译

    多工程编译类似于CMake基本操作->多级目录章节中的方法二,这里就不重新讲。参考示例如下:

    # Sets lib_src_DIR to the path of the target CMake project.
    set( lib_src_DIR ../gmath )
    
    # Sets lib_build_DIR to the path of the desired output directory.
    set( lib_build_DIR ../gmath/outputs )
    file(MAKE_DIRECTORY ${lib_build_DIR})
    
    # Adds the CMakeLists.txt file located in the specified directory
    # as a build dependency.
    add_subdirectory( # Specifies the directory of the CMakeLists.txt file.
                      ${lib_src_DIR}
    
                      # Specifies the directory for the build outputs.
                      ${lib_build_DIR} )
    
    # Adds the output of the additional CMake build as a prebuilt static
    # library and names it lib_gmath.
    add_library( lib_gmath STATIC IMPORTED )
    set_target_properties( lib_gmath PROPERTIES IMPORTED_LOCATION
                           ${lib_build_DIR}/${ANDROID_ABI}/lib_gmath.a )
    include_directories( ${lib_src_DIR}/include )
    
    # Links the top-level CMake build output against lib_gmath.
    target_link_libraries( native-lib ... lib_gmath )
    

    CMake与Android结合总结

    本章主要讲解了cmake与android和结合,如何在android中使用cmake,cmake如何使用android平台自带的系统库,构建动/静态库的过程,以及多工程编译,这里已经基本满足我们日常NDK开发过程中遇到的大部分情况。

    CMake与Gradle

    未完待续......

    相关文章

      网友评论

        本文标题:CMAKE与Android NDK开发

        本文链接:https://www.haomeiwen.com/subject/qepncctx.html