背景: 项目采用 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 install
和 pod update
命令来拉取和切换资源文件.编程
这里主要记录一下二进制组件的建立和使用 及 .podspec 配置.swift
其中涉及到的工具备:vim
使用 Xcode 进行编译时, 默认生成的 .framework 文件会存放在 Xcode 文件夹下xcode
咱们能够将编译后的 .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/"
复制代码
脚本完成后, 接下来使用 Xcode 添加一个 Aggregate Target
用来单独调用脚本, 同时实现工程参数的自动填充.
File->New->Target->cross-platform->Aggregate
, 新建一个 Aggregate Target
:
并在 Build Phases->New Run Script Phase
内添加新的 script, 并输入如下命令用于调用 build.sh:
${SRCROOT}/build.sh ${PROJECT_NAME} ${CONFIGURATION}
复制代码
上面的指令等价于下面两条命令, 省却了配置信息的输入 :
$ cd BAPurchaseDirectory
$ ./build.sh BAPurchase Debug #Release, Debug, InHouse, AdHoc
复制代码
使用 Aggregate Target
的一大好处是不用每次都 cd 到目录去运行脚本, 只须要**切换到 Aggregate Target
, 确认 Edit Scheme -> Run->Build Configuration
, 执行 build **便可.
.framework 由 shell 生成后位于工程目录下, 和源文件一块儿存放, 咱们还须要另外配置 .podspec 实现选择性导入文件, 介绍两种方式:
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
指令切换到动态库.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
时作到互不干扰, 只更新指定的私有库.与条件判断相比, 也有缺点:
业务组件内颇有可能会包含有环境变量, 最典型的是 Objective-C 中使用预处理宏 #if DEBUG
来获取测试域名或生产域名, 可是二进制组件是已经编译完成的, 有可能不能匹配主项目的环境配置. 能够用两种方式解决:
使用 CocoaPods 的 subspec 能够完成该配置, 此时组件提供三个subspec:
平时默认导入 Debug framework, 在须要调试时切换到源码, 在线上测试和提审时使用 Release framework. 这样的优缺点很明显:
Debug 和 Release 除了编译器优化的区别外, 影响最大的就是使用 Preprocessor Macros(Objective-C) 和 Active Compilation Conditions(Swift). 咱们能够把环境变量的肯定延迟到连接时: 在组件内定义一个环境变量, 全部的域名根据环境变量对应返回, 这个变量由主项目传递, 详情参考示例.
我更倾向于使用组件环境变量, 避免在组件中使用 Preprocessor Macros(Objective-C) 和 Active Compilation Conditions(Swift).
另外, 对于项目中大量依赖的三方库, 也能够将他们制做成私有 framework, 减小编译时间, 在须要源码调试时, 切换成源代码便可. 也能够经过 CocoaPods 插件的方式, 在 pod install
或 pod update
后, 将三方代码直接编译成 framework 进行集成.
Error: Unknown class _SomeModuleSomeCell in Interface Builder file:
这是因为组件中的 Xib 有对应的 class, xib 加载后会去将 outlet 赋值到对应类实例, 而类和 xib 不在同一 bundle 内形成错误. 因此须要在 xib 的 Identity Inspector->Custom Class->Module
指定类所属模块.
Error: 'ASwiftFrameworkClass' is unavailable: cannot find Swift declaration for this class
对 Swift framework 进行多 architecture 合并时, 除了 exec 可执行文件外, 还须要将 .framework/Modules 文件夹内的描述文件一并合并, 不然编译时会提示错误.
Error: Module 'BAPurchase' not found
在 Objective-C 源项目中导入 Swift framework 后, 会出现此错误, 须要在 Objective-C Target -> Build Settings
中, 设置 alwaysEmbedSwiftStandardLibraries = YES