iOS-关于组件化代码分离第三方库封装framework的一次实践

前言

去年项目开发过程中曾有个大致需求,需要把模块组件封装出来给别人用,那时候初期要先看走通流程,于是匆匆忙忙直接打了个动态库,跑通了流程,但是被引用的过程中,控制台各种重复类的警告输出,存在不少风险,后来这需求不了了之,也就没有再继续研究优化忙别的需求去了。现在想起来就基于之前的想法继续再探究了下,记录一下。(2021/1更新)

场景

随着公司项目逐步变多、变复杂,组件化是必然的优化结果。当遇到需要封装部分功能或模块组件向外提供SDK的时候,如何简洁高效的打包SDK是我们需要面对的问题。既然已经组件化了,那打包自然依旧基于CocoaPods管理的方式方便随时更新,打包.a与framwork的区别就不多说了,本文以framework的封装为例,基于模拟器测试,真机流程一致。

打包实践(基于debug、objective-c)

1. framework的创建

简单模拟下场景,现有一个私有功能组件HudTool,依赖MBProgressHUD;私有业务组件TestModule(核心业务代码),依赖HudTool;然后创建一个Framework,新建一个类TestManager,提供TestModule的相关入口封装。

  1. 新建工程TestSDK, 选择 iOS -> Framework & Library -> Cocoa Touch Framework, 进行下一步。
  2. 为TestSDK初始化pod,依赖组件TestModule、HudTool,创建了类TestManager作为SDK对外的方法头文件。
  3. 为研究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

  1. TargetTestSDK_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_RemoveAllTarget中,并设定Embed&Sign,组件需要作为动态库被包含到SDK中,且需要签名,因此打包时需要设置team与证书。如此打包完成后,也就达成同样的移除部分第三方库的效果了,与TestSDK_Dynamic_RemoveMBSDK不同的是,在被引入使用时,需要设置为Embed 为Embed Without Signing,否则会因组件库证书无效崩溃。此外podfile依赖其他仓库时也需前后一致使用use_frameworks!

  1. TargetTestSDK_Static场景,与上述动态库SDK流程类似,将.a文件拷入PrivateFrameworks添加到工程中,在被引入使用时,podfile依赖不能使用use_frameworks!

回顾总结

从上面的实践中可以看到:

  1. 不做任何处理直接pod .a方式引入打包动态库(Target TestSDK_Dynamic_All)打包会包含所有代码,可能会造成多份重复引用。
  2. pod .a方式引入打包动态库并通过修改OTHER_LDFLAGS移除第三方(Target TestSDK_Dynamic_RemoveMB)是较为理想的方式,不过不能兼容bitcode。
  3. 通过添加必要组件到SDK内部也是一种不错的方式,不过打包动态库引入需要使用use_frameworks!,静态库不能使用use_frameworks!。不考虑这一点的话,因为动态库中组件库是可以直接在Frameworks下获取到的,相比之下静态库的方式会更理想一点。
  4. 分离的第三方再被外部导入时应尽量确保与原本需要的版本一致,也就是输出文档时要指定第三方库的引用版本范围确保兼容性没问题。

基于四种打包结果,新建了一个用于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合并了模拟器与真机。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350