NDK(五):CMake基础语法及导入静动态库

这篇主要介绍CMake语法学习以及怎样把上一篇文章中编译生成的交叉编译库导入到Android项目中
由于其他原因耽误导致这篇文章间隔这么久才写好,导入的时候才发现上一篇文章中交叉编译后的库有点问题又进行重新修改。这篇文章是NDK系列的最后一篇了,算是大概知道了NDK系列的入门知识。

NDK系列文章

mk

之前是使用,现在Google基本放弃了,都采用CMake

CMake

在学习CMake之前,我们或多或少了解过其他Make工具。其他的Make工具遵循不同的标准到时执行的Makefile格式不同,比如要保证在不同平台下编译,就需要一个跨平台的Make工具,CMake就是一个跨平台的构建工具。
Cmake并不直接构建出最终的软件,通过编写CmakeList.txt文件,根据目标用户的平台进一步生成对应的Makefile文件,从而达到跨平台的目的,如Android Studio就是通过ninja。所以可以这么理解为,编写Cmake语法,通过ninja,生成Makefile。

安装CMake

1
2
3
4
brew install cmake
# 查看版本
cmake -version
cmake version 3.13.2

CMake语法入门

  • 官网语法手册(最好的资料)

    学下CMake语法,可以参考官网的文档,进入官网,然后Resource –> Documentation –> 选择最新的Documentation –> Reference Manuals中的cmake-commands(7)。就可以看到所有的命令了。

    image-20190120213206616

实践

Demo1 单文件

main.c文件

1
2
3
4
5
#include <stdio.h>
int main(){
printf("hello world\n");
return 0;
}

CMakeLists.txt文件

1
2
3
4
5
6
# CMake 最低版本号要求
cmake_minimum_required (VERSION 3.4.1)
# 项目信息(如果不设置该参数也可以正常运行)
project (MyProject1)
# 指定生成目标
add_executable(Demo1 main.c)

利用CMake命令编译生成执行文件

1
2
3
4
5
6
7
# 编译生成Makefile文件
cmake .
# 执行Makefile文件生成可执行文件
make
# 执行可执行文件,输出hello world
./Demo1
hello world
Demo2 多文件

如果源文件有很多个,比如有main.c、hello.c、hello.h等,那么一个个写进去比较麻烦,如

main.c文件

1
2
3
4
5
6
7
#include "hello.h"

int main()
{
hello("world");
return 0;
}

hello.h文件

1
2
3
4
5
6
#ifndef HELLO_H
#define HELLO_H

void hello(const char *name);

#endif //HELLO_H

hello.c文件

1
2
3
4
5
6
#include <stdio.h>

void hello(const char *name)
{
printf("Hello %s!\n", name);
}

CMakeLists.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# CMake 最低版本号要求
cmake_minimum_required (VERSION 3.4.1)
# 项目信息
project (MyProject2)

# 这样多的话,写进去就比较麻烦,导入只需要写源文件,.h可以不写
add_executable(Demo2 main.c hello.c)
# 查找当前目录所有源文件 并将名称保存到 DIR_SRCS 变量
# 注意这样不能查找子目录,也不会自动往子目录找
aux_source_directory(. DIR_SRCS)
# 或者也可以使用如果通配符的方式,引入所有.c文件,同样保存到 DIR_SRCS变量中
file(GLOB DIR_SRCS *.c)

# $() 为引用变量DIR_SRCS的值
add_executable(Demo2 ${DIR_SRCS})
Demo3 多文件多目录

新建文件夹helloDir,把Demo2中的hello.c、hello.h和新建一个新的CMakeLists.txt文件放到该目录下

main.c文件

1
2
3
4
5
6
7
8
// 记得修改头文件的引用路径
#include "helloDir/hello.h"

int main()
{
hello("world");
return 0;
}

CMakeLists.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
# CMake 最低版本号要求
cmake_minimum_required (VERSION 3.4.1)
# 项目信息
project (MyProject3)
# 添加当前目录下的所有源文件
aux_source_directory(. DIR_SRCS)

# 添加 helloDir 子目录下的cmakelist
add_subdirectory(helloDir)
# 指定生成目标
add_executable(Demo3 ${DIR_SRCS})
# 添加链接库,hello为helloDir子目录的生成的链接库名称
target_link_libraries(Demo3 hello)

helloDir/CMakeLists.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
# CMake 最低版本号要求
cmake_minimum_required (VERSION 3.4.1)
# 指定生成目标
aux_source_directory(. DIR_LIB_SRCS)
# 生成链接库,默认编译为静态库
# add_library (hello ${DIR_LIB_SRCS})

# 指定编译为静态库,与上面的一样
add_library (hello STATIC ${DIR_LIB_SRCS})
# 指定编译为动态库
add_library (hello SHARED ${DIR_LIB_SRCS})

add_library (hello STATIC ${DIR_LIB_SRCS})

NDK项目之CMake

