场景

随着公司项目逐步变多、变复杂,组件化是必然的优化结果。当遇到需要封装部分功能或模块组件向外提供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并在新的空项目中进行引用对比,结果如下:

ios引入第三方库 ios 第三方库封装经验_ios引入第三方库

由此可见,若要打出来的包直接包含通过pod引入的代码,只能设置Podfile .a引入打包动态库,这么一来私有组件HudTool、公用第三方MBProgressHUD也就都打包在该动态库里了,那么就回到文初的问题,在被其他项目引入该SDK的时候,就很容易会因为其他项目本身有引入MBProgressHUD,而导致ipa里有MBProgressHUD的两份引用,而实际场景中常用的其他基础库像Masonry、AFNetwork、YY系列等等若有引用的话,那就会有大量的类重复,一来两份引用可能来源于不同版本,存在兼容性的风险,即便同样的版本引用也是增加了最终包的体积;二来控制台输出大量的类重复的警告,这谁受得了?。

3. framework中第三方库的移除

生成framework编译器打包依赖的其他代码主要来源于打包Target里的Other Linker Flags的配置,进入到TestSDK_Dynamic_RemoveMB中Build Setting中可以看到MBProgressHUD的配置来源于inherited,继承于project。

ios引入第三方库 ios 第三方库封装经验_动态库_02


在project中,可以看到Podfile配置执行后,为project设定了配置来源,

ios引入第三方库 ios 第三方库封装经验_ios引入第三方库_03


找到Pods文件夹下对应的xcconfig,在OTHER_LDFLAGS对应设置中移除-l"MBProgressHUD"并添加上-undefined dynamic_lookup,避免找不到库导致的编译报错。

ios引入第三方库 ios 第三方库封装经验_ios framework 找不到.h_04

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中第三方的代码移除成功。

4. 通过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

回顾总结

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

  1. 不做任何处理直接pod .a方式引入打包动态库(Target TestSDK_Dynamic_All)打包会包含所有代码,可能会造成多份重复引用。
  2. pod .a方式引入打包动态库并通过修改OTHER_LDFLAGS移除第三方(Target TestSDK_Dynamic_RemoveMB)是较为理想的方式,但需要注意的是分离的第三方再被外部导入时应尽量确保与原本需要的版本一致,也就是输出文档时要指定第三方库的引用版本范围确保兼容性没问题。

那么Target TestSDK_Dynamic_RemoveAll、TestSDK_Static打包出来的framework什么情况下能用呢?

  • 对于TestSDK_Static打包的静态库,是否use_frameworks!包大小有些区别,引入使用的话因为不包含任何pod引入组件和第三方,额外引入这些后均可运行成功。
  • 对于TestSDK_Dynamic_RemoveAll,按照TestSDK_Dynamic_RemoveMB的思路可以全加入ignoreThirds,也可以按照之前打包方式的对比结果,通过use_frameworks!便可移除所有pod引入组件和第三方。需要注意的是通过use_frameworks!设定打出来的包被引用时依赖的TestModule、HudTool也需在Podfile中指定use_frameworks!,否则运行时会报错dyld: Library not loaded: @rpath/HudTool.framework/HudTool
    因此若需要移除所有pod引入的代码,TestSDK_Static的方式即Mach-O-Type使用Static Library更为方便。
    基于四种打包结果,新建了一个用于framework验证的项目,对应四个Target,Podfile分别补入对应被移除的组件或第三方。依次运行成功,验证完成。
# Uncomment the next line to define a global platform for your project
platform :ios, '9.0'

target 'TestSDKDemo_Dy_All' do
#  引入MBProgressHUD 则控制台会输出类重复警告
#  pod 'MBProgressHUD', '1.2.0'
end
target 'TestSDKDemo_Dy_RemoveMB' do
  pod 'MBProgressHUD', '1.2.0'
end
target 'TestSDKDemo_Dy_RemoveAll' do
  use_frameworks!
  pod 'TestModule', :path ='../TestModule'
  pod 'HudTool', :path ='../HudTool'
end
target 'TestSDKDemo_Static' do
  pod 'TestModule', :path ='../TestModule'
  pod 'HudTool', :path ='../HudTool'
end
基于部分场景的实践结果,若有不对的地方欢迎指正!

链接

完整Demo地址->SDKTest