在之前章节中我们介绍了怎样通过CMake来构建静态库与动态库。

静态库与动态库的优缺点都是相对的,是不同应用场景下的不同选择,在同一标准下很难说出孰优孰劣。

在本节中我们会讨论静态库的使用,动态库的使用以及动态库生成中的一些细节(如函数的导出与隐藏、动态库加载与卸载的回调函数等)

静态库

可以认为静态库是「一堆」.o(编译产物)的打包集合形成.lib文件,在链接时,链接器会根据目标文件所依赖的函数从.lib文件中提取.o文件,参与生成新的目标文件,这里就需要注意,如果库源文件只有一个.c源码文件,那么就只会生成一个.o文件,这个文件与源码文件大小正相关,这样做的坏处在于如果目标文件只依赖了.o中的一个函数(而.o中可能有上百个函数)但链接器依然需要将整个.o文件链接进入目标文件中,徒增不少的大小。

因此正确的做法是每个函数一个.c源文件,这样就是每个函数对应一个.o文件,链接器在链接时就会针对性的链接。

//myMath.h
extern "C"{
  int my_add(int,int);
  int my_sub(int,int);
}
//my_add.cpp
int my_add(int a,int b){
  return a + b;
}
//my_sub.cpp
int my_sub(int a,int b){
  return a - b;
}

文件结构为

/cpp/ src/ my_add.cpp my_sub.cpp myMath.h CMakeLists.txt libtest.cpp CMakeLists.txt

我们的目标是构建一个myMath的静态库,但在cpp目录下引入了libtest.cpp,这样做的目的是因为,单纯的使用ndk下的CMake构建静态库是无法生成的,这个静态库必须要被使用,而这个libtest.cpp的作用就是用来依赖myMath这个静态库,使得最终能够生成静态库文件。

我们自顶向下思考

cpp/目录下的CMakeLists.txt内容为:

cmake_minimum_required(VERSION 3.10.2)
project("myMathProject")

#很重要,添加子目录,链接时链接器会去这个目录中查找
add_subdirectory(src)
#生成可执行文件
add_executable(libtest libtest.cpp)
#指定生成可执行文件时所需要的静态库(链接时的参与方),这里的关键是必须让链接器能够正确找到这些库的所在路径,否则链接失败,相关符号(函数名)无法找到
target_link_libraries(libtest myMath)

cpp/src目录下的CMakeLists.txt主要用来构建myMath静态库

cmake_minimum_required(VERSION 3.10.2)
project("myMath")
#给变量SRC定义为当前目录 .
aux_source_directory(. SRC)
#生成静态库,参与编译的源文件位于SRC(当前目录下的所有源文件都参与编译)
add_library(myMath STATIC ${SRC})

这样我们在app/.cxx/cmake/debug目录下找到相关的架构。

经过以上构建我们生成了libmyMath.a文件,我们新建文件夹myMath,然后加入头文件:

myMath/
	libmyMath.a(前缀lib为构建工具自动添加)
	myMath.h

那么文件夹myMath就是我们对外提供的静态库

现在我们要使用这个静态库,在此之前我们需要确定的是,我们构建的可执行文件架构与提供的静态库架构要是一致的abiFilters

./project
	CMakeLists.txt
	src/
		main.cpp
		CMakeLists.txt
	lib/
		myMath.h
		libmyMath.a

我们构建的目标可执行文件位于src目录下,./project目录下的CMakeLists.txt内容如下:

cmake_minimum_required(VERSION 3.10.2)
project("main")
# 添加子目录
add_subdirectory(src)

src/目录下的CMakeLists.txt内容如下:

project("myndk03")

#CMAKE_SOURCE_DIR是CMake内置变量,表示CMakeLists.txt文件所在位置(NDK中根文件夹是cpp)
set(ROOT_DIR ${CMAKE_SOURCE_DIR})

