iOS 组件化-混编下的二进制方案

3,862 阅读5分钟

背景: 项目采用 Target-Action + CocoaPods 进行组件化, 去年由 Objective-C 转向 Swift, 所有新的组件全部使用 Swift 编写, 主干项目由 Objective-C 和 Swift 混编. 在体验到面向值和协议编程的便利的同时, 也深深被 Swift 编译速度所困扰. 在对 Swift 编译速度优化后, 效果还是不够理想, 所以为了提高主干项目打包提测的速度, 决定将组件二进制化.


关于 Cocoapods 私有库的创建就不说了, 官方文档里也都有用法.

由于项目中的业务组件中也会拥有资源文件, 且之前 Swift 不支持静态库, 所以采用动态库 framework 的方式.

更新: Xcode9 beta 4 和 CocoaPods 1.5 已经支持 Swift 静态库.

其实就是使用 CocoaPods 对源文件和 .framework 进行管理, 使用 pod installpod update 命令来拉取和切换资源文件.

这里主要记录一下二进制组件的创建和使用.podspec 配置.

其中涉及到的工具有:

  • xcodebuild
  • Command Line Tool
  • Xcode
  • CocoaPods

一. 自动创建 framework

使用 Xcode 进行编译时, 默认生成的 .framework 文件会存放在 Xcode 文件夹下

framework_in_finder

我们可以将编译后的 .framework 移动到文件夹内进行版本管理, 但是如果每次手动拖动到项目文件夹中, 不免有些累赘. 且模拟器和真机也需要不同 architecture, 所以我们可以编写一个 build.sh 来自动生成及合并 exec 可执行文件, 并将生成的 .framework 保存到项目目录内. 其中主要的命令是:

# 生成 iphoneos 和 iphonesimulator 两种可执行文件
$ xcodebuild -workspace ${WORKSPACE_NAME}".xcworkspace" -configuration "${CONFIG}" -scheme ${SCHEME_NAME} SYMROOT=$(PWD)/build -sdk iphoneos clean build
$ xcodebuild -workspace ${WORKSPACE_NAME}".xcworkspace" -configuration "${CONFIG}" -scheme ${SCHEME_NAME} SYMROOT=$(PWD)/build -sdk iphonesimulator clean build
# 合并为一个可执行文件
$ lipo -create "${DEVICE_DIR}/${FMK_NAME}" "${SIMULATOR_DIR}/${FMK_NAME}" -output "${DES_DIR}/${FMK_NAME}"

build.sh 接受两个参数, 分别是项目名称和构建配置, 将脚本置于 .workspace 同级目录下, 如以下命令将输出 Debug 配置的 BAPurchase.framework:

./build.sh BAPurchase Debug

Objective-C framework 中, 使用 lipo 合成 iphoneos 和 iphonesimulator 可执行文件后, .framework 即可正常工作, 不过在合成 Swift framework 后, 使用 .framework 会出现错误:

'SomeClass' is unavailable: cannot find Swift declaration for this class

这是因为 Swift framework 内包含有 .swiftmodule 文件, 其定义了 framework 所支持的 architecture, 所以对于 Swift framework, 我们除了将 .exec 文件合并外, 还需要将 .framework/Module/.swiftmodule 文件夹内的所有描述文件移动到一起:

cp -R "${SIMULATOR_DIR}/Modules/${FMK_NAME}.swiftmodule/." "${DES_DIR}/Modules/${FMK_NAME}.swiftmodule/"

swift_framework_module

脚本完成后, 接下来使用 Xcode 添加一个 Aggregate Target 用来单独调用脚本, 同时实现工程参数的自动填充.

File->New->Target->cross-platform->Aggregate, 新建一个 Aggregate Target :

create_aggregate

并在 Build Phases->New Run Script Phase 内添加新的 script, 并输入以下命令用于调用 build.sh:

${SRCROOT}/build.sh ${PROJECT_NAME} ${CONFIGURATION}

build_phases

上面的指令等价于下面两条命令, 省却了配置信息的输入 :

$ cd BAPurchaseDirectory
$ ./build.sh BAPurchase Debug #Release, Debug, InHouse, AdHoc

使用 Aggregate Target 的一大好处是不用每次都 cd 到目录去运行脚本, 只需要**切换到 Aggregate Target , 确认 Edit Scheme -> Run->Build Configuration , 执行 build **即可.

二 .podspec 配置

.framework 由 shell 生成后位于工程目录下, 和源文件一起存放, 我们还需要另外配置 .podspec 实现选择性导入文件, 介绍两种方式:

  1. 使用条件语句
