静态库与动态库

CPU架构与指令集

Apple相关的cpu指令集主要包括i386, x86_64, armv7, armv7s, arm64。
其中i386和x86_64是intel cpu指令集,armv7, armv7s, arm64是arm cpu指令集。
intel cpu主要优势是高性能,并发处理更快,适合台式机,服务器。
arm cpu主要优势是低功耗,尺寸小,发热小,适合移动设备。

Apple笔记本和台式机一般都是使用intel架构,但从M1处理器开始开始转向arm架构。
Apple移动设备都是使用arm架构,但运行在模拟器上是使用电脑的架构x86_64或i386(M1处理器以前)。

i386,armv7和armv7s的架构是32位,x86_64和arm64架构是64位。
数据类型里面,NSInteger在32位时等同于int,在64位时等同于long。CGFloat在32位时等于float,在64位时等同于double。
可以使用lipo -info + 静态库(动态库)文件完整路径,查看静态库(动态库)支持的架构,经常会出现静态库(动态库)在模拟器上编译失败,是因为该库不支持模拟器架构。支持的架构越多,将会占用更大的内存空间,所以一般用于真机的app不需要支持模拟器架构(i386/x86_64)。lipo LoginSDK.a -thin armv7 -output arm/LoginSDK.a拆分多架构支持的静态库。
可通过宏避免模拟器下运行的库报错。

// 通过模拟器和真机宏避免只支持真机架构的库在模拟器下运行报错,RPSDK人脸识别库不支持模拟器架构。
#if TARGET_IPHONE_SIMULATOR
    // 模拟器执行
#elif TARGET_OS_IPHONE
    // 真机执行
    [RPSDK start:token rpCompleted:rpCompleted withVC:naviController];
#endif

可以在Xcode的Architectures里面设置编译时的架构选择。Yes代表只编译当前选中的模拟器或者当前连接iOS真机的架构,这种编译速度更快;No代表编译当前所有的架构。一般debug状态是模拟器或真机直接连接调试,选择Yes;release状态是打包分发给测试或其他人员使用,选择false。
可以在Edit Scheme里面修改Run,Archive等的编译配置(debug或release)


架构选择

当前所有架构,可修改
相关名词解释

dyld(the dynamic link editor):苹果的动态链接器
tdb格式:从Xcode7开始,使用.tdb格式的系统动态库描述文件来代替原来的.dylib系统动态库文件,tdb全称为text-based stub libraries,本质是一个YAML描述的文本文件。它记录了动态库的符号,架构,库依赖等信息。因为是描述文件,它的大小远小于dylib。

For those who are curious, the .tbd files are new "text-based stub libraries", that provide a much more compact version of the stub libraries for use in the SDK, and help to significantly reduce its download size.
https://developer.apple.com/forums/thread/4572?answerId=9176022#9176022

静态库:在编译时会将库copy一份到目标程序中,编译完成之后,目标程序不依赖外部的库,也可以运行。
动态库:编译时只存储了指向动态库的引用。可以多个程序指向这个库,在运行时才加载,运行时加载会损耗部分性能。
.o文件:实现文件编译生成的二进制的目标文件
可执行程序:胖二进制文件
mach-o:可执行程序
mach:苹果系统内核

静态库与动态库
  • 动态库形式:.dylib和.framework
  • 静态库形式:.a和.framework

iOS中动态库分为两种类型,一种是开发者自定义动态库(Embedded Framework),一种是系统SDK动态库。Embedded Framework并不是传统意义的动态库,下面分别来说明。

在App打包阶段:系统SDK动态库并不会打包到可执行程序中,因为在iOS设备内已经内置了这些系统动态库,而Embedded Framework会打包到frameworks文件夹中。静态库则会在静态链接阶段合并到可执行文件中。
在App运行阶段:系统SDK动态库会由dyld进行动态链接,存储中共享内存空间,多个app共用一份,并通过虚拟内存空间中的符号表映射到最终的共享内存空间物理地址;而Embedded Framework会像静态库一样加载到程序虚拟内存空间,多个app不共享。

