背景
剛加入新的公司,接觸到新公司的代碼以后,心中是一篇翻江倒海,不是因?yàn)轫?xiàng)目代碼有多優(yōu)秀,多牛逼,而是因?yàn)檫@是一個(gè)7年的老項(xiàng)目,期間經(jīng)歷過不知多少個(gè)程序員的手,項(xiàng)目簡(jiǎn)直是面目全非,各種重復(fù)的第三方庫,代碼耦合嚴(yán)重,不同時(shí)期的代碼風(fēng)格及開發(fā)模式完全不一樣,造成項(xiàng)目過大,編譯花費(fèi)很多時(shí)間?,F(xiàn)在的同事們正在想辦法優(yōu)化項(xiàng)目,在使用組件化的發(fā)開模式,減少與項(xiàng)目中老代碼及第三方重復(fù)庫的耦合。
因此,一些老的代碼和一些已經(jīng)不怎么更新且非常穩(wěn)定的第三方庫進(jìn)行二進(jìn)制處理,加快編譯速度,同時(shí)在未來的開發(fā)中能更好進(jìn)行整合和淘汰部分重復(fù)的代碼。但是當(dāng)錯(cuò)誤發(fā)生在二進(jìn)制庫中的時(shí)候,我們不能有效定位具體代碼,那么就需要切回源碼,進(jìn)行分析處理。為此,最近研究了源碼與二進(jìn)制平滑切換的方法,并分享一下心得,如有不足,請(qǐng)指出。
framework與.a的區(qū)別
-
.a:只把代碼文件打包編譯成二進(jìn)制。 -
framework:把代碼文件及其他資源,如圖片,音頻等文件,一起打包成二進(jìn)制。
在選用何種二進(jìn)制類型時(shí),可以根據(jù)實(shí)際的項(xiàng)目情況進(jìn)行打包。
二進(jìn)制打包方式
- 1.通過
Xcode的官方打包方式,編譯打包 - 2.使用
Aggregate打包 - 3.使用腳本直接打包
- 4.使用第三方工具打包,如
cocoapods-packager
我這里選用的是Aggregate打包,因?yàn)閄code的官方打包方式比較麻煩;使用腳本會(huì)因?yàn)椴煌M件,不用項(xiàng)目要去修改腳本,維護(hù)不方便;使用cocoapods-packager,雖然打包方便容易,但是在pod spec lint的時(shí)候出現(xiàn)了本地與遠(yuǎn)程倉庫之間二進(jìn)制文件路勁校驗(yàn)失敗的情況的,具體原因還沒找到,待后續(xù)補(bǔ)充。因此,最后選擇了Aggregate打包方式,下面也以Aggregate打包的方式講解。但是本人希望大家去嘗試一下cocoapods-packager。
源碼和二進(jìn)制切換方案
經(jīng)過一周的調(diào)研和實(shí)踐,發(fā)現(xiàn)網(wǎng)上主要是兩種方案
1.在
podspec中使用if-else的條件語句去區(qū)分源碼和二進(jìn)制。但是在源碼和二進(jìn)制切換時(shí),每次都需要pod cache clean一下,切換非常麻煩。雖然原作者給除了解決方案,但是需要一個(gè)靜態(tài)服務(wù)器去存放二進(jìn)制文件,切需要多個(gè)腳本去維護(hù),開發(fā)及維護(hù)成本比較大。而且,在源碼與二進(jìn)制切換時(shí),如果pod cache clean --all所有的二進(jìn)制都會(huì)切換成源碼,且pod時(shí)需要重新拉代碼或者下載二進(jìn)制,非常耗時(shí)。-
2.使用
Carthage和cocoapods結(jié)合的方式,由pod管理源碼,Carthage管理二進(jìn)制,由于我們項(xiàng)目一直是使用pod管理,且Carthage又要付出一定的學(xué)習(xí)成本,對(duì)于我們這種人數(shù)并不多的團(tuán)隊(duì)很不劃算。參考:http://www.cocoachina.com/ios/20170512/19229.html?from=singlemessage&isappinstalled=0
subspec實(shí)現(xiàn)源碼和二進(jìn)制切換
在嘗試了以上兩種方案,發(fā)現(xiàn)他們的不足及不適應(yīng)當(dāng)前團(tuán)隊(duì)的情況下,和同時(shí)經(jīng)過討論,制定了使用cocoapods的subspec去實(shí)現(xiàn)源碼和二進(jìn)制切換。
subspec主要是在cocoapods中給私有庫或第三方做目錄分層使用。在pod的時(shí)候。在podfile中寫入指定的subspec,可以只導(dǎo)入指定目錄下的文件。根據(jù)這個(gè)功能,我們將源碼和二進(jìn)制一起做成私有庫,分別放在兩個(gè)subspec下。下面,我將會(huì)以BlocksKit的私有化為例子,講解詳細(xì)過程。
1.添加Framework類型的target
1、我這里使用的是用Xcode直接創(chuàng)建私有庫,本人建議使用pod lib create XXX的方式去創(chuàng)建,兩者項(xiàng)目只是創(chuàng)建方式不同,實(shí)際操作上是一樣的。
當(dāng)創(chuàng)建完項(xiàng)目后,把我們需要私有化或者組件化的代碼拖到項(xiàng)目中,并在target中創(chuàng)建二進(jìn)制的target
2、在組件庫或私有庫新建Framework類型的target


