学前须知
常见问题
1、动态库和静态库的区别是什么?
2、静态库链接到主程序,它存放在什么位置?动态库呢?
3、静态库、动态库与framework的关系?
4、什么是xcframework文件?有什么作用?与framework、.a文件有什么区别?
5、什么是dead code strip?它与-ObjC、-force_load有什么关系吗?在不添加-ObjC、-force_load和all_load等链接指令的情况下能否让APP程序链接Category文件呢?
6、什么是tbd文件?在日常开发中有哪些应用场景?
7、要减小app的体积应该使用静态库还是动态库?为什么?
常用命令
nm:查看符号,常见参数:
-a Display all symbol table entries, including those inserted for use by debuggers.
-g Display only global (external) symbols.
-n Sort numerically rather than alphabetically.
-o Prepend file or archive element name to each output line, rather than only once.
-p Don't sort; display in symbol-table order.
-r Sort in reverse order.
-u Display only undefined symbols.
-U Don't display undefined symbols.
man:可以查看某个命令的用法,比如在终端输入man nm,回车:
这里就显示了nm的使用说明。这里面进入了vim的环境,如果我们想搜索nm下的参数,比如-p,我们可以输入/-p搜索,它会跳到第一个-p的地方,比如有多个-p,只需要按n键,表示跳到下一个-p的位置,按键N表示上一个。
file:查看文件格式。
ar: create and maintain library archives。
libtool:
静态库合并 libtool -static -o libTest.a(输出文件)libTest1.a(合并的静态库1) libTest2.a(合并的静态库2) ,表示的是将libTest1.a和libTest2.a合并生成libTest.a。
lipo:
查看库文件包含的CPU架构
lipo -i xxx.a
合成一个库的两个不同CPU架构的库文件为一个
lipo -create xxx.a xxx.a -output xxx.a
拆分提取一个库的CPU架构
从库中提取armv7架构保存
lipo xxx.a -thin armv7 -output armv7.a
从库中提取arm64架构保存
lipo xxx.a -thin arm64 -output arm64.a
常见的文件格式
- .a文件:是一个静态库文件,是目标文件.o的集合,经过链接可生成可执行文件。
- .dylib文件:是一个动态库文件。
-
.framework文件:Framework是Mac OS/iOS平台特有的文件格式。Framework实际上是一种打包方式,将库的二进制文件、头文件和有关的资源打包到一起,方便管理和分发。Framework有静态库也有动态库,静态库的Framwork = .a+头文件+资源文件 + 签名;动态库的framework=.dylib+头文件+资源文件 + 签名。
我们自己创建的Framework和系统的Framework比如(UIKit.framework)还是有很大的区别的。系统的Framework不需要拷贝到目标程序中,我们自己做出来的Framework哪怕是动态库的,最后也还是要拷贝到APP中(APP和Extension的Bundle是共享的),因此苹果又把这种Framework称为Embedded Framework。
Embedded Framework 开发中使用的动态库会被放入ipa下的framework目录下,基于沙盒运行。不同的APP使用相同的动态库,并不会只在系统中存一份,而是会在多个app中各自打包、签名、加载一份。
- .xcframework文件:XCFramework是苹果官方推荐的、支持的可以方便的表示一个平台的分发二进制库的格式。需要Xcode11以上版本的支持,是为更好的支持Mac Catalys和ARM芯片的macOS,专门在2019年提出的framework的另一种先进格式。
和传统的framework相比:
1、可以用单个.xcframework文件提供多个平台的分发二进制文件;
2、与Fat Header 相比,可以按照平台划分,可以包含相同架构的不同平台的文件;
3、在使用时不需要通过脚本去剥离不需要的架构体系。
- .tbd文件:tbd是text-based stub libraries的简称。本质上是一个YAML描述的文本文件。它的作用是用于记录动态库的一些信息,包括导出的符号、动态库的架构信息、动态库的依赖信息等。用于避免在真机开发过程中直接使用传统的dylib。对于真机来说,由于动态库都是在设备上,在Xcode上使用基于tbd格式的伪framework可以大大减少Xcode的大小。
静态库
什么是静态库?
静态库即静态链接库,可以看成是一组目标文件(.o文件)的集合。即很多目标文件经过压缩打包后形成的文件。静态库在被目标程序链接时代码直接拷贝到目标程序中。Windows下的.lib, Linux和Mac下的.a, Mac独有的.framework。
创建静态库
在iOS中,静态库的格式有.a和framework。这里我们创建一个framework格式的静态库,创建framework的target的时候默认情况向是dynamic,需要我们指定为static:
创建两个类LNPerson和LNStudent,分别在文件LNPerson.h&LNPerson.m 和LNStudent.h&LNStudent.m。
LNPerson.h&LNPerson.m:
@interface LNPerson : NSObject
- (void)say;
@end
@implementation LNPerson
- (void)say
{
NSLog(@"%s", __func__);
}
@end
LNStudent.h&LNStudent.m:
@interface LNStudent : LNPerson
- (void)learn;
@end
编译就可以得到一个静态库文件。
静态库是.o文件的合集
静态库符号包括它的全局符号是以.o文件为单位分开的,所以叫.o文件的集合,经过链接生成可执行文件。使用file命令查看文件格式:
这是一个归档文件。
通过ar -t命令可以列出静态库的所有.o文件:
符号信息
通过命令nm -p查看静态库的符号信息:
这里可以看出静态库实际上是目标文件(.o文件)的集合,它的符号是以.o文件为单位分开的。
静态库链接到主程序app
首先把前面的两个文件的头文件LNPerson.h和LNStudent.h设置为public,然后在主app代码引用,实例化这两个对象然后xcode直接build主程序,通过objdump --macho -t 命令查看主程序的符号信息:
静态库链接到主程序之后它的符号变成了本地符号,实际上是跟主程序app合并在一起了。
-noall_load
xcode 的build默认是-noall_load,-noall_load顾名思义就是不会所有符号的加载,而是链接器链接一个静态库之前去扫描静态库文件,找到需要的代码再进行链接。
-all_load
链接所有符号,不管代码有没有使用,比如上面的例子,即使不用是LNStudent,LNStudent也会被链接到app中:
-force_load
当符号冲突时,链接指定的库。比如我们在创建一个静态库StaticLibrary2,然后创建LNPenson类和LNStudent类,在主app中同时引用两个静态库:
直接xcode build:
这时会出现符号冲突。
我们通过-force_load指定当冲突时使用静态库StaticLibrary2,再次build,冲突解决了:
-ObjC
告诉链接器链接OC相关的代码,包括纯Category的文件。由于OC语言符号链接的基本单位是类,静态库链接时首先会链接本类,而Category是运行时才会被加载的,因此会被静态链接器直接忽略掉,通过-ObjC命令是告知链接器链接所有的OC代码。比如我们实现LNPerson 的Category,在纯Category的文件中:
@interface LNPerson (CateA)
- (void)cat_say;
@end
@implementation LNPerson (CateA)
- (void)cat_say{
NSLog(@"%s", __func__);
}
@end
在app中引用这个category:
查看app中的符号信息:
添加-ObjC命令之后可以正常调用,Category已被链接到主程序:
在不添加-ObjC、-force_load和all_load等链接指令的情况下能否让APP程序链接到Category呢?
一种方法是可以把Category的实现写入到本类的.m文件里面,这样在本类的.o文件被链接的时候Category也会被一起链接到主程序里面。
另一种方法是把Category变成不是纯Category的文件。比如说在Category文件里实现任意的c函数:
#import <StaticLibrary/LNPerson.h>
void testFunc(void);
@interface LNPerson (CateA)
- (void)cat_say;
@end
#import "LNPerson+CateA.h"
void testFunc(void) {
NSLog(@"%s",__func__);
}
@implementation LNPerson (CateA)
- (void)cat_say{
NSLog(@"%s", __func__);
}
@end
然后在本类中调用这个c函数:
#import "LNPerson.h"
#import "LNPerson+CateA.h"
@implementation LNPerson
- (void)say
{
NSLog(@"%s", __func__);
testFunc();
}
@end
在app中引用静态库并调用Category方法:
运行之后没有崩溃的情况。
这个原因是OC符号链接是以类为单位的,但是在这里Category已经不是纯OC代码,还有C函数。有了C函数那么链接器就不得不链接这个.o文件了,自然就把Category也链接进来了。这样做看起来确实很多余,但是有时候还是很有用的,比如说你写了一个静态库,为了避免使用方忘记添加-ObjC关键字导致程序崩溃的问题,那么这种方法是很有效的,对使用方来说比较友好。
dead code striping
dead code strip的作用(Remove functions and data that are unreacheble by entry point or export symbols)不管是.o文件、静态库还是动态库,未被使用代码会被剥离。没有被使用的代码,就是dead code 。xcode的默认情况下是会剥离dead code的:
比如上面的demo,我们改为在app中只使用LNPenson,不使用LNStudent,build后通过命令objdump --macho -t 查看符号表:
这时候的LNStudent已经被剥离掉了。
备注:前面提到的链接指令-noall_load、 -all_load、-force_load和-ObjC都是针对静态库的,跟dead code strip没有任何关系。dead code strip是针对的是.o文件、静态库和动态库。
动态库
什么是动态库?
动态库是一个最终链接完成的镜像文件。与静态库相比,动态库在编译时并不会被拷贝到目标程序中,目标程序中只会储存储指向动态库的引用。等到程序运行时,动态库才会被真正加载进来。格式有.framework、.dylib、.tdb。
创建一个动态库
创建一个动态库DynamicLibrary,并增加两个类LNPerson和LNStudent以及它们对应的文件:
在build setting的Mach-o Type 选择Dynamic Library(xcode默认)类型。然后执行xcode的build,得到DynamicLibrary.framework。
文件格式
通过file命令查看DynamicLibrary.framework/DynamicLibrary的文件格式:
可见是一个已链接的可执行文件。
符号信息
通过nm -p 命令查看DynamicLibrary.framework/DynamicLibrary的符号信息:
通过符号信息可以看出,动态库跟静态库不一样,不一样在于静态的符号是以.o文件为单位分开的,而动态的所有符号都是放在一起的。
动态库链接到主程序app
可见动态库在链接到主程序之后它的符号不像静态库那样变成本地符号。实际上主程序链接动态库是根据动态库的导出符号链接的,链接的动态库符号会统一放到间接符号表中点击了解更多关于符号的信息。
静态库和动态库优缺点的比较
- 静态库:
优点:静态链接阶段会直接链接到目标程序,差不多直接将代码拷贝到目标程序,符号链接编译时确定、程序运行效率高。
缺点:浪费内存和磁盘空间,模块更新困难,而且容易出现符号冲突。 - 动态库:
优点:节省内存空间,使得模块可以动态更新,且由于动态库的二维命名空间,不会出现符号冲突。
缺点:会导致一些性能损失。但是可以优化,比如延迟绑定技术。