NDK之CMake

1、简介

JNI(Java Native Interface),是方便Java调用C、C++等Native代码所封装的一层接口,相当于一座桥梁。通过JNI可以操作一些Java无法完成的与系统相关的特性,尤其在图像和视频处理中大量用到。

NDK(native development kit)提供了一系列的工具,帮助开发者快速开发C(或C++)的动态库,并能自动将so和java应用一起打包成apk。NDK集成了交叉编译器,并提供了相应的mk文件隔离CPU、平台、ABI等差异,开发人员只需要简单修改mk文件(指出“哪些文件需要编译”、“编译特性要求”等),就可以创建出so,该动态库可以兼容各个平台。

2、CMake

CMake是一个跨平台的安装(编译)工具,通过编写CMakeLists.txt,可以生成对应的makefile或project文件,再调用底层的编译。AS 2.2之后工具中增加了对CMake的支持,官方也推荐用CMake+CMakeLists.txt的方式,代替ndk-build+Android.mk+Application.mk的方式去构建JNI项目.

创建使用CMake构建的项目

开始前AS要先在SDK Manager中安装SDK Tools->CMake,只要勾选Include C++ Support。其中会提示配置C++支持的功能.

一般默认就可以了,各个选项的具体含义:

C++ Standard:指定编译库的环境。
Exception Support:当前项目支持C++异常处理
Runtime Type Information Support:除异常处理外,还支持动态转类型(dynamic casting) 、模块集成、以及对象I/O

工程的目录结构

创建好的工程主Module下直接就有.externalNativeBuild,多出一个CMakeLists.txt,相当于以前的配置文件。并且在src/main目录下多了一个cpp文件夹,里面存放的是C++文件,相当于以前的jni文件夹。这个是工程创建后AS生成的示例JNI方法,返回了一个字符串。后面开发JNI就可以按照这个目录结构。

相应的,build.gradle下也增加了一些配置:

android {
    ...
    defaultConfig {
        ...
        externalNativeBuild {
            cmake {
                cppFlags "-std=c++14 -frtti -fexceptions"
                abiFilters "x86", "arm64-v8a", "armeabi-v7a"
            }
        }
    }
    buildTypes {
        ...
    }
    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }
}

defaultConfig中的externalNativeBuild各项属性和前面创建项目时的选项配置有关,外部的externalNativeBuild则定义了CMakeLists.txt的存放路径。
如果只是在自己的项目中使用,CMake的方式在打包APK的时候会自动将cpp文件编译成so文件拷贝进去。如果要提供给外部使用时,Make Project,之后在libs目录下就可以看到生成的对应配置的相关CPU平台的.so文件。

CMakeLists.txt

CMakeLists.txt可以自定义命令、查找文件、头文件包含、设置变量,具体可见 官方文档。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 编译本地库时我们需要的最小的cmake版本
cmake_minimum_required(VERSION 3.4.1)

#设置生成的so动态库最后输出的路径,一定要在add_library之前设置,否则不会生效
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/libs/${ANDROID_ABI})

# 相当于Android.mk。如果是多次使用add_library,则会生成多个so库;
# 如果想将多个本地文件编译到一个so库中,只要最后一个参数添加多个C/C++文件的相对路径就可以
add_library( # Sets the name of the library.设置编译生成本地库的名字
native-lib

# Sets the library as a shared library.库的类型
SHARED

# Provides a relative path to your source file(s).编译文件的路径
src/main/cpp/native-lib.cpp )

# 添加一些我们在编译我们的本地库的时候需要依赖的一些库,这里是用来打log的库。可以写多个find_library
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 )

# 关联自己生成的库和一些第三方库或者系统库
target_link_libraries( # Specifies the target library.
native-lib

# Links the target library to the log library
# included in the NDK.
${log-lib} )

#设置头文件搜索路径(和此txt同个路径的头文件无需设置),可选
#INCLUDE_DIRECTORIES(${CMAKE_CURRENT_SOURCE_DIR}/common)

#指定用到的系统库或者NDK库或者第三方库的搜索路径,可选。
#LINK_DIRECTORIES(/usr/local/lib)