下图展示了ipa打包文件内部结构。
其中_CodeSignature是文件的签名,避免用户修改数据。
Frameworks内包含所有一起打包动态库文件,可以看到swift相关动态库也在里面,因此swift的使用会增加一定的包体积。如果有自定义的动态库也会在该文件夹看到。

每个自定义动态库也会有_CodeSignature文件签名,避免用户修改数据。
而静态库会在静态链接阶段合并到最终的可执行程序中。

下图展示了整个编译过程:

  • 预处理:Clang会预处理你的代码,比如把宏嵌入到对应的位置、注释被删除,条件编译被处理
  • 词法分析:词法分析器读入源文件的字符流,将他们组织称有意义的词素序列。
  • 语法分析:这一步是把词法分析生成的标记流,解析成一个抽象语法树(abstract syntax tree -- AST)。
    AST 是抽象语法树,结构上比代码更精简,遍历起来更快,所以使用 AST 能够更快速地进行静态检查。
  • 静态分析:把源码转化为抽象语法树之后,编译器就可以对这个树进行静态分析处理。静态分析会对代码进行错误检查,如出现方法被调用但是未定义、定义但是未使用的变量等,以此提高代码质量。当然,还可以通过使用 Xcode 自带的静态分析工具(Product -> Analyze)进行手动分析。最后 AST 会生成 IR,IR 是一种更接近机器码的语言,区别在于和平台无关,通过 IR 可以生成多份适合不同平台的机器码。
    静态分析的阶段会进行类型检查,比如给属性设置一个与其自身类型不相符的对象,编译器会给出一个可能使用不正确的警告。在此阶段也会检查时候有未使用过的变量等。
  • 中间代码生成和优化:此阶段LLVM 会对代码进行编译优化,例如针对全局变量优化、循环优化、尾递归优化等,最后输出汇编代码xx.ll文件。
  • 生成汇编代码: 汇编器LLVM会将汇编码转为机器码。此时的代码就是.o二进制的目标文件。每个实现文件(.m,.cpp,.swift)都会生成对应的.o目标文件,每个.o文件也是个Mach-O文件。
  • 静态链接:连接器把编译产生的.o文件和(a,tbd)文件,生成一个可执行文件。

可执行文件是个胖二进制文件,里面是一个或多个Mach-O文件的聚合体。
关于胖二进制文件和Mach-O文件的介绍参考https://www.jianshu.com/p/31c68ab2d1dd,这里只简单说明。

Mach-O文件结构如下:
其中Header对应mach_header结构体,保存Mach-O的一些基本信息,包括平台、文件类型、指令数、指令总大小,dyld标记Flags等。
Load Commands:紧跟Header,加载Mach-O文件时会使用这部分数据确定内存分布,对系统内核加载器和动态连接器起指导作用。
Data: 每个segment的具体数据保存在这里,包含具体的代码、数据等。

Segment分为__TEXT(代码区),__DATA_CONST(常量区),DATA(全局数据区)三种。每个Segment又包含多种Section。
__TEXT存放的是函数的实现代码,内部是函数的汇编实现对应的二进制机器码。
苹果对蜂窝网络下载ipa包的默认限制是200MB,对__TEXT段大小也有限制,其大小不得超过500MB。因为自定义的动态库的实现代码在编译阶段不会合并到可执行文件的__TEXT段,因此可以使用动态库代替静态库以减少__TEXT段的大小。

静态库和自定义动态库的分发包差异

静态库会将所有.o文件简单的合并到一起,而每个.o文件都是Mach-O文件,包含header,load commands描述信息,这样最终的静态库文件就会产生很多的冗余信息。
而动态库会将.o文件中所有的描述信息抽离出来,生成一个总的描述文件,因此一般情况动态库会比静态库体积更小。

下图展示了AFN生成的静态库和自定义动态库分发包结构区别:

静态库的分发包
动态库的分发包
静态库的分发包大小
自定义动态库的分发包大小

下面MachOView来查看静态库和自定义动态库的内部结构:
可以看到和上面展示一样,静态库中.o目标文件并没有合并,每个.o文件都有Mach Header和Load Commands,造成数据冗余。

