frankfan的胡思乱想

学海无涯,回头是岸

NDK

编译链接与CMake & NDK中的静态库和动态库

C/C++是编译链接型语言,程序(源码)写完后,需要将源码编译,然后链接,最终生成可执行文件。而脚本语言则在源码完成后直接交给解释器执行即可。因此我们说,C/C++是在工程实践中需要投入「额外」时间成本的,并且成本不低。并且要命的是很多时候这些因为编译链接所花去的时间是沉默成本,也就是说你昨天将源码编译链接生成可执行文件所耗费的时间,到了今天你修改源码后仍旧需要再次编译链接,显然,这对于大型工程(有众多源码文件)是不可接受的。

​ 因此远古大神们想到的解决方案就是,「将工程拆件成小工程」,具体而言就是将1个c/c++源码文件拆分成n个c/c++源码文件,这样的好处就是修改的粒度更小了,以前是哪怕只修改一行源码,整个项目都需要重新「生成」过,而现在可以针对性的修改那些被拆分的源文件,剩下的问题是怎么将n个源文件「组合」起来生成一个可执行文件,远古大神们给出的方案是「编译然后链接」,将n个源码文件每一个都单独「编译」,针对性的生成n个中间文件linux中被称为.o文件,windows中被称为.obj文件),然后将这n个中间文件「链接」起来生成一个文件(可执行),显然这两步尤其各自的意义。将源码编译后生成中间文件,只要源码没有再次修改,那么这个中间文件就可以不用修改一直用,昨天编译出的中间文件可以给今天用,昨天花的时间今天就不用额外再浪费了,编译能让源码「复用」的意义可见一斑。而链接的意义就在于将各种中间文件「粘合」起来生成一个新的最终文件(通常这就是我们的可执行文件)

复用」是减少时间成本的秘诀所在,那么,具体怎么复用的呢?

//my_project.cpp
#include<iostream>
using namespace std;
int my_add(int a,int b){
  return a + b;
}

int my_sub(int a,int b){
  return a - b;
}

int main(int argc,char*[]argvs){
  int ret = my_add(11,22);
  int ret2 = my_sub(22,11);
  cout<<ret<<endl;
  return 0;
}

通常,my_addmy_sub这类功能成熟的源码不会反复修改,因此我们认为这部分源码是稳定的,可以单独成为一个源码文件

//my_math.cpp
int my_add(int a,int b){
  return a + b;
}

int my_sub(int a,int b){
  return a - b;
}
//my_project.cpp
#include<iostream>
#include "my_math.h"
using namespace std;
int main(int argc,char*[]argvs){
  int ret = my_add(11,22);
  int ret2 = my_sub(22,11);
  cout<<ret<<endl;
  return 0;
}

这样,1份源码文件就被我们拆分成了2份,通过分别对这2个源码文件进行编译,我们会得到2个中间文件,my_math.omy_project.o ,最终通过链接可得到可执行文件。

其中我们认为my_project是用来执行业务逻辑的工程,而my_math是单纯提供功能,这种单纯提供功能的被我们称之为「」,不过实践中库通常是更多的.o集合被打包成一个归档文件。在从编译到库的过程中,实际可以有2种类型

  • 静态库
  • 动态库

这两种库的本质区别在于「库代码是何时加载到进程中的」。

静态库是当程序位于磁盘上没运行起来还是一个静态文件时,就已经与「程序代码」混合到一起了;而动态库则是直到程序运行起来,才被加载到进程中,甚至直到程序代码显式调用dlopen函数,才被加载到进程中。

对于静态库,其实以上的源码文件划分方式并不合理,两个函数my_addmy_sub被放到同一个源码文件my_math中,编译后生成一个.o文件,这样的坏处在于有可能my_project程序只调用了my_add这1个函数,但链接时.o文件的所有内容都要参与链接,最终生成的可执行文件臃肿而无必要。因此合理的做法是每个函数都拆分为一个单独的.cpp文件,这样就会生成2个.o文件,这2个文件打包成为.a库文件后,就能做到「按需加载」,避免无意义的可执行文件增大。而动态库是无法做到这一点的。

到现在我们面临的唯一问题是,怎么将数量众多的源码文件编译为中间文件,并且生成可执行文件或者制作成库文件?手工执行编译链接器命令是不可能的(实在太麻烦了),此时,「构建工具」诞生了。

构建工具是用来帮助我们将源码构建成库或者可执行文件的,背后是一系列工具链的支持。

本章我们学习CMake这个构建工具的使用。

