前言
去年项目开发过程中曾有个大致需求,需要把模块组件封装出来给别人用,那时候初期要先看走通流程,于是匆匆忙忙直接打了个动态库,跑通了流程,但是被引用的过程中,控制台各种重复类的警告输出,存在不少风险,后来这需求不了了之,也就没有再继续研究优化忙别的需求去了。现在想起来就基于之前的想法继续再探究了下,记录一下。(2021/1更新)
场景
随着公司项目逐步变多、变复杂,组件化是必然的优化结果。当遇到需要封装部分功能或模块组件向外提供SDK的时候,如何简洁高效的打包SDK是我们需要面对的问题。既然已经组件化了,那打包自然依旧基于CocoaPods管理的方式方便随时更新,打包.a与framwork的区别就不多说了,本文以framework的封装为例,基于模拟器测试,真机流程一致。
打包实践(基于debug、objective-c)
1. framework的创建
简单模拟下场景,现有一个私有功能组件HudTool,依赖MBProgressHUD;私有业务组件TestModule(核心业务代码),依赖HudTool;然后创建一个Framework,新建一个类TestManager,提供TestModule的相关入口封装。
- 新建工程TestSDK, 选择 iOS -> Framework & Library -> Cocoa Touch Framework, 进行下一步。
- 为TestSDK初始化pod,依赖组件TestModule、HudTool,创建了类TestManager作为SDK对外的方法头文件。
- 为研究Framework的打包形式和分离方式,创建了四个Target,TestSDK_Dynamic_All、TestSDK_Dynamic_RemoveMB、TestSDK_Dynamic_RemoveAll、TestSDK_Static,分别用于打包动态库包含所有引用组件及第三方代码、打包动态库移除MBProgressHUD,打包动态库移除所有引用组件及第三方代码、打包静态库。配置好对应的info文件。
2. framework的配置及打包
新建的framework默认Mach-O-Type为Dynamic Library,将TestSDK_Static的Mach-O-Type配置修改为Static Library。TestSDK_Dynamic_All、TestSDK_Dynamic_RemoveMB、TestSDK_Dynamic_RemoveAll这三个Target配置项目前没有区别,后续会用到。现在Mach-O-Type有两种情况,pod引入也有是否use_frameworks!两种情况,那么对于TestSDK_Dynamic_All、TestSDK_Static分别build并在新的空项目中进行引用对比,结果如下:
由此可见,若要打出来的包直接包含通过pod引入的代码,只能设置Podfile .a引入打包动态库,这么一来私有组件HudTool、公用第三方MBProgressHUD也就都打包在该动态库里了,那么就回到文初的问题,在被其他项目引入该SDK的时候,就很容易会因为其他项目本身有引入MBProgressHUD,而导致ipa里有MBProgressHUD的两份引用,而实际场景中常用的其他基础库像Masonry、AFNetwork、YY系列等等若有引用的话,那就会有大量的类重复,一来两份引用可能来源于不同版本,存在兼容性的风险,即便同样的版本引用也是增加了最终包的体积;二来控制台输出大量的类重复的警告,这肯定不能忍。
3. framework中第三方库的移除
3.1 移除第三方的链接依赖
生成framework编译器打包依赖的其他代码主要来源于打包Target里的Other Linker Flags的配置,进入到TestSDK_Dynamic_RemoveMB中Build Setting中可以看到MBProgressHUD的配置来源于inherited,继承于project。
在project中,可以看到Podfile配置执行后,为project设定了配置来源,
找到Pods文件夹下对应的xcconfig,在OTHER_LDFLAGS对应设置中移除-l"MBProgressHUD"并添加上-undefined dynamic_lookup,避免找不到库导致的编译报错。
OTHER_LDFLAGS = $(inherited) -ObjC -l"HudTool" -l"MBProgressHUD" -l"TestModule" -framework "CoreGraphics" -framework "QuartzCore"
修改为OTHER_LDFLAGS = $(inherited) -ObjC -l"HudTool" -l"TestModule" -framework "CoreGraphics" -framework "QuartzCore" -undefined dynamic_lookup
重新编译,发现可执行文件缩小到49KB了,在空项目中引入运行,报错MBProgressHUD未找到,另外引入MBProgressHUD,运行成功,如此framework中第三方的代码移除成功。
通过CocoaPods post_install hook修改OTHER_LDFLAGS参数
要移除的第三方库较多的情况下手动修改毕竟是件麻烦事,可以修改Podfile注入代码使其在pod配置执行过程中自动修改,经过多次尝试后,可以在Podfile中加入如下代码,要移除的第三方库只需加入数组ignoreThirds,执行pod install
,那么xcconfig中的OTHER_LDFLAGS就自动配置好了,重新编译后与手动移除效果一致。
demo中为了对比效果多Target混合使用use_frameworks!命令,因此不同Target引用的同样的库.a包pod会增加-library区分命名,这是CocoaPods的机制,通常情况下pod统一使用.a的方式引入即可,ignoreThirds中"MBProgressHUD" "MBProgressHUD-library"
视情况选其一。
# 多target 混合use_frameworks时 .a会增加-library区分命名
ignoreThirds = ["MBProgressHUD","MBProgressHUD-library"]
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] ='9.0'
if target.name == "Pods-TestSDK_Dynamic_RemoveMB"
xcconfig_path = config.base_configuration_reference.real_path
# 获取build_settings
build_settings = Hash[*File.read(xcconfig_path).lines.map{|x| x.split(/\s*=\s*/, 2)}.flatten]
# 获取OTHER_LDFLAGS并移除末尾换行
$other_ldflags = build_settings['OTHER_LDFLAGS'].chomp
# 移除忽略库
ignoreThirds.each do |value|
$other_ldflags = $other_ldflags.gsub("-l\"#{value}\"", "")
end
# 避免已忽略库编译错误
$other_ldflags = "#{$other_ldflags} -undefined dynamic_lookup"
# 设置OTHER_LDFLAGS
build_settings['OTHER_LDFLAGS'] = $other_ldflags
# 清空xcconfig文件数据
File.open(xcconfig_path, "w") {|file| file.puts ""}
# 重写入xcconfig文件数据
build_settings.each do |key,value|
File.open(xcconfig_path, "a") {|file| file.puts "#{key} = #{value}"}
end
end
end
end
end
3.2 添加私有组件库到SDK的打包文件
前面方案用到-undefined dynamic_lookup
避免打包编译报错,但是该设置在真机场景下与bitcode
的的开启状态是不能并存的,如果需要支持bitcode
的话,只能换种方案了,既然不能移除,那么可以尝试下反向处理,前面实践可以看到设置Static Library
打包静态库或者podfile
在使用use_frameworks!
的时候打的包是不包含任何pod引用库的,在此基础上,可以在TestSDK_Dynamic_RemoveAll、TestSDK_Static
工程中尝试。
SDK项目新增私有库目录PrivateFrameworks
。
- Target
TestSDK_Dynamic_RemoveAll
场景,在build phases中添加运行脚本。
copyFrameworks=('HudTool' 'TestModule')
for element in ${copyFrameworks[*]}
do
copyPath="$PODS_CONFIGURATION_BUILD_DIR/${element}-framework/${element}.framework"
goalPath="${SRCROOT}/${PROJECT_NAME}/PrivateFrameworks/"
cp -rf $copyPath $goalPath
done
编译后pod生成的组件framework就拷到PrivateFrameworks
文件夹下了,第一次需要手动操作下,把framework添加到TestSDK_Dynamic_RemoveAll
Target中,并设定Embed&Sign,组件需要作为动态库被包含到SDK中,且需要签名,因此打包时需要设置team与证书。如此打包完成后,也就达成同样的移除部分第三方库的效果了,与TestSDK_Dynamic_RemoveMB
SDK不同的是,在被引入使用时,需要设置为Embed 为Embed Without Signing
,否则会因组件库证书无效崩溃。此外podfile依赖其他仓库时也需前后一致使用use_frameworks!
。
- Target
TestSDK_Static
场景,与上述动态库SDK流程类似,将.a文件拷入PrivateFrameworks
添加到工程中,在被引入使用时,podfile依赖不能使用use_frameworks!
。
回顾总结
从上面的实践中可以看到:
- 不做任何处理直接pod .a方式引入打包动态库(Target TestSDK_Dynamic_All)打包会包含所有代码,可能会造成多份重复引用。
- pod .a方式引入打包动态库并通过修改OTHER_LDFLAGS移除第三方(Target TestSDK_Dynamic_RemoveMB)是较为理想的方式,不过不能兼容bitcode。
- 通过添加必要组件到SDK内部也是一种不错的方式,不过打包动态库引入需要使用
use_frameworks!
,静态库不能使用use_frameworks!
。不考虑这一点的话,因为动态库中组件库是可以直接在Frameworks下获取到的,相比之下静态库的方式会更理想一点。 - 分离的第三方再被外部导入时应尽量确保与原本需要的版本一致,也就是输出文档时要指定第三方库的引用版本范围确保兼容性没问题。
基于四种打包结果,新建了一个用于framework验证的项目,对应四个Target,Podfile分别补入被移除的第三方。依次运行成功,验证完成。
platform :ios, '9.0'
target 'TestSDKDemo_Dy_All' do
# 引入MBProgressHUD 则控制台会输出类重复警告
# pod 'MBProgressHUD', '1.2.0'
end
target 'TestSDKDemo_Dy_RemoveMB' do
#use_frameworks! #可用可不用
pod 'MBProgressHUD', '1.2.0'
end
target 'TestSDKDemo_Dy_RemoveAll' do
# 必须使用use_frameworks
use_frameworks!
pod 'MBProgressHUD', '1.2.0'
end
target 'TestSDKDemo_Static' do
#use_frameworks! 打包时需用.a引入私有组件,此处若使用use_frameworks,会找不到方法崩溃
pod 'MBProgressHUD', '1.2.0'
end
基于部分场景的实践结果,若有不对的地方欢迎指正!
链接
完整Demo地址->SDKTest
Demo中SDK合并了模拟器与真机。