NDK-静态库与动态库的使用
在之前章节中我们介绍了怎样通过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");
}
以上就是动态库使用中的一些小细节。
posted on 2021-12-28 00:05 shadow_fan 阅读(462) 评论(0) 编辑 收藏 举报