#添加子目录,将会调用子目录中的CMakeLists.txt
ADD_SUBDIRECTORY(one)
ADD_SUBDIRECTORY(two)

  • cmake_minimum_required(VERSION 3.4.1):指定CMake的最小版本
  • add_library:创建一个静态或者动态库,并提供其关联的源文件路径,开发者可以定义多个库,CMake会自动去构建它们。Gradle可以自动将它们打包进APK中。
    第一个参数——native-lib:是库的名称
    第二个参数——SHARED:是库的类别,是动态的还是静态的
    第三个参数——src/main/cpp/native-lib.cpp:是库的源文件的路径
  • find_library:找到一个预编译的库,并作为一个变量保存起来。由于CMake在搜索库路径的时候会包含系统库,并且CMake会检查它自己之前编译的库的名字,所以开发者需要保证开发者自行添加的库的名字的独特性。
    第一个参数——log-lib:设置路径变量的名称
    第一个参数—— log:指定NDK库的名子,这样CMake就可以找到这个库
  • target_link_libraries:指定CMake链接到目标库。开发者可以链接多个库,比如开发者可以在此定义库的构建脚本,并且预编译第三方库或者系统库。
    第一个参数——native-lib:指定的目标库
    第一个参数——${log-lib}:将目标库链接到NDK中的日志库,

如果想要配置so库的目标CPU平台,可以在build.gradle中设置

1
2
3
4
5
6
7
8
9
10
android {
...
defaultConfig {
...
ndk{
abiFilters "x86","armeabi","armeabi-v7a"
}
}
...
}

set_target_properties

设置目标的一些属性来改变它们构建的方式。

1
2
3
set_target_properties(target1 target2 ...
PROPERTIES prop1 value1
prop2 value2 ...)

使用示例为:

1
2
3
4
5
6
set_target_properties(cocos2d
PROPERTIES
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib"
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib"
VERSION "${COCOS2D_X_VERSION}"
)

为一个目标设置属性。该命令的语法是列出所有你想要变更的文件,然后提供你想要设置的值。你能够使用任何你想要的属性/值对,并且在随后的代码中调用GET_TARGET_PROPERTY命令取出属性的值。

大家在用cmake时,应该经常会用到第三方so库,导入第三方so库中需要使用到set_target_properties,例如这样写:

1
2
3
4
5
6
7
8
9
set_target_properties(

Thirdlib

PROPERTIES IMPORTED_LOCATION

${CMAKE_CURRENT_SOURCE_DIR}/jniLibs/libThirdlib.so

)

CMAKE_CURRENT_SOURCE_DIR 这个变量是系统自定义的,表示CMakeLists.txt文件的绝对路径

包含其他 CMake 项目

如果想要编译多个 CMake 项目并在 Android 项目中包含它们的输出,您可以使用一个 CMakeLists.txt 文件(即您关联到 Gradle 的那个文件)作为顶级 CMake 编译脚本,并添加其他 CMake 项目作为此编译脚本的依赖项。以下顶级 CMake 编译脚本使用 add_subdirectory() 命令将另一个 CMakeLists.txt 文件指定为编译依赖项,然后关联其输出,就像处理任何其他预编译库一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 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 )

3、Android.mk

Android.mk内容示例如下:

1
2
3
4
5
6
7
8
9
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := ndkdemotest-jni

LOCAL_SRC_FILES := ndkdemotest.c

include $(BUILD_SHARED_LIBRARY)

  • LOCAL_PATH := $(call my-dir):每个Android.mk文件必须以定义开始。它用于在开发tree中查找源文件。宏my-dir则由Build System 提供。返回包含Android.mk目录路径。

  • include $(CLEAR_VARS) :CLEAR_VARS变量由Build System提供。并指向一个指定的GNU Makefile,由它负责清理很多LOCAL_xxx。例如LOCAL_MODULE,LOCAL_SRC_FILES,LOCAL_STATIC_LIBRARIES等等。但不是清理LOCAL_PATH。这个清理是必须的,因为所有的编译控制文件由同一个GNU Make解析和执行,其变量是全局的。所以清理后才能便面相互影响。

  • LOCAL_MODULE := ndkdemotest-jni:LOCAL_MODULE模块必须定义,以表示Android.mk中的每一个模块。名字必须唯一且不包含空格。Build System 会自动添加适当的前缀和后缀。例如,demo,要生成动态库,则生成libdemo.so。但请注意:如果模块名字被定义为libabd,则生成libabc.so。不再添加前缀。

  • LOCAL_SRC_FILES := ndkdemotest.c:这行代码表示将要打包的C/C++源码。不必列出头文件,build System 会自动帮我们找出依赖文件。缺省的C++ 源码的扩展名为.cpp。

  • include $(BUILD_SHARED_LIBRARY):BUILD_SHARED_LIBRARY是Build System提供的一个变量,指向一个GUN Makefile Script。它负责收集自从上次调用include $(CLEAR_VARS)后的所有LOCAL_xxxxinx。并决定编译什么类型:
    1)BUILD_STATIC_LIBRARY:编译为静态库
    2)BUILD_SHARED_LIBRARY:编译为动态库
    3)BUILD_EXECUTABLE:编译为Native C 可执行程序
    4)BUILD_PREBUILT:该模块已经预先编译

参考资料

https://blog.csdn.net/wzhseu/article/details/79683045
CMake学习