通过Android Stuido创建支持NDK的项目,就可以看到,多了一个CMakeList.txt文件。在Android Studio 2.2及其以上,构建原生库的默认工具就是CMake。

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
# 设置cmake最低支持版本
cmake_minimum_required(VERSION 3.4.1)
# 编译library库
add_library(
# 设置编译后的library名称
native-lib
# 设置library模式,STATIC为静态库,如果想编译为动态库,则修改为SHARED
STATIC
# 设置编译使用到的源代码,如果添加了源代码,都需要修改此处添加源代码的路径
src/main/cpp/native-lib.cpp )

# 查找library,因为NDK中已经有一部分预构建库,已经被配置为cmake搜索路径的一部分,所以,可以直接添加库的名称。
find_library(
# 此处为查找名称为log的库,将绝对路径赋值到变量log-lib
log-lib
log )

# 链接library
target_link_libraries(
# 链接自己编写的native-lib库和上面查找到的lib库
# 位置不能更改,native-lib为目标的库,必须放在前面
native-lib
${log-lib} )

# 如果上面不预先使用find_library的话,也可以直接使用
target_link_libraries( native-lib
log)

同时build.gradle文件也多了一些配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
android {
compileSdkVersion 28
defaultConfig {
externalNativeBuild {
// 主要是配置了cmake的命令参数
cmake {
cppFlags ""
// 设置编译c/c++ 源文件的cpu类型
// 如果不设置,连接不同的设备,gralde会自动根据连接的设备,如armeabi-v7a架构的手机,则编译出armeabi-v7a,如模拟器,则编译出x86。
// 由于考虑编译出来后包的大小,一般实际开发中,都只是编译兼容性最多的armeabi-v7a架构
abiFilters 'armeabi-v7a'
}
}
}
externalNativeBuild {
// 主要定义了CMake的构建脚本CMakeLists.txt的路径
cmake {
// 当前的CMakeLists.txt与build.gradle在同一个目录下
// CMakeLists.txt也可以在其他路径下,比如可以在cpp目录下创建一个CMakeList.txt配置cpp目录下的源文件
path "CMakeLists.txt"
}
}
}

添加预编译库

在上一篇文章中,分别对怎样编译Android平台上的静、动态库进行介绍,接下来,就把其编译好的库(即预编译库)放到Android Stuido项目中

引入静态库

引入libmain.a的静态库,在新建armeabi-v7a在cpp文件下,把libmain.a放入其中。

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
cmake_minimum_required(VERSION 3.6)

add_library( native-lib
SHARED
src/main/cpp/native-lib.cpp )

# hello-jni: 变量名 最终会生成的so名字
# SHARED: 动态库 STATIC:静态库
add_library(hello-jni SHARED hello-jni.c)

# 引入预编译库
# Main:引入的库的名字,可以随便定义,但是要和下面的保存一致
# STATIC:声明导入的是动态库或者静态库,STATIC为静态库
# IMPORTED:表示这个库是以导入的方式导入
add_library(Main STATIC IMPORTED)

# 设置导入的路径
# Main:库的名称
# 设置目标属性 导入路径 当前库的路径
# ${ANDROID_ABI}引用的变量,在上面的build.grale中,我们设置abiFilters为armeabi-v7a
set_target_properties(Main PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/src/main/cpp/${ANDROID_ABI}/libmain.a)

find_library( log-lib
log )

target_link_libraries(hello-jni Main)

修改native-lib.cpp文件,修改后如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <jni.h>
#include <string>
#include <android/log.h>
// 使用上面的log库

// 该文件为c++,库为c,因此需要添加extern "C"
extern "C" int main();

extern "C" JNIEXPORT jstring
JNICALL
Java_cn_guidongyuan_studyndk_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
// 如果输出结果与库中main定义的返回值一致,则表示调用成功
__android_log_print(ANDROID_LOG_ERROR,"Log","%d", main());
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}

引入动态库

引入libTest.so动态库,动态库必须放在src/jniLibs/armeabi-v7a(不同CPU架构不同目录)下,否则不会打包进去

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
cmake_minimum_required(VERSION 3.6)

add_library( native-lib
SHARED
src/main/cpp/native-lib.cpp )

find_library( log-lib
log )

# 设置库的查找路径
# set方法 定义一个变量,此处为在原来的CMAKE_CXX_FLAGS变量上,添加-L加库的查找路径
# 因为项目中的native-lib.cpp为C++,因此存在C++则使用CMAKE_CXX_FLAGS,完全没有就使用CMAKE_C_FLAGS
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI} ")

# 链接动态库的名字,库名词为libmain.so,这里必须使用main,就会结合上面设置的库查找路径以及库名称查找到对应的库
target_link_libraries( native-lib
${log-lib}
main)

message("ANDROID_ABI : ${ANDROID_ABI}")
# set 方法 定义一个变量
#CMAKE_C_FLAGS = "${CMAKE_C_FLAGS} XXXX"
# -L: 库的查找路径 libTest.so
# 如果库用c写的,则用CMAKE_C_FLAGS,如果使用c++或者c++和c混合使用,则用CMAKE_CXX_FLAGS
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -L${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI} ")

find_library(log-lib log)
# 此处的Test名字,必须为动态库的名称,查找路径在上面的set设置了
target_link_libraries(hello-jni Test ${log-lib} )

修改native-lib.cpp文件,修改内容与上面静态库一样

参考资料

公众号:亦袁非猿

欢迎关注,交流学习