要研究这个问题,首先我们需要知道这个问题:什么是库?
在我们的日常开发中经常会用到别人封装好的第三方库,比如AFNetworking,SDWebImage等,而对于一些可以被抽取为单独功能组件的逻辑代码,我们也会将部分代码封装成库,以便使用。那么,库到底是什么呢?
库实际上是共享程序代码的方式,一般分为静态库和动态库。那么两者又有什么区别呢?
静态库与动态库的区别
静态库
静态库的后缀为.a,链接时完整地拷贝至可执行文件中,多个程序均使用某一静态库,那么就有多份冗余拷贝
动态库
动态库的后缀为.dll(windows) .dylib(mac) so(linux).链接时不复制,在程序启动后用 dyld 加载,然后再决议符号.所以理论上动态库只用存在一份,多个程序都可以动态链接到这个动态库上面,达到了节省内存的目的(不是磁盘是内存中只有一份动态库),还有另外一个好处,由于动态库并不绑定到可执行程序上,所以我们想升级这个动态库就很容易
tip
tip1 .framework为什么既是静态库又是动态库
系统的.framework是动态库,我们自己建立的.framework是静态库
tip 2 .a与.framework有什么区别?
.a是一个纯二进制文件,.framework中除了有二进制文件之外还有资源文件。
.a文件不能直接使用,至少要有.h文件配合,.framework文件可以直接使用。
.a + .h + sourceFile = .framework
静态库和动态库的依赖关系
在探讨这个问题之前,我们先来了解一下编译和链接
编译: 将我们的源代码文件编译为目标文件
链接: 将我们的各种目标文件加上一些第三方库,和系统库链接为可执行文件。
由于某个目标文件的符号(可以理解为变量,函数等)可能来自其他目标文件,其实链接这一步最主要的操作就是 决议符号的地址。
若符号来⾃静态库(本质就是.o 的集合包)或 .o,将其纳⼊链接并确定符号地址
若符号来⾃动态库,打个标记,等启动的时候再说---交给 dyld 去加载和链接符号。
静态库和动态库的依赖关系有以下四种:
1.libA.a dependency libB.a
静态库互相依赖,这种情况非常常见,制作静态库的时候只需要有被依赖的静态库头文件在就能编译出来。但是这就意味者你需要告诉使用者你的依赖关系
幸运的是 CocoaPod就是这样做的
2.UIKit.dylib dependency Foundation.dylib
动态库依赖动态库,两个动态库是相互隔离的具有隔离性,但是制作静态库的时候需要被依赖动态库参与链接,但是具体的符号决议交给dyld来做。
3.libA.a dependency Foundation.dylib
静态库依赖动态库,也很常见,静态库制作的时候也需要动态库参与链接,但是符号的决议交给 dyld 来做。
4.MyXX.dylib dependency libA.a
动态库依赖静态库,这种情况就有点特殊了。首先我们设想动态库编译的时候需要静态库参与编译,但是静态库交由 dyld 来做符号决议,这和我们前面说的就矛盾了啊。静态库本质是一堆.o 的打包体,首先并不是二进制可执行文件,再者你无法保证主程序把静态库参与链接共同生成二进制可执行文件。
目前的编译器的解决办法是,首先我无法保证主程序是否包含静态库,再者静态库也无法被dyld加载,那么我直接把你静态库的.o 偷过来,共同组成一个新的二进制。也被称做吸附性
那么我有多份动态库都依赖同样的静态库,每个动态库为了保证自己的正确性会把静态库吸附进来。然后两个库包含了同样的静态库,于是问题就出现了。
总结
可执⾏⽂件(主程序或者动态库)在构建的链接阶段
遇到静态库,吸附进来
遇到动态库,打标记,彼此保持独⽴
如何生成静态/动态库的包
1. 创建项目
如下图使用xcode新建动态/静态包即可:
2. 设置参数
2.1 Build Setting ->Enable Bitcode
针对你所要创建的库对应修改Enable Bitcode的值,具体参加以下的解释:
官方: Bitcode is an intermediate representation of a compiled program. apps you upload to App Store Connect that contain bitcode will be compiled and linked on the App Store. Including bitcode will allow Apple to re-optimize your app binary in the future without the need to submit a new version of your app to the App Store. For iOS apps, bitcode is the default, but optional. For watchOS and tvOS apps, bitcode is required. If you provide bitcode, all apps and frameworks in the app bundle (all targets in the project) need to include bitcode.
翻译: Bitcode是编译程序的中间表现,上传到app store connect的包含bitcode的应用程序将在app store上编译和链接。包含Bitcode可以在不提交新版本App的情况下,允许Apple在将来的时候再次优化你的App 二进制文件。 对于iOS Apps,Bitcode默认为yes,但是可选的(可以改为NO)。对于WatchOS和tvOS,bitcode是强制的。如果你的App支持bitcode,App Bundle(项目中所有的target)中的所有的Apps和frameworks都需要包含Bitcode。
2.2 Build Settings ->Build Active Architecture Only
当Debug和Release属性设置为YES时,是为了debug的时候编译速度更快,它只编译当前的architecture版本,建议打包时进行关闭
2.3 设置外部可访问文件
3. 打包
使用xcodebuild命令打包,并使用lipo命令合并并验证
可以手动打包,也可以使用脚本打包
// 切换到项目目录
cd ~/Desktop/TestFrameworkDemo
// 使用xcodebuild命令编译TestFramework这个target的arm版本
xcodebuild -project TestFrameworkDemo.xcodeproj -scheme TestFramework -configuration Release CONFIGURATION_BUILD_DIR='~/Desktop/build/arm'
// 使用xcodebuild命令编译TestFramework这个target的x86版本
xcodebuild -project TestFrameworkDemo.xcodeproj -scheme TestFramework -configuration Release -sdk iphonesimulator CONFIGURATION_BUILD_DIR='~/Desktop/build/x86'
// 合并两个版本,输出文件TestFramework
lipo -create ~/Desktop/build/arm/TestFramework.framework/TestFramework ~/Desktop/build/x86/TestFramework.framework/TestFramework -output ~/Desktop/build/TestFramework
// 将合并的文件覆盖arm/TestFramework.framework中对应文件
cp ~/Desktop/build/TestFramework ~/Desktop/build/arm/TestFramework.framework
// 合并后的framework包移动到桌面
mv ~/Desktop/build/arm/TestFramework.framework ~/Desktop/
// 删除build文件夹
rm -r ~/Desktop/build
// 验证framework包含的框架集,如果结果是“armv7 i386 x86_64 arm64”说明没有问题
lipo -info ~/Desktop/TestFramework.framework/TestFramework
4. tip
针对项目中集成的第三方库,如果不支持某一框架导致某一框架下编译失败的问题,出现如下图所示错误:
此时怎么做呢?三种办法
4.1 真实可靠地重新打包
源码在手,自行打包
4.2 打空包,当然仅能支持编译,并不支持实际功能
针对利用别人的第三方库,很大可能下拿不到源码,此时怎么做呢?只能对功能妥协,仅支持编译。
利用公开的.h文件按照前述3个步骤自行打包,然后利用lipo 命令与原有包进行合并。
此时还有一个问题,如果实际有调用入口,那么上述方法运行时依然会报找不到对应架构下的符号表错误,这是需要手动实现对应类的对应方法,当然实际上我们并不知道对应方法的具体实现,但是别忘了我们这时是处于妥协状态,对应方法直接写成空方法即可。
4.3 如果上述两种方法都无效,使用宏屏蔽第三方库调用入口
当然如果上述两种方法都无效,那么依然是要妥协的,可以使用宏屏蔽第三方库调用入口,即例如:
// 例如第三方库原有包仅支持arm,那么就可以对模拟器运行情况进行屏蔽
#if TARGET_IPHONE_SIMULATOR
#else
// 调用第三方库的代码入口
[IDLFaceLivenessManager.sharedInstance reset];
#endif
参考:组件化-动态库实战