在開始之前昵慌,還是明確一下我們的目標(biāo),希望通過對 Cocoapods-binary 的改造使其支持 server 端緩存腕唧,從而達到 一處編譯如暖,處處使用 的 pods lib dependencies。同時會簡單對比一下現(xiàn)有已經(jīng)公開的大廠的實踐和利弊橄维,以及我們?yōu)楹芜@么做尺铣。
業(yè)內(nèi)實踐
對于人數(shù)較多的業(yè)務(wù)團隊,為了更好的團隊協(xié)作組件化是不可避免的争舞,關(guān)于如何逐步的組件拆分以及提升編譯美團有一篇不錯的入門 美團外賣iOS多端復(fù)用的推動凛忿、支撐與思考 里面提到了項目的二進制化,但是并沒有涉及如何實現(xiàn)的竞川,更多是關(guān)于如何分步進行組件化迭代店溢。那么如何開始,又有哪些巨人的肩膀可以踩呢委乌?
知乎 iOS 基于 CocoaPods 實現(xiàn)的二進制化方案
知乎的實踐是基于項目工程在提交 PR 后觸發(fā) binary package 的 CI 腳本床牧,相對完整描述了如何進行源碼和 binary 的切換和控制,生成的 binary package 如何在 server 端存儲福澡,還附了基本的流程圖叠赦。總結(jié)一下要點:
- 通過 YML 配置 binary 白名單革砸、文件服務(wù)配置信息等除秀;
- 利用libo將xcodebuild后的 dSYM 和 binary 整合(包含了模擬器和真機設(shè)備)后的 ZIP 包上傳至靜態(tài)服務(wù)器,得到對應(yīng)的 URL算利;
- 利用 CocoaPods Analysis 修改 podSpec 將 binary 為true的庫的 source 指向獲取到的 URL册踩,同時更新 Tag。將修改后的 spec 文件推送至私有倉庫效拭;
分析
- 通過 YML 配置來控制源碼和 binary 切換是不錯的方式暂吉,不過如果能基于cocoapod-plugin 插件給 pod DSL 添加 binary 的屬性來控制就更好了。
- 對于修改 podspec 以及更新 Private Pod Repo 感覺是有一點冗余的缎患。其實可以在 install 過程中檢查 binary 為 true 的 pod 是否已有打好的 ZIP 包慕的,存在則替換,否則進入 prebuild 流程打包即可挤渔。當(dāng)然這里需要約定好生成的 ZIP 包名肮街,知乎是以 tag + zhihu-static,如/path/to/server/AFNetworking-3.20-zhihu-static判导。本質(zhì)上不論使用哪種方式引用 Pod嫉父,背后對應(yīng)的都是 spec 文件里配置的 source 所指向倉庫中對應(yīng)的一個Git 節(jié)點(PS:每個 commit 對應(yīng)的 hash沛硅,所以管理好版本很重要)。CocoaPods 在解決沖突依賴時绕辖,是依據(jù)語義化版本來遞歸摇肌,所以我認為是不需要單獨對應(yīng)的 static spec。
火掌柜 iOS 端基于 CocoaPods 的組件二進制化實踐
同樣采用雙私有源策略仪际,一個靜態(tài)服務(wù)器保存預(yù)先打好包的 binary围小,一個是源碼服務(wù)地址。區(qū)別于知乎的方案的地方是弟头,他們事先將各個私有庫更新時吩抓,觸發(fā) CI 打包并上傳服務(wù)器,在 pod install 過程中進行替換源赴恨。知乎是在完整項目的構(gòu)建中完成對 binary 的打包和替換疹娶,知乎這樣的一攬子方案才是正解。不過該文章提到不少在實踐中的坑伦连,有比較多的參考意義雨饺,他們還產(chǎn)出了一個 Pod 插件 CocoaPods-bin』蟠荆總結(jié)一下該文章要點:
- 改造 CocoaPods-Package 额港,支持對單個 pod 進行二進制編譯,打包上傳靜態(tài)服務(wù)器;
- 基于 Podfile 中添加的全局變量 tdfire_use_source_pods 來控制 binary 白名單歧焦,pod install 時注入環(huán)境變量以控制源碼切換移斩;
分析
- CocoaPods-Package 作為官方提供的插件在 1.7.0 正式版發(fā)布后做了一次更新,也是時隔多年绢馍,支持了Swift 的 package 及修復(fù)了一些問題向瓷。以單個 pod 進行二進制編譯的最大麻煩在于,團隊如果進行了比較重度的組件化舰涌,一般會有大量依賴庫需要維護猖任,如果每個庫都需要配置一份 package 腳本成本比較高,同時第三方庫也需要進行鏡像維護瓷耙,盡管支持了 CI 自動化也需要花費一部分精力朱躺,同時業(yè)務(wù)工程師也需要對項目有完整的認知,否則難以捋清其中的關(guān)系搁痛。
- 以 IS_SOURCE環(huán)境變量控制 binary 和源碼切換的方式也不是很友好长搀。也是可以給 pod DSL 添加擴展來支持 binary switch。當(dāng)前在每次 install 前加入變量去控制鸡典,使用上感覺有些奇怪盈滴;
改造 CocoaPods-Binary
關(guān)于 Cocoapods-Binary 前段時間寫過一篇簡單介紹,淺析 Cocoapods-Binary 實現(xiàn)。在了解了該插件如何工作之后巢钓,就可以將我們端想法付諸實踐了。
首先疗垛,我們要做的事情很多插件都已經(jīng)幫我們完成了症汹,而我們要做的就是簡單的支持一下對 binary framework 的靜態(tài)服務(wù)器存儲和下發(fā)就好,先來一張流程圖:
- 上圖中的 featch remote framework 和 upload zips to server 就是我們要做的事情贷腕。
在 Prebuild framework 之前檢查當(dāng)前 pod_target 是否有對應(yīng)的 server cache背镇,存在則 download 至本地同時 unarchive 至 GenerateFramework 文件目錄下,然后跳過當(dāng)前 pod_target 的編譯泽裳。
exist_remote_framewo = sandbox.fetch_remote_framework_for_target(target)
def fetch_remote_framework_for_target(target)
existed_remote_framework = self.remote_framework_names.include?(zip_framework_name(target))
return false unless existed_remote_framework
begin
zip_framework_path = self.ftp.get(remote_framework_dir + zip_framework_name(target))
rescue
Pod::UI.puts "Retry fetch remote fameworks"
self.reset_ftp
zip_framework_path = self.ftp.get(remote_framework_dir + zip_framework_name(target))
end
return false unless File.exist?(zip_framework_path)
target_framework_path = generate_framework_path + target.name
return true unless Dir.empty?(target_framework_path)
extract_framework_path = generate_framework_path + target.name
zf = Zipper.new(zip_framework_path, extract_framework_path)
zf.extract()
true
end
在 Prebuild 結(jié)束后會進行文件清理和 binary 的替換鏈接瞒斩,在此時進行批量 binary 文件的同步。將GenerateFramework 目錄中所匹配的 pod_target 且資源服務(wù)器所不存在的 binary 文件進行上傳涮总,統(tǒng)一至 static_frameworks 目錄下胸囱,文件名則是 pod_name + tag, 例如 pod 'AFNetworking', '3.0'對應(yīng)的 zip framework 名字為 AFNeworkings-3.0.0.zip 。
sync_prebuild_framework_to_server(target)
def sync_prebuild_framework_to_server(target)
zip_framework = zip_framework_name(target)
target_framework_path = framework_folder_path_for_target_name(target.name)
zip_framework_path = framework_folder_path_for_target_name(zip_framework)
# ftp server 已有相同 Tag 的包
return if self.remote_framework_names.include? zip_framework
# 本地 archive 失敗
return if !File.exist?(target_framework_path) || Dir.empty?(target_framework_path)
begin
Zipper.new(target_framework_path, zip_framework_path).write unless File.exist?(zip_framework_path)
self.ftp.put(zip_framework_path, remote_framework_dir)
remote_zip_framework_path = self.ftp.local_file(remote_framework_dir + zip_framework)
FileUtils.mv zip_framework_path, remote_zip_framework_path, :force => true
rescue
Pod::UI.puts "ReTry To Sync Once"
self.reset_ftp
sync_prebuild_framework_to_server(target)
end
end
實踐過程中瀑梗,為了方便直接是利用了公司現(xiàn)有的 ftp 文件服務(wù)器烹笔,單獨開了一個進行目錄維護。相比 CocoaPods-binary 僅增加了 ftp_tools.rb 和 zip_tools.rb 兩個文件抛丽,實現(xiàn)比較簡單這里就不貼出來了谤职。
限制
- 最終的 binary size 會比使用源碼的時候大一點,不建議最終上傳 Store 的時候使用亿鲜;
- 缺少一個驗證的機制允蜈,如果已發(fā)布的二進制包不能被項目正常引用,那么會導(dǎo)致所有人的編譯失斴锪饶套;;
- 由于工程采用的是全部靜態(tài)庫依賴的形式其馏,所以在二進制和源碼切換的過程中會對 project 文件產(chǎn)生更改凤跑;
- CocoaPods 在 1.7 以上版本,更改了framework 邏輯叛复,不會把 resource copy 至 framework仔引,因此我們需要將 CocoaPods 版本固定到 1.6.x;
- 對于動態(tài)配置生成的 framework褐奥,例如RN 相關(guān)的依賴等咖耘,不支持binary;
- 不同版本 Swift 編譯出的 binary 是不能兼容撬码。如果項目中引用了 Swift 庫Xcode 版本需要統(tǒng)一儿倒。
在使用 binary 的過程中,還有一些意想不到的問題。例如夫否,為了減少源碼和 binary 切換過程中產(chǎn)生的大量 git change彻犁,將 Pods 目錄進行了 ignore,導(dǎo)致工程師在過渡階段切換分支中凰慈,多數(shù)被限制在 pod install 中一些三方庫的 download 上面汞幢,非翻所不能也。還有微谓,在 install 后發(fā)現(xiàn) pod 對應(yīng)的 symbol link 沒有正確生成森篷、對應(yīng)的 source 沒有 copy 成功、業(yè)務(wù) framework 打包耗時超常等一系列問題豺型。
總結(jié)
真實項目實踐中仲智,沒有一勞永逸的辦法。不同的業(yè)務(wù)依賴和環(huán)境配置姻氨,包括工程代碼的規(guī)范钓辆,甚至簡單的頭文件管理都會導(dǎo)致開發(fā)過程產(chǎn)生各種各樣的問題『甙螅總之岩馍,是一個不斷探索和進化的過程。