#指定头文件的搜索路径,myMath.h与main.cpp是不同的路径,不指定库头文件的话链接会出错
include_directories(${ROOT_DIR}/lib)
#同理,指定静态库的搜索路径(${ROOT_DIR}/lib 等价 ./lib)
link_directories(${ROOT_DIR}/lib)
#生成可执行文件
add_executable(main main.cpp)
#指定生成可执行文件所需要的中间文件(库),可以写成libmyMath.a;也可以是myMath,链接时链接器会自动去找libmyMath.a文件
target_link_libraries(main myMath)

动态库的链接过程也与之相同,不过在ndk的开发中,我们通常不使用静态加载动态库的方式,因为动态库加载时是有固定的搜索顺序的,而Android设备的/system/目录是禁止写入的,因此我们通常采用动态加载。

其实无论是动态库链接还是静态库链接,CMakeLists.txt文件的写法基本都是大同小异的。核心就是讲清楚几件事情:

  • 要生成什么目标文件(可执行文件还是静/动态库文件)(add_executable() add_library()
  • 要使用的头文件在哪个路径下(include_directories()
  • 要使用的库文件在哪个路径下(link_directories()
  • 要生成的目标文件名是啥,依赖了哪些库(target_link_libraries()

以上,基本就是CMakeLists.txt的内容「套路」

动态库

动态库的加载分为静态加载动态加载

所谓静态加载是指程序运行时,自动根据链接信息去「默认」的路径下进行搜索,而在安卓中,动态库能够被静态加载的路径只有

  • /system/lib
  • /data/data/packagename/…

/system/lib这个路径是没有权限操作的,因此动态库的静态加载,动态库的路径只能在/data/data/packagename/…下,这对于原生安卓应用是正常的;而如果是Native原生可执行文件则只适合动态加载了

(关于动态库的CMakeLists.txt配置本文不做赘述,与静态库的配置雷同)

动态库的动态加载相较于静态加载有其特有优势,能更细粒度的控制如加载时机等,也可以通过服务器动态下发配置的方式来自由灵活的控制加载,真正的做到代码「插件化」

#include <dlfcn.h>
using namespace std;
int main(){

  	//注意这里的动态库路径是「绝对地址」,RTLD_NOW是指「即刻加载」而非「延迟加载」,所谓即刻加载就是指此刻,就将动态库代码加载入内存中,而延迟加载是指不加载,直到动态库中的函数被使用时才加载(各有优缺点)
    void *handle = dlopen("/data/local/tmp/frankfanDir/libmy_math.so",RTLD_NOW);
  
  	//dlsym是加载相关「符号」函数,返回函数的地址,此处用一个函数直接接受
    int(*add)(int,int) = (int(*)(int,int))dlsym(handle, "my_add");
  
  	//这样就可以直接使用这个函数了
    cout<<"the result is "<<add(11,22)<<endl;
   	
  	//记得要及时卸载掉已加载的动态库
    dlclose(handle);
    return 0;
}

动态库的使用如上所示

动态库番外

//my_math.h
extern "C"{ //避免C++编译时的命名倾轧,这里采用C规则的符号编译规则
    int my_add(int,int);
    int my_sub(int,int);
}
//my_math.cpp
#include "my_math.h"
#include <stdio.h>

//CMake生成动态库时是默认是将所有函数符号导出的,这里采用Clang(gcc)提供的一种源码注解方式,使得开发者可以定制编译器的行为,常用__attribute__(xxx)形式,xxx就是我们要求编译器的行为,这里我们通过这样的形式隐藏「my_add」这个符号(的导出)
int __attribute__ ((visibility ("hidden"))) my_add(int a,int b){
    return a + b;
}

//这是默认情况 __attribute__ ((visibility ("default")));默认情况下是导出符号的,因此通常不用刻意声明
int my_sub(int a,int b){
    return a - b;
}

//在动态库被load时,被__attribute__((constructor))修饰的函数会被调用;只要被修饰了,就会调用,无论有多少个函数,无论其名字是什么
void  __attribute__((constructor)) init1() {
    printf("hello init so\r\n");
}

//在动态库被unload是,被__attribute__((destructor))修饰的函数会被调用;只要被修饰了,就会调用,无论有多少个函数,无论其名字是什么
void __attribute__((destructor)) deint(){
    printf("hello deint so\r\n");
}

以上就是动态库使用中的一些小细节。