在開(kāi)始之前,還是明確一下我們的目標(biāo),希望通過(guò)對(duì) Cocoapods-binary 的改造使其支持 server 端緩存,從而達(dá)到 一處編譯,處處使用 的 pods lib dependencies。同時(shí)會(huì)簡(jiǎn)單對(duì)比一下現(xiàn)有已經(jīng)公開(kāi)的大廠的實(shí)踐和利弊,以及我們?yōu)楹芜@么做。
業(yè)內(nèi)實(shí)踐
對(duì)于人數(shù)較多的業(yè)務(wù)團(tuán)隊(duì),為了更好的團(tuán)隊(duì)協(xié)作組件化是不可避免的,關(guān)于如何逐步的組件拆分以及提升編譯美團(tuán)有一篇不錯(cuò)的入門 美團(tuán)外賣iOS多端復(fù)用的推動(dòng)、支撐與思考 里面提到了項(xiàng)目的二進(jìn)制化,但是并沒(méi)有涉及如何實(shí)現(xiàn)的,更多是關(guān)于如何分步進(jìn)行組件化迭代。那么如何開(kāi)始,又有哪些巨人的肩膀可以踩呢?
知乎 iOS 基于 CocoaPods 實(shí)現(xiàn)的二進(jìn)制化方案
知乎的實(shí)踐是基于項(xiàng)目工程在提交 PR 后觸發(fā) binary package 的 CI 腳本,相對(duì)完整描述了如何進(jìn)行源碼和 binary 的切換和控制,生成的 binary package 如何在 server 端存儲(chǔ),還附了基本的流程圖。總結(jié)一下要點(diǎn):
- 通過(guò) YML 配置 binary 白名單、文件服務(wù)配置信息等;
- 利用libo將xcodebuild后的 dSYM 和 binary 整合(包含了模擬器和真機(jī)設(shè)備)后的 ZIP 包上傳至靜態(tài)服務(wù)器,得到對(duì)應(yīng)的 URL;
- 利用 CocoaPods Analysis 修改 podSpec 將 binary 為true的庫(kù)的 source 指向獲取到的 URL,同時(shí)更新 Tag。將修改后的 spec 文件推送至私有倉(cāng)庫(kù);
分析
- 通過(guò) YML 配置來(lái)控制源碼和 binary 切換是不錯(cuò)的方式,不過(guò)如果能基于cocoapod-plugin 插件給 pod DSL 添加 binary 的屬性來(lái)控制就更好了。
- 對(duì)于修改 podspec 以及更新 Private Pod Repo 感覺(jué)是有一點(diǎn)冗余的。其實(shí)可以在 install 過(guò)程中檢查 binary 為 true 的 pod 是否已有打好的 ZIP 包,存在則替換,否則進(jìn)入 prebuild 流程打包即可。當(dāng)然這里需要約定好生成的 ZIP 包名,知乎是以 tag + zhihu-static,如/path/to/server/AFNetworking-3.20-zhihu-static。本質(zhì)上不論使用哪種方式引用 Pod,背后對(duì)應(yīng)的都是 spec 文件里配置的 source 所指向倉(cāng)庫(kù)中對(duì)應(yīng)的一個(gè)Git 節(jié)點(diǎn)(PS:每個(gè) commit 對(duì)應(yīng)的 hash,所以管理好版本很重要)。CocoaPods 在解決沖突依賴時(shí),是依據(jù)語(yǔ)義化版本來(lái)遞歸,所以我認(rèn)為是不需要單獨(dú)對(duì)應(yīng)的 static spec。
火掌柜 iOS 端基于 CocoaPods 的組件二進(jìn)制化實(shí)踐
同樣采用雙私有源策略,一個(gè)靜態(tài)服務(wù)器保存預(yù)先打好包的 binary,一個(gè)是源碼服務(wù)地址。區(qū)別于知乎的方案的地方是,他們事先將各個(gè)私有庫(kù)更新時(shí),觸發(fā) CI 打包并上傳服務(wù)器,在 pod install 過(guò)程中進(jìn)行替換源。知乎是在完整項(xiàng)目的構(gòu)建中完成對(duì) binary 的打包和替換,知乎這樣的一攬子方案才是正解。不過(guò)該文章提到不少在實(shí)踐中的坑,有比較多的參考意義,他們還產(chǎn)出了一個(gè) Pod 插件 CocoaPods-bin??偨Y(jié)一下該文章要點(diǎn):
- 改造 CocoaPods-Package ,支持對(duì)單個(gè) pod 進(jìn)行二進(jìn)制編譯,打包上傳靜態(tài)服務(wù)器;
- 基于 Podfile 中添加的全局變量 tdfire_use_source_pods 來(lái)控制 binary 白名單,pod install 時(shí)注入環(huán)境變量以控制源碼切換;
分析
- CocoaPods-Package 作為官方提供的插件在 1.7.0 正式版發(fā)布后做了一次更新,也是時(shí)隔多年,支持了Swift 的 package 及修復(fù)了一些問(wèn)題。以單個(gè) pod 進(jìn)行二進(jìn)制編譯的最大麻煩在于,團(tuán)隊(duì)如果進(jìn)行了比較重度的組件化,一般會(huì)有大量依賴庫(kù)需要維護(hù),如果每個(gè)庫(kù)都需要配置一份 package 腳本成本比較高,同時(shí)第三方庫(kù)也需要進(jìn)行鏡像維護(hù),盡管支持了 CI 自動(dòng)化也需要花費(fèi)一部分精力,同時(shí)業(yè)務(wù)工程師也需要對(duì)項(xiàng)目有完整的認(rèn)知,否則難以捋清其中的關(guān)系。
- 以 IS_SOURCE環(huán)境變量控制 binary 和源碼切換的方式也不是很友好。也是可以給 pod DSL 添加擴(kuò)展來(lái)支持 binary switch。當(dāng)前在每次 install 前加入變量去控制,使用上感覺(jué)有些奇怪;
改造 CocoaPods-Binary
關(guān)于 Cocoapods-Binary 前段時(shí)間寫過(guò)一篇簡(jiǎn)單介紹,淺析 Cocoapods-Binary 實(shí)現(xiàn)。在了解了該插件如何工作之后,就可以將我們端想法付諸實(shí)踐了。
首先,我們要做的事情很多插件都已經(jīng)幫我們完成了,而我們要做的就是簡(jiǎn)單的支持一下對(duì) binary framework 的靜態(tài)服務(wù)器存儲(chǔ)和下發(fā)就好,先來(lái)一張流程圖:
- 上圖中的 featch remote framework 和 upload zips to server 就是我們要做的事情。
在 Prebuild framework 之前檢查當(dāng)前 pod_target 是否有對(duì)應(yīng)的 server cache,存在則 download 至本地同時(shí) unarchive 至 GenerateFramework 文件目錄下,然后跳過(guò)當(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é)束后會(huì)進(jìn)行文件清理和 binary 的替換鏈接,在此時(shí)進(jìn)行批量 binary 文件的同步。將GenerateFramework 目錄中所匹配的 pod_target 且資源服務(wù)器所不存在的 binary 文件進(jìn)行上傳,統(tǒng)一至 static_frameworks 目錄下,文件名則是 pod_name + tag, 例如 pod 'AFNetworking', '3.0'對(duì)應(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
實(shí)踐過(guò)程中,為了方便直接是利用了公司現(xiàn)有的 ftp 文件服務(wù)器,單獨(dú)開(kāi)了一個(gè)進(jìn)行目錄維護(hù)。相比 CocoaPods-binary 僅增加了 ftp_tools.rb 和 zip_tools.rb 兩個(gè)文件,實(shí)現(xiàn)比較簡(jiǎn)單這里就不貼出來(lái)了。
限制
- 最終的 binary size 會(huì)比使用源碼的時(shí)候大一點(diǎn),不建議最終上傳 Store 的時(shí)候使用;
- 缺少一個(gè)驗(yàn)證的機(jī)制,如果已發(fā)布的二進(jìn)制包不能被項(xiàng)目正常引用,那么會(huì)導(dǎo)致所有人的編譯失敗;;
- 由于工程采用的是全部靜態(tài)庫(kù)依賴的形式,所以在二進(jìn)制和源碼切換的過(guò)程中會(huì)對(duì) project 文件產(chǎn)生更改;
- CocoaPods 在 1.7 以上版本,更改了framework 邏輯,不會(huì)把 resource copy 至 framework,因此我們需要將 CocoaPods 版本固定到 1.6.x;
- 對(duì)于動(dòng)態(tài)配置生成的 framework,例如RN 相關(guān)的依賴等,不支持binary;
- 不同版本 Swift 編譯出的 binary 是不能兼容。如果項(xiàng)目中引用了 Swift 庫(kù)Xcode 版本需要統(tǒng)一。
在使用 binary 的過(guò)程中,還有一些意想不到的問(wèn)題。例如,為了減少源碼和 binary 切換過(guò)程中產(chǎn)生的大量 git change,將 Pods 目錄進(jìn)行了 ignore,導(dǎo)致工程師在過(guò)渡階段切換分支中,多數(shù)被限制在 pod install 中一些三方庫(kù)的 download 上面,非翻所不能也。還有,在 install 后發(fā)現(xiàn) pod 對(duì)應(yīng)的 symbol link 沒(méi)有正確生成、對(duì)應(yīng)的 source 沒(méi)有 copy 成功、業(yè)務(wù) framework 打包耗時(shí)超常等一系列問(wèn)題。
總結(jié)
真實(shí)項(xiàng)目實(shí)踐中,沒(méi)有一勞永逸的辦法。不同的業(yè)務(wù)依賴和環(huán)境配置,包括工程代碼的規(guī)范,甚至簡(jiǎn)單的頭文件管理都會(huì)導(dǎo)致開(kāi)發(fā)過(guò)程產(chǎn)生各種各樣的問(wèn)題??傊且粋€(gè)不斷探索和進(jìn)化的過(guò)程。