静态库的内部结构
自定义动态库的内部结构
静态库和动态库在静态链接生成可执行程序阶段差异

虽然说静态库的分发包有数据冗余,但这通常影响不大,因为我们重点是希望控制最终的可执行程序大小。
在静态链接生成可执行程序阶段,静态库会剔除冗余信息,和可执行程序合并,__TEXT,__DATA段融合。而动态库不会进行融合,只是在可执行程序的符号表中记录动态库的符号信息,当App运行进行动态链接时,会将这些符号关联到最终的动态库代码地址。
静态库在链接到可执行程序阶段,可以通过配置(-noall_load)只链接所需的功能;而动态库相对可执行程序独立的存在,生成的动态库必须包括所有代码。

静态库链接配置:-noall_load(默认值),-all_load,-ObjC,-force_load。

  • -noall_load:只加载使用到的文件(部分runtime特性可能用不了,比如分类,动态创建类)。
  • -ObjC:cocoapods默认选项,将所有OC文件都加载到可执行文件。
  • -all_load:将所有文件都加载到可执行文件。
  • -force_load:指定加载的文件。

通过-force_load或-noall_load可减少生成的可执行文件体积。
可通过开启配置输出linkMap文件,查看具体链接的相关文件。

Bitcode

iOS正常语法编译流程如下图,其中BitCode是由LLVM IR(ll格式)生成,其格式是.bc类型。
IR指生成的中间表达形式,即Intermediate Representation。


打开Bitcode后,当我们提交项目到AppStore时,不再是提交ipa文件,而是Bitcode中间代码,其编译链接过程在AppStore内部进行,Apple会生成相应芯片的机器码,对其生成的体积进行优化。但Bitcode需要项目中所有代码文件都使用bitcode才可进行,包括第三方库等。

Swift支持

如果要在Swift项目中使用外部的代码,可选的方式只有两种,一种是把代码拷贝到工程中,另一种是用动态 Framework。使用静态库是不支持的。

造成这个问题的原因主要是 Swift 的运行库没有被包含在 iOS 系统中,而是会打包进 App 中(这也是造成 Swift App 体积大的原因)。

CocoaPods 的做法

在纯 ObjC 的项目中,CocoaPods 使用编译静态库 .a 方法将代码集成到项目中。在 Pods 项目中的每个 target 都对应这一个 Pod 的静态库。

对于 Swift 项目,CocoaPods 提供了动态 Framework 的支持。通过 use_frameworks! 选项控制。对于 Swift 写的库来说,想通过 CocoaPods 引入工程,必须加入 use_frameworks! 选项。

在使用CocoaPods的时候在Podfile里加入use_frameworks! ,那么你在编译的时候就会默认帮你生成动态库,我们能看到每个源码Pod都会在Pods工程下面生成一个对应的动态库Framework的target,我们能在这个target的Build Settings -> Mach-O Type看到默认设置是Dynamic Library。也就是会生成一个动态Framework,我们能在Products下面看到每一个Pod对应生成的动态库。

这些生成的动态库将链接到主项目给主工程使用,但是我们上面说过动态库需要在主工程target的General -> Embedded Binaries中添加才能使用,而我们并没有在Embedded Binaries中看到这些动态库。那这是怎么回事呢,其实是cocoapods已经执行了脚本把这些动态库嵌入到了.app的Framework目录下,相当于在Embedded Binaries加入了这些动态库。我们能在主工程target的Build Phase -> Embed Pods Frameworks里看到执行的脚本。

所以Pod默认是生成动态库,然后嵌入到.app下面的Framework文件夹里。我们去Pods工程的target里把Build Settings -> Mach-O Type设置为Static Library。那么生成的就是静态库,但是cocoapods也会把它嵌入到.app的Framework目录下,而因为它是静态库,所以会报错:unrecognized selector sent to instanceunrecognized selector sent to instance 。

动态链接流程:https://www.jianshu.com/p/31c68ab2d1dd

iOS动态库VS静态库:https://mp.weixin.qq.com/s/UGY7IijHnvNEwyGWiZLmGQ

你可能感兴趣的:(静态库与动态库)