前言
前面文章讲了静态库和动态库,讲的内容都是为了这篇文章做准备,这边我们就聊一下实际SDK开发中对静态库和动态库的应用,平时开发也会用到文章讲的内容。
XCFramework
XCFramework简介
- 1.是苹果官方推荐的、支持的,可以更方便的表示
一个多个平台和架构的分发二进制库的格式。在19年推出 - 2.需要
Xcode11以上支持。(不是iOS系统版本,是Xcode11才有这个功能) - 3.是为
更好的支持Mac Catalyst(是为了将iPad上的App更好的移植到ARM芯片的macOS的一个功能)和ARM芯片的macOS。 专⻔在2019年提出的framework的另一种先进格式。
和传统的framework相比
- 1.可以用
单个.xcframework文件提供多个平台的分发二进制文件; - 2.与
Fat Header相比,可以按照平台划分,可以包含相同架构的不同平台的文件; - 3.在使用时,
不需要再通过脚本去剥离不需要的架构体系。
动态库合并
写了一个功能名字叫SYTimer,我们将它编译成动态库
模拟器架构
此时需要编译命令:
点击回车,就会发现项目开始编译,最后在
指定文件生成我们需要的文件
编译过程中
一直生成.o文件,上面文件就是我们平时Xcode编译的文件
我们展示下包内容,看下里面的东西
SKIP_INSTALL设置为NO就是为了将这个framework拷贝到这个目录下
真机架构
上面我们已经编译了模拟器架构,下面我们来编译成真机架构
红框就是不同点,一个是
分发平台,一个名字,点击回车
此时我们看到文件夹中已经有两个文件,一个是
模拟器,一个是真机,下面我们来进行合并
合并
在制作SDK过程中,我们需要提供的SDK需要即支持真机,又需要支持模拟器!此时我们需要量两个动态库合并,此时我们常用的就是lipo命令
上面就是lipo的解释:
创建或者操作一个文件。
胖二进制
我们先说下胖二进制,胖二进制就是多个架构打包到一起,但是就会有一个问题就是打包在一起的架构是不能相同的,下面我们查一下生成的真机SYTimer可执行文件的架构
我们发现有两个架构分别为
arm_v7,arm64!
上面我们进行打包的时候,只指定了分发平台,并未指定架构,但是为什么会出现两个呢?
- 这是因为在打包的时候,会执行Xcode项目中Build settings的设置。
继续说胖二进制,胖二进制是将
各个动态库的mach-header放在一起,各个库的文件也是肩并肩放在一起,并不是真真意义上的合并。
合并
下面我们使用lipo命令进行合并
我们回车后发现报错了,报错的原因就是
两个架构都存在arm64
这个主要是因为,我们在进行模拟器打包时,
Build Settings设置的有arm64
如果非要合并,那么我们需要将模拟器的x86架构提取出来,获奖arm64架构删除掉,下面我们来做提取
提取出来后,我们再做合并
此时具有多架构的SYTimer就生成了
问题:
- 1.生成的SYTimer
需要重新处理头文件 - 2.需要
包装成一个新的framework - 3.需要
重新签名以上问题都需要手动操作 还有其他问题,我们在打包的时候会生成sdYM以及map
dsYM用处应该都懂,而
map是如果支持了bitcode,就需要BCSymbolMaps,否则即使上传dsYM也无法解析
说完问题,怎么解决,就引出我们的要讲的内容:苹果在19年推出的XCFramework
使用XCFramework
下面我们就来通过命令编译一个XCFramework
按下回车,我们就发现
生成了我们需要的XCFramework
- 我们可以看到
相同架构,也是能合并的,而且自动生成相应架构的framework,上面我们说的sdYM没有放进来,我们给它放进来 - 注意的是
-debug-symbols链接文件需要是绝对路径,不能使用相对路径 - 我们上面说的
sdYM和map都给链接了进来下面我们来使用这个xcframework - 1.直接将SYTimer.xcframework拖入项目
- 2.引入头文件,创建属性
- 3.选择模拟器进行编译
上面讲了,
xcframework会根据不同的平台选择不同的架构,而不像传统的framework,都包含在内,下面我们验证一下 - 1.根据
模拟器编译成功,会生成一个可执行文件,我们查看下这个可执行文件包内容里的Frameworks,包含的SYTimer.framework的架构这个是
模拟器架构 - 2.换成
arm64架构进行编译,查看下可执行文件这个是
真机架构
总结
通过上面我们知道xcframework比lipo命令好很多
- 1.
不用处理头文件 - 2.
不用再去管重复的架构 - 3.
可以把调试符号暴露出来所以建议大家赶快将xcframework用起来吧
项目实际应用
弱引用
创建一个项目,我们想在这个项目中引入SYTimer.framework
按照我们之前讲的,链接一个framework的
三要素
- 1.
指定头文件 -I - 2.
指定framework文件所在目录 -F - 3.
指定framework名称 -l写完后,写代码运行,运行成功了
下面我们运行项目
项目报错,很熟悉的错误,这是因为我们
并没有告诉framework的具体位置,我们可以将framework拖进项目,但是我不想这么做,我想通过xcconfig来设置@rpath那么这个
设置的@rpath和报错的/SYTimer.framework/SYTimer拼接就能找到路径,我们再运行一次,运行成功
弱引用符号
之前说过弱引用,如果说这个东西在运行的时候没有定义,它有是个弱引用的符号,那么链接器自动将它置为null,也就是它可以允许在运行的时候可以为空,可以在运行的时候找不到
- 上面讲如果
没有路径运行会报错 - 我们可以
把SYTimer.framework置为弱引用,这样就不会报错了 - 我们看下
-weak-framework在链接器中的定义 - 跟上面解释一致,不再过多叙述,直接运行,项目看会不会报错
项目没有报错,但是打印为null,此时我们设置起作用的,这有什么好处,当项目某一个库找不到的时候也不会报错 - 那么此时Mach-o做了什么,我们可以看一下
此时它
cmd不再是:LC_LOAD_DYLIB而是:LC_LOAD_WEAK_DYLIB
链接冲突
我们项目中会遇到这样的情况,引入第三方A,里面有个库叫E,在引入一个第三方B,里面也有个库叫E,此时就会出现冲突,因为引入了两个E,那么此时怎么解决呢?下面我们模拟一下,假设项目中引入两个AFNetworking,我们来链接这两个库
下面我们创建xcconfig文件,在文件里进行操作
- 写完后我们编译,没有问题,下面我们引入头文件进行操作,之后编译也没有问题。
我们
链接了两个相同的静态库,只是名字不同,但是在编译期是没有冲突的,为什么?
因为在链接静态库的时候,默认的是
-noall_load,也就是它会进行代码剥离,当找到AFNetworking的时候后面的AFNetworking2就不会再链接进去了
- 如果换成
-all_load,再编译
报错,提示有
223个符号冲突,那么怎么解决这个问题呢?
- 我们看下
cocoapod的xcconfig文件,发现OTHER_LDFLAGS有个-ObjC,是指所有的OC代码都会被导入,因为AFNetworking和AFNetworking2都为OC代码!那么此时我们怎么做?我们可以使用-load_hidden
对
静态库文件可以进行隐藏,不导出任何东西下面就是对xcconfig文件重新写
- 运行成功
动态库链接动态库
- 1.我们创建一个Fremework叫
LJNetworkManager,然后通过cocoaPods导入动态库AFN - 2.此时我们在动态库
LJNetworkManager的LJAFNetworkingManager.m来引入动态库AFNetworking,在LjNetworkManagerTests,调用LJAFNetworkingManager方法 - 3.此时
编译是没有问题的,但是对LjNetworkManagerTests进行运行时报错的 - 4.原因是并
没有找到AFNetworking,此时我们看LjNetworkManagerTests的包内容
它是
不存在Frameworks文件夹的,更不会存在AFNetworking
- 5.如何解决呢?我们可以
将AFN的绝对路径给到LjNetworkManagerTests - 6.此时再运行就会发现都成功了
但这个问题不能这么解决,所以
绝对路径实不可取的
- 7.我们可以通过
copy的形式将framework拷贝到包里
这个是
cocoapods中的.sh脚本中代码,意思就是将项目拷贝到相应的目录,这也就是为什么cocoapods管理的动态库都没这样的问题,还有一种简单的解决办法
- 8.
让cocoapods对LjNetworkManagerTests同样导入AFN,然后更新 - 9.
更新后再运行,我们就发现也成功了
拓展
如果我LJNetworkManager想引用LjNetworkManagerTests里的代码(反向依赖)是否可行,之前文章说过dylid在链接的过程中会将所有的导出符号放在一起,只要再运行的时候能够找到,就能正常运行
- 1.暴露Header
- 2.导入头文件,在
LJNetworkManager引入LJTestAppObject.h,就需要改一下Pods-LjNetworkManager.debug.xcconfig - 3.在
LJAFNetworkingManager中引入LJTestAppObject,发现识别出来了 - 4.运行后发现报错,找不到这个符号
我们前面说过项目
只要运行起来,它自己就能找到,怎么让项目运行起来,之前提过-undefined,这里直接写此时运行就成功了,但是有个问题,我们
随便写的符号也会被忽略掉,不会报错!
- 5.
指定符号-U
此时也会成功
- 6.验证
我们看到此时
调到了LJTestAppObject方法,所以我们上面操作是对的,实现了反向依赖
动态库链接静态库
还是上面的代码,此时我们将Podfile中引入AFN的use_frameworks!去掉
- 1.更新Podfile后,引入的就是
AFN的静态库 - 2.此时
编译不会报错 - 3.此时我们
LjNetworkManagerTests能否引入静态库AFN,答案是可以的,我们来配置一下 - 4.上面设置完就可以了,因为我们
链接的是静态库,只需要找到头文件就行了,之前文章说过,这里不再解释。运行时成功的
拓展
当我给别人提供动态库时,我不想静态库的符号暴露出来,怎么做呢?
- 1.
链接器给我们提供了相应的参数来隐藏:-hidden-l - 2.此时我们再编译
报错,
未定义的符号!我们通过这种方式来控制静态库符号是否展示
静态库链接静态库
还是上面的项目,此时我们将我们创建的库变成静态库,而AFN同样也是静态库
此时LjNetworkManagerTests使用LJAFNetworkingManager会不会报错?
我们分析一下:LjNetworkManagerTests引入静态库没什么问题,但是组件是个静态库,它引入静态库,需要手动集成才行,我们编译下:
找不到符号,此时就需要我们暴露出去
- 1.在Build settings
- 2.此时在
编译项目,就成功了
组件链接静态库,并没有暴露给LjNetworkManagerTests,所以需要我们手动导入
静态库链接动态库
还是上面的项目,此时我们导入AFN为动态库
- LjNetworkManagerTests
使用静态库,没什么问题,但是要使用动态库就需要将动态库放入LjNetworkManagerTests中才行。编译项目发现项目报错
未找到符号,原因
LjNetworkManagerTests使用LJAFNetworkingManager,而LJAFNetworkingManager使用了动态库AFN,而LjNetworkManagerTests并不知道AFN的位置,故报错
- 告诉LjNetworkManagerTests需要使用的
AFN动态库的位置 - 运行项目,不成功
因为我们的
LjNetworkManagerTests不存在AFN的包,这个问题和我们将的动态库链接动态库相似,这里不再说,大家可以自己试试。上面的LjNetworkManagerTests大家可以看成一个一个新的App,我是为了省事用LjNetworkManagerTests来代替App
拓展
Podfile导入多样库
上面我们讲了动态库和静态库间得相互链接,我们知道我们用cocoapods导入AFN是通过use_frameworks!来确定导入静态库还是动态库,现在有个问题,我想在项目中即导入动态库又导入静态库,该怎么做
比如此时我想让
AFN作为静态库导入,而SDWebImage作为动态库导入
- 在Podfile做如下操作
简单解释下,就是我们在
导入podfile的第三方时,判断如果是AFN就以静态库方式导入,否则就按动态库导入
通过podfile同时导入多个项目
MulitProject.xcworkspace是基于TestOC而来,而LjNetworkManager是带有Podfile的第三方库
- 我们通过LjNetworkManager的
Podfile去同时给TestOC和LjNetworkManager添加依赖库,在LjNetworkManager的Podfile做下面改动: - 更新下Podfile
简单总结
XCFramework
- 1.
头文件 - 2.
调试符号 - 3.
相同架构的处理
项目应用
- 1.
weak_import:动态库运行时->位置 - 2.
静态库冲突:App->all_load\ObjC - 3.App ->
动态库链接动态库->pod\脚本复制->rexport动态库 - 4.App ->
动态库链接静态库->静态库代码不想暴露->hidden-l静态库 - 5.App ->
静态库链接静态库-> 三要素:1.名称 2.头文件位置 3.包所在位置 - 6.App ->
静态库链接动态库-> 编译报错:不知道动态库所在位置,运行报错:动态库@rpath -> pod\脚本复制
写在最后
内容比较多,写了一周左右时间!写了5000多字,写的比较细,都是把自己探索过程记录下来,希望大家能够按着文章去实操一遍,这部分也比较无聊,但是这是高阶必走的路。有什么疑问可以在下面留言,也希望大家多多交流,点赞!
补充
上面写的命令可以通过终端打印:man ld,man nm进行查询