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

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


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

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

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

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

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

其中涉及到的工具备:vim

  • xcodebuild
  • Command Line Tool
  • Xcode
  • CocoaPods

一. 自动建立 framework

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

framework_in_finder

咱们能够将编译后的 .framework 移动到文件夹内进行版本管理, 可是若是每次手动拖动到项目文件夹中, 难免有些累赘. 且模拟器和真机也须要不一样 architecture, 因此咱们能够编写一个 build.sh 来自动生成及合并 exec 可执行文件, 并将生成的 .framework 保存到项目目录内. 其中主要的命令是:ruby

# 生成 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:bash

./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

相关文章
相关标签/搜索