然后將需要二進(jìn)制的文件引用到framework的target。注意:這里不需要copy文件過去

然后設(shè)置framework的build setting和build phases
Xcode 12以上只需要添加x86_64,其他被廢棄了,默認(rèn)會(huì)添加arm64,具體看編輯完后綴帶的內(nèi)容





注意:做完上面的工作后,嘗試編譯這個(gè)framework的target時(shí),會(huì)發(fā)現(xiàn)代碼中引入的三方庫提示找不到了。這是因?yàn)樵赑od時(shí)沒有針對(duì)該target 來關(guān)聯(lián)三方庫。所以你需要修改你的Podfile 文件, 讓新加入的framework的target也導(dǎo)入三方庫
use_frameworks!
platform :ios, '9.0'
target 'YourLib_Example' do
pod '你需要的三方庫'
end
target 'Framework名稱' do
pod '你需要的三方庫'
end
修改完成后,再執(zhí)行一次安裝命令:
pod install
2.添加Aggregate 類型的target,并加入打包腳本


具體腳本:
#!/bin/sh
#要build的target名,需要替換成自己項(xiàng)目的名稱
TARGET_NAME='CXBlocksKitFramework'
#${PROJECT_NAME}
if [[ $1 ]]
then
TARGET_NAME=$1
fi
UNIVERSAL_OUTPUT_FOLDER="${SRCROOT}/${PROJECT_NAME}_Products/"
#創(chuàng)建輸出目錄,并刪除之前的framework文件
mkdir -p "${UNIVERSAL_OUTPUT_FOLDER}"
rm -rf "${UNIVERSAL_OUTPUT_FOLDER}/${TARGET_NAME}.framework"
#分別編譯模擬器和真機(jī)的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}"
#刪除編譯之后生成的無關(guān)的配置文件
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創(chuàng)建的target,運(yùn)行common+B后會(huì)自動(dòng)執(zhí)行生成腳本,生成新的framework庫

注意事項(xiàng)
Xcode 12以上雖然運(yùn)行不會(huì)報(bào)錯(cuò),其實(shí)已經(jīng)打包失敗了,查看編譯記錄可以發(fā)現(xiàn)Build/Products/Debug-iphonesimulator/路徑不存在,最終打包的結(jié)果文件缺失,所以需要修改編譯路徑。


將
CONFIGURATION替換成CONFIGURATIONRELEASE指定編譯release模式
#!/bin/sh
#要build的target名,需要替換成自己項(xiàng)目的名稱
TARGET_NAME='YCDatasModule'
CONFIGURATIONRELEASE='Release'
真機(jī)模擬器庫無法合并,報(bào)錯(cuò):have the same architectures (arm64) and can't be in the same fat output file
XCode12之前:
編譯模擬器靜態(tài)庫支持i386 x86_64兩架構(gòu)
編譯真機(jī)靜態(tài)庫支持armv7 arm64兩架構(gòu)
使用lipo -create -output命令可以將兩個(gè)庫合并成一個(gè)支持模擬器和真機(jī)i386 x86_64 armv7 arm64四種架構(gòu)的胖子庫。
XCode12編譯的模擬器靜態(tài)庫也支持了arm64,導(dǎo)致出現(xiàn)真機(jī)庫和模擬器庫不能合并的問題。
按如下配置:

3.編寫podspec
在編寫subpsec時(shí),我們團(tuán)隊(duì)規(guī)定了source是源碼,framework是二進(jìn)制,用于pod時(shí)進(jìn)行區(qū)分,這里我們默認(rèn)使用二進(jìn)制的subspec。這里的source和framework的命名可以根據(jù)項(xiàng)目具體情況做出調(diào)整。
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
使用
默認(rèn)framework
pod ‘CXBlocksKit'
切換成源碼
pod 'CXBlocksKit/source'
或者
pod 'CXBlocksKit', :subspec => ['source']
修改步驟
當(dāng)lib庫被修改,重新打包需要處理的步驟
- 1.修改
.podSpec文件,將tag版本號(hào)改成新的版本 - 2.將在
lib工程中新添加的文件,在framework庫中添加好引用 (lib工程與framework工程文件同步) - 3.切換
scheme到->aggregate創(chuàng)建的target,運(yùn)行并執(zhí)行生成腳本,生成新的framework庫(新生成的庫包含了這次的修改及新增的文件) - 4.連同
.podSpec文件及修改或添加的文件一并提交到遠(yuǎn)程私有庫 - 5.將修改后的
.podspec文件push到遠(yuǎn)程索引庫pod repo push … - 6.使用時(shí),需要本地更新一下索引庫
pod repo update來獲取到新的版本
總結(jié)
通過subspec的方式實(shí)現(xiàn)源碼和二進(jìn)制的切換,降低了學(xué)習(xí)成本和維護(hù)成本,且切換平滑。雖然需要修改podfile,但是與團(tuán)隊(duì)約定好以后,使用起來還是很方便的,并且一目了然,通過podfile可以清晰的知道哪個(gè)是源碼,哪個(gè)是二進(jìn)制。
原文鏈接:iOS 組件化的二進(jìn)制化