if ENV[ 'f' ]
      s.vendored_framework = 'BAPurchase/Frameworks/BAPurchase.framework'
else
      s.source_files = 'BAPurchase/Classes/**/*'
      s.resource_bundles = {
          'Resources' => 'BAPurchase/Assets/*/**'
      }
end

导入时使用以下指令切换到 .framework:

$ pod install
$ f=1 pod update BAPurchase

但这种方式有两个问题:

  • pod update BAPurchase 的时候会将所有其他的 pod 也更新, 即如果有多个私有库都配置了 framework 和源文件导入, 则我们对其中一个使用 pod update 会导致两个都切换到 framework 或源文件.
  • 不能在 pod install 时添加参数, 只能 pod install 源文件, 之后再次执行 f=1 pod update BAPurchase 指令切换到动态库.
  1. 使用 subspec
s.subspec 'Framework' do |sf|
      sf.s.vendored_framework = 'BAPurchase/Frameworks/BAPurchase.framework'
  end
s.subspec 'Core' do |sc|
      sc.source_files = 'BAPurchase/Classes/**/*'
      sc.resource_bundles = {
          'Resources' => 'BAPurchase/Assets/*/**'
      }
end
s.default_subspecs = 'Framework'

以上配置定义了两个导入方式: 源码和 .framework, 默认导入方式为 .framework. 如果需要进行源码调试, 则可以修改 podfile 为 pod BAPurchase/Core, 然后执行以下命令:

$ vim podfile
# pod 'BAPurchase/Core'
$ pod update BAPurchase --no-repo-update

subspec 的好处显而易见:

  • 可以在 podfile 内直接指定导入方式为源码或 .framework.
  • pod update 时做到互不干扰, 只更新指定的私有库.

与条件判断相比, 也有缺点:

  • 需要额外去修改 podfile.

三. Debug or Release

业务组件内很有可能会包含有环境变量, 最典型的是 Objective-C 中使用预处理宏 #if DEBUG 来获取测试域名或生产域名, 但是二进制组件是已经编译完成的, 有可能不能匹配主项目的环境配置. 可以用两种方式解决:

  1. subspec

    使用 CocoaPods 的 subspec 可以完成该配置, 此时组件提供三个subspec:

    • 源码
    • Debug framework
    • Release framework

    平时默认导入 Debug framework, 在需要调试时切换到源码, 在线上测试和提审时使用 Release framework. 这样的优缺点很明显:

    • 容易实现
    • 但是 pod 需要更频繁的切换, 在迭代末期, 测试环境和线上环境同时测试时, 操作麻烦且易出错
    • 在组件提交时, 需要分别对 Debug 和 Release Framework 进行编译和上传, 增加工作量和时间消耗.
  2. 组件环境变量

    Debug 和 Release 除了编译器优化的区别外, 影响最大的就是使用 Preprocessor Macros(Objective-C) 和 Active Compilation Conditions(Swift). 我们可以把环境变量的确定延迟到链接时: 在组件内定义一个环境变量, 所有的域名根据环境变量对应返回, 这个变量由主项目传递, 详情参考示例.

    • 需要在代码中传递环境变量
    • 只需要编译一个 Release framework 即可.
    • 主项目中无需频繁切换 framework

我更倾向于使用组件环境变量, 避免在组件中使用 Preprocessor Macros(Objective-C) 和 Active Compilation Conditions(Swift).

另外, 对于项目中大量依赖的三方库, 也可以将他们制作成私有 framework, 减少编译时间, 在需要源码调试时, 切换成源代码即可. 也可以通过 CocoaPods 插件的方式, 在 pod installpod update 后, 将三方代码直接编译成 framework 进行集成.

Troubleshooting Tips

  1. Error: Unknown class _SomeModuleSomeCell in Interface Builder file:

    这是由于组件中的 Xib 有对应的 class, xib 加载后会去将 outlet 赋值到对应类实例, 而类和 xib 不在同一 bundle 内造成错误. 所以需要在 xib 的 Identity Inspector->Custom Class->Module 指定类所属模块.

class_module

  1. Error: 'ASwiftFrameworkClass' is unavailable: cannot find Swift declaration for this class

    Swift framework 进行多 architecture 合并时, 除了 exec 可执行文件外, 还需要将 .framework/Modules 文件夹内的描述文件一并合并, 否则编译时会提示错误.

  2. Error: Module 'BAPurchase' not found

    在 Objective-C 源项目中导入 Swift framework 后, 会出现此错误, 需要在 Objective-C Target -> Build Settings 中, 设置 alwaysEmbedSwiftStandardLibraries = YES