背景
刚加入新的公司,接触到新公司的代码以后,心中是一篇翻江倒海,不是因为项目代码有多优秀,多牛逼,而是因为这是一个7年的老项目,期间经历过不知多少个程序员的手,项目简直是面目全非,各种重复的第三方库,代码耦合严重,不同时期的代码风格及开发模式完全不一样,造成项目过大,编译花费很多时间。现在的同事们正在想办法优化项目,在使用组件化的发开模式,减少与项目中老代码及第三方重复库的耦合。
因此,一些老的代码和一些已经不怎么更新且非常稳定的第三方库进行二进制处理,加快编译速度,同时在未来的开发中能更好进行整合和淘汰部分重复的代码。但是当错误发生在二进制库中的时候,我们不能有效定位具体代码,那么就需要切回源码,进行分析处理。为此,最近研究了源码与二进制平滑切换的方法,并分享一下心得,如有不足,请指出。
framework与.a的区别
-
.a
:只把代码文件打包编译成二进制。 -
framework
:把代码文件及其他资源,如图片,音频等文件,一起打包成二进制。
在选用何种二进制类型时,可以根据实际的项目情况进行打包。
二进制打包方式
- 1.通过
Xcode
的官方打包方式,编译打包 - 2.使用
Aggregate
打包 - 3.使用脚本直接打包
- 4.使用第三方工具打包,如
cocoapods-packager
我这里选用的是Aggregate
打包,因为Xcode的官方打包方式比较麻烦;使用脚本会因为不同组件,不用项目要去修改脚本,维护不方便;使用cocoapods-packager
,虽然打包方便容易,但是在pod spec lint
的时候出现了本地与远程仓库之间二进制文件路劲校验失败的情况的,具体原因还没找到,待后续补充。因此,最后选择了Aggregate
打包方式,下面也以Aggregate
打包的方式讲解。但是本人希望大家去尝试一下cocoapods-packager
。
源码和二进制切换方案
经过一周的调研和实践,发现网上主要是两种方案
1.在
podspec
中使用if-else
的条件语句去区分源码和二进制。但是在源码和二进制切换时,每次都需要pod cache clean
一下,切换非常麻烦。虽然原作者给除了解决方案,但是需要一个静态服务器去存放二进制文件,切需要多个脚本去维护,开发及维护成本比较大。而且,在源码与二进制切换时,如果pod cache clean --all
所有的二进制都会切换成源码,且pod时需要重新拉代码或者下载二进制,非常耗时。-
2.使用
Carthage
和cocoapods
结合的方式,由pod
管理源码,Carthage
管理二进制,由于我们项目一直是使用pod管理,且Carthage又要付出一定的学习成本,对于我们这种人数并不多的团队很不划算。参考:http://www.cocoachina.com/ios/20170512/19229.html?from=singlemessage&isappinstalled=0
subspec实现源码和二进制切换
在尝试了以上两种方案,发现他们的不足及不适应当前团队的情况下,和同时经过讨论,制定了使用cocoapods
的subspec
去实现源码和二进制切换。
subspec
主要是在cocoapods
中给私有库或第三方做目录分层使用。在pod的时候。在podfile中写入指定的subspec,可以只导入指定目录下的文件。根据这个功能,我们将源码和二进制一起做成私有库,分别放在两个subspec下。下面,我将会以BlocksKit的私有化为例子,讲解详细过程。
1.添加Framework类型的target
1、我这里使用的是用Xcode直接创建私有库,本人建议使用pod lib create XXX
的方式去创建,两者项目只是创建方式不同,实际操作上是一样的。
当创建完项目后,把我们需要私有化或者组件化的代码拖到项目中,并在target中创建二进制的target
2、在组件库或私有库新建Framework类型的target
然后将需要二进制的文件引用到framework
的target。注意:这里不需要copy文件过去
然后设置framework的build setting
和build phases
Xcode 12以上只需要添加x86_64,其他被废弃了,默认会添加arm64,具体看编辑完后缀带的内容
注意:做完上面的工作后,尝试编译这个framework的target时,会发现代码中引入的三方库提示找不到了。这是因为在Pod时没有针对该target 来关联三方库。所以你需要修改你的Podfile 文件, 让新加入的framework的target也导入三方库
use_frameworks!
platform :ios, '9.0'
target 'YourLib_Example' do
pod '你需要的三方库'
end
target 'Framework名称' do
pod '你需要的三方库'
end
修改完成后,再执行一次安装命令:
pod install
2.添加Aggregate
类型的target,并加入打包脚本
具体脚本:
#!/bin/sh
#要build的target名,需要替换成自己项目的名称
TARGET_NAME='CXBlocksKitFramework'
#${PROJECT_NAME}
if [[ $1 ]]
then
TARGET_NAME=$1
fi
UNIVERSAL_OUTPUT_FOLDER="${SRCROOT}/${PROJECT_NAME}_Products/"
#创建输出目录,并删除之前的framework文件
mkdir -p "${UNIVERSAL_OUTPUT_FOLDER}"
rm -rf "${UNIVERSAL_OUTPUT_FOLDER}/${TARGET_NAME}.framework"
#分别编译模拟器和真机的Framework
xcodebuild -target "${TARGET_NAME}" ONLY_ACTIVE_ARCH=NO -configuration ${CONFIGURATION} -sdk iphoneos BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}" clean build
xcodebuild -target "${TARGET_NAME}" ONLY_ACTIVE_ARCH=NO -configuration ${CONFIGURATION} -sdk iphonesimulator BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}" clean build
#拷贝framework到univer目录
cp -R "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${TARGET_NAME}.framework" "${UNIVERSAL_OUTPUT_FOLDER}"
#合并framework,输出最终的framework到build目录
lipo -create -output "${UNIVERSAL_OUTPUT_FOLDER}/${TARGET_NAME}.framework/${TARGET_NAME}" "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${TARGET_NAME}.framework/${TARGET_NAME}" "${BUILD_DIR}/${CONFIGURATION}-iphoneos/${TARGET_NAME}.framework/${TARGET_NAME}"
#删除编译之后生成的无关的配置文件
dir_path="${UNIVERSAL_OUTPUT_FOLDER}/${TARGET_NAME}.framework/"
for file in ls $dir_path
do
if [[ ${file} =~ ".xcconfig" ]]
then
rm -f "${dir_path}/${file}"
fi
done
#判断build文件夹是否存在,存在则删除
if [ -d "${SRCROOT}/build" ]
then
rm -rf "${SRCROOT}/build"
fi
rm -rf "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator" "${BUILD_DIR}/${CONFIGURATION}-iphoneos"
#打开合并后的文件夹
open "${UNIVERSAL_OUTPUT_FOLDER}"
然后就可以打包framework
了。
切换scheme
到-> aggregate
创建的target
,运行common+B
后会自动执行生成脚本,生成新的framework库
注意事项
Xcode 12以上虽然运行不会报错,其实已经打包失败了,查看编译记录可以发现Build/Products/Debug-iphonesimulator/
路径不存在,最终打包的结果文件缺失,所以需要修改编译路径。
将
CONFIGURATION
替换成CONFIGURATIONRELEASE
指定编译release模式
#!/bin/sh
#要build的target名,需要替换成自己项目的名称
TARGET_NAME='YCDatasModule'
CONFIGURATIONRELEASE='Release'
真机模拟器库无法合并,报错:have the same architectures (arm64) and can't be in the same fat output file
XCode12之前:
编译模拟器静态库支持i386 x86_64两架构
编译真机静态库支持armv7 arm64两架构
使用lipo -create -output命令可以将两个库合并成一个支持模拟器和真机i386 x86_64 armv7 arm64四种架构的胖子库。
XCode12编译的模拟器静态库也支持了arm64,导致出现真机库和模拟器库不能合并的问题。
按如下配置:
3.编写podspec
在编写subpsec
时,我们团队规定了source
是源码,framework
是二进制,用于pod时进行区分,这里我们默认使用二进制的subspec。这里的source和framework的命名可以根据项目具体情况做出调整。
Pod::Spec.new do |s|
s.name = 'CXBlocksKit'
s.version = '0.1.1'
s.summary = 'A short description of TPBlocksKit.'
s.description = <<-DESC
TODO: Add long description of the pod here.
DESC
s.homepage = 'https://gitee.com/NickQCX/CXBlocksKit'
# s.screenshots = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { 'Nick' => 'nick.qiu@cootek.cn' }
s.source = { :git => 'https://gitee.com/NickQCX/CXBlocksKit.git', :tag => s.version.to_s }
# s.social_media_url = 'https://twitter.com/<TWITTER_USERNAME>'
s.ios.deployment_target = '8.0'
s.pod_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64' }
s.user_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64' }
#s.source_files = 'TPBlocksKit/Classes/**/*’
s.default_subspec = ‘framework'
s.subspec 'source' do |ss|
ss.source_files = 'CXBlocksKit/CXBlocksKit/BlocksKit/**/*'
end
s.subspec 'framework' do |ss|
ss.ios.vendored_frameworks = 'Example/CXBlocksKit_Products/*.framework'
end
end
使用
默认framework
pod ‘CXBlocksKit'
切换成源码
pod 'CXBlocksKit/source'
或者
pod 'CXBlocksKit', :subspec => ['source']
修改步骤
当lib库被修改,重新打包需要处理的步骤
- 1.修改
.podSpec
文件,将tag
版本号改成新的版本 - 2.将在
lib
工程中新添加的文件,在framework
库中添加好引用 (lib工程与framework工程文件同步) - 3.切换
scheme
到->aggregate
创建的target,运行并执行生成脚本,生成新的framework
库(新生成的库包含了这次的修改及新增的文件) - 4.连同
.podSpec
文件及修改或添加的文件一并提交到远程私有库 - 5.将修改后的
.podspec
文件push
到远程索引库pod repo push …
- 6.使用时,需要本地更新一下索引库
pod repo update
来获取到新的版本
总结
通过subspec的方式实现源码和二进制的切换,降低了学习成本和维护成本,且切换平滑。虽然需要修改podfile,但是与团队约定好以后,使用起来还是很方便的,并且一目了然,通过podfile可以清晰的知道哪个是源码,哪个是二进制。
原文链接:iOS 组件化的二进制化