Demo文件夹下:
main.cpp
util.cpp
parse.cpp
math/
	my_add.h 
	my_add.cpp
	my_sub.h
	my_sub.cpp
	CMakeLists.txt
CMakeLists.txt

工程结构如上图所示,Demo为项目根目录文件夹

其中有2个CMakeLists.txt文件,一个用来构建main可执行文件,另一个用来构建my_math静态库

根目录下的CMakeLists.txt内容如下

# CMake 最低版本号要求
cmake_minimum_required (VERSION 2.8)
# 项目信息
project (Demo)
# 查找当前目录下的所有源文件
# 并将名称保存到 DIR_SRCS 变量
aux_source_directory(. DIR_SRCS)#这个的意义是 aux_source_directory(file_dir VAR)给定一个文件路径(该路径是源文件的所在处),然后给定一个变量,这个变量用来表示这些源文件的位置,后续可用;在本例中,「.」表示当前目录下的所有源文件,同时将该值赋予DIR_SRCS变量
# 添加 math 子目录
add_subdirectory(math)
# 指定生成目标 
add_executable(Demo ${DIR_SRCS})#将DIR_SRCS所表示的路径下的所有源文件参与编译,生成可执行文件Demo,使用变量的好处在于不用一个个的添加源文件add_executable(Demo main.cpp util.cpp parse.cpp),这样的方式如果源文件太多就跪了...
# 添加链接库
target_link_libraries(Demo MathFunctions)#这个用来添加构建Demo项目时所依赖的其他二进制库,第11行用来生成Demo可执行文件,而生成可执行文件Demo的源码中使用了math目录下的函数,而math/目录下的源文件被用来生成了一个静态库,这个库就是MathFunctions

math/目录下的CMakeLists.txt用来构建MathFunctions静态库,内容如下

# 查找当前目录下的所有源文件
# 并将名称保存到 DIR_LIB_SRCS 变量
aux_source_directory(. DIR_LIB_SRCS)
# 生成链接(静态)库(将STATIC换成SHARED,则编译为动态库)
add_library (MathFunctions STATIC ${DIR_LIB_SRCS})

当项目使用开源第三方库时,项目结构如下

Demo文件夹下
src/main.cc tool.cc parse.cc ...
ext/jsoncpp/
	include/
	lib/
CMakeLists.txt

Demo作为根文件夹,下有一个源码文件夹src有一个第三方库文件夹ext,里面放了一个第三方源码jsoncpp文件夹,jsoncpp的源码文件放在lib目录下,头文件放在include目录下

#指明CMake的版本
cmake_minimum_required (VERSION 2.8)
project(Demo)

#指明编译构建时使用的c++版本
add_definitions(-std=gnu++11)

#EXECUTABLE_OUTPUT_PATH和PROJECT_BINARY_DIR都是CMake的环境变量,EXECUTABLE_OUTPUT_PATH表示可执行文件最终的输出目录,而PROJECT_BINARY_DIR则表示构建过程中中间文件所在的路径,在当前的结构中,这个路径就是指Demo根目录

#set用来设置变量值(或者利用环境变量来指定值
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)#这个的意思就是指将最终可执行文件输出到Demo/bin文件夹下

#CMAKE_SOURCE_DIR环境变量,指当前源码的顶层目录,这条用来指明第三方源码的头文件路径
include_directories(${CMAKE_SOURCE_DIR}/ext/jsoncpp/include)

#指明去哪个(哪些)目录下寻找要链接的库
link_directories(${CMAKE_SOURCE_DIR}/ext/jsoncpp/lib)

#查找当前目录下的所有源文件
#并将名称保存到 DIR_SRCS 变量
aux_source_directory(src SRC)

#编译src目录下的源码文件,用以生成可执行文件demo
add_executable(demon ${SRC})

#将demo kmsjsoncpp这些中间文件链接起来,生成demo
target_link_libraries(demon kmsjsoncpp)

说明:生成动态库后,当在Android开发中我们ndk需要使用这个动态库时,是很难使用静态加载的。也就是说,如果我们通过CMake在编译时就链接动态库,最终生成可执行文件,那么在Android中这个程序是无法执行的(除非这个动态库是预置的,而非我们自制的),因为Android只会去加载libs/armeabi目录下的动态库,而这个目录是禁止写操作的(root也没用),这时候我们通常两种选择是:

  • 做静态库链接(直接自制静态库)
  • 动态加载来使用动态库(dlopen)

这两种方式都是比较快捷方便的

posted on 2021-12-28 00:10  shadow_fan  阅读(92)  评论(0)    收藏  举报

导航