學習 CocoaPods:Swift、框架以及模塊
About the Speaker: Marius Rackwitz
Marius 是一位住在柏林的 iOS 開發者。作為 CocoaPods 開發團隊的核心成員,他負責實現 CocoaPods 對 Swift 以及動態框架的支持。當他從開發 iOS & Mac 工具的任務中空閑下來的時候,他會參與到 Realm 的開發當中來,這是一個為 iOS以及Android平臺設計的全新數據庫。
About the Speaker: Orta Therox
Orta 是 Artsy 的首席 iOS 開發工程師,Artsy 是一個收集世界所有藝術作品的應用。他受到 Artsy 開源承諾的激勵,因此他常常致力于 CocoaPods 生態系統的發展,包括諸如 CocoaDocs 之類工具的構建,維護 Specs 遠程倉庫,以及文檔的校正。如果 CocoaPods 團隊擁有頭銜的話,他應該會被稱為“社區經理”。
Marius Rackwitz 和 Orta Therox 是 CocoaPods 的主要貢獻者之一。Marius 完成了 CocoaPods 中所有 Swiftification 以及 Frameworkification 的工作(用新的方法替代了舊方法);Orta 則是設計總監,并且他的貢獻遠不止于此(比如說維護本身就是一個龐大的項目的 CocoaDocs,CocoaPods 的官網、插件等等)。他們將會談論關于 CocoaPods 動態框架的實際過渡流程,并且提供如何制作動態框架的相關建議。在本次講座中,將會包含下列內容:
- CocoaPods 是什么? :它是一個第三方庫管理器,更為重要的是,它是一個社區。它自建立起已經有五個年頭了,非常成熟和穩定;
- 術語介紹 :目標(target),通常存在于您項目中的應用或者應用擴展;
- 編譯階段(build phase) :CocoaPods 的編譯階段,不是 Xcode 的。
整合歷史(03:36)
動態框架并不是一直都有提供支持的。在動態框架出現之前,我們可以使用靜態庫(直到 iOS 7 之后便不再可用)。您可以通過 Linked Frameworks and Libraries 來偷使用靜態庫以及標準框架,然而這樣的話你就不能使用 App Bundle 來分發你得框架了。我們對它們進行了整合工作,因為它們只能在 OS X 以及 iOS 平臺(僅有的兩個平臺)上可用了!
什么是靜態庫(03:48)
靜態庫并不會進行所謂的“鏈接”,它們是 一系列對象文件的集合 (對象文件通過元信息進行了編譯)。它將機器代碼從原始位置中提煉出來,總而言之它們只是編譯過的對象文件而已。
當我們使用 Clang 編譯我們的源代碼的時候,我們將會得到對象文件,然后將其放入到 Ranlib 當中(將它們進行打包)。歸檔文件中擁有不少內容(比如說整個文件中某個對象文件位于何種位置),存有文件信息,以及對象文件的原始名稱。我們為每個架構都進行了歸檔,最后使用 Lipo 命令將所有歸檔文件整合到一起,從而得到一個龐大的二進制文件。
在幻燈片上,我們可以看到一個BananaKit庫(和另一個Monkey庫建有依賴關系),以及一個 SealEye 工具(這是我們準備構建的的程序,它引用了這兩個庫)。對于簡單的應用來說,我們只需要把所有東西放在一起,然后一起編譯,建立關聯即可。對于 CocoaPods 來說,我們會建立排斥項目(exclude project)以及靜態庫目標。此外(由于靜態庫并不包含或者說并不支持資源文件以及其他關聯文件),我們需要諸如關聯模型(correlater model)之類的東西。CocoaPods 通過使用一個額外的名為 CocoaPods 的編譯文件(用于所集成的目標)解決了這個問題,它是一個資源編譯文件。因此,這和我們使用腳本來編譯文件沒什么區別:我們運行 shell 腳本(存放于另一個 shell 文件當中,因為重新生成一個單獨的 shell 文件很容易,而讓這個腳本存放在 Xcode 中的 Pods 項目中就沒那么簡單了,這會導致集成的結果發生較大的變化)。而現在,我們需要動態框架了(因為 Swift 只能用它)。
動態框架(Swift)(08:37)
框架內部 + Cocoa Touch 框架(09:37)
對于框架來說,如果你在 Finder (編譯項目中)中查看其中的內容,你會發現它不過只是一個文件包而已。比如說 Alamofire 這個非常熱門的網絡庫(如果我們使用 Swift 來完成網絡訪問):
- 第一個文件是可執行文件:Alamofire,一個動態庫文件。(實際上,它們是靜態框架;你可以在這個地方放上其他的靜態庫,你會發現它 竟然可以 正常工作);
- 下面是頭文件,有 Swift 鏈接頭文件,還有用于對象橋接 Swift 模型的一部分。Umbrella 頭文件是非常重要的(和模型映射文件在一起的),因為它對定義公共接口來說十分有用。Umbrella 頭文件將會被模型映射文件所引用。由這個頭文件所導入的所有內容都將是公共模型接口的一部分,并且和其建立連接的所有對象都可以使用;
- 我們同樣還可以看到其余文檔信息,這些信息都會被工具的其余部分所使用。這些都是專門給特定平臺所使用的(我們可以通過文件名看出來);
- Info.plist,這里面包含了諸多元信息。其中包含了存在 Umbrella 頭文件中的相關信息(比如說版本和版權信息),默認情況下這些信息都會被用以進行讀取。這里有一個很關鍵的字段——庫版本號,這是一個常量。在框架編譯的時候這個版本信息就會從 info.plist 文件中提取出來以供讀取。C 文件會讀取這個字段,并且它并不會在錯誤的時刻從 info.plist 中讀取這個字段,因為它作為二進制文件的一部分,將會被編譯進去。這個信息僅在導入和建立鏈接的時候有用。如果你將你的應用打包為 App Store 預發布版本,info.plist 會提供關于庫文件的相關信息。如果你在應用和應用擴展之間共享代碼的話,還需要提供受保護的信息、框架文檔等等內容。
動態庫(14:01)
動態庫是可以關聯映像的文件(在其上運行有鏈接器)。我們將源代碼進行編譯,然后在上面運行鏈接器(linker)。這里有 Mac 對象文件,它是一個擁有元數據信息的頭文件(比如說依賴庫),并且我們會對每個所需的架構重復執行這些個操作流程。對于 Swift 文件來說也是同樣的道理,只不過編譯過程不一樣而已。首先我們把 Monkey 這個 Swift 文件進行編譯,然后給我們的公共接口寫入數據。接著我們編譯好 BananaKit(我們必須將我們所使用的外部符號:MKMonkey,鏈接給所有引用它的 Monkey Swift 文件、庫文件以及注釋等等)。我們編譯完所有的程序,當然這時候我們仍然不能將其導出,因為程序只是對自身進行了鏈接,并沒有其他的程序鏈接給它。我們必須給這兩個庫解決符號引用的問題。
動態框架必須嵌入到包(框架文件夾)當中。構建文件是一個基于名字拷貝的文件。【Marius 運行了一個關于 pod 文件的樣式如何的示例】。我們需要拿出一個腳本,允許嵌入到 pods 對于當前編譯配置是可用的(Xcode 不允許我們對此進行配置)。 頭文件需要被分離出來 ,然后框架需要簽名。所有的一切都需要對自己本身進行簽名:框架會被 單獨分配一個簽名 ,其他的可執行文件單獨分配一個簽名。
框架和靜態庫的對比(18:44)
優點:
- 一旦框架構建并編譯完畢之后,就更容易進行分發操作以及集成到應用當中。(因為框架將所有內容都整合到了一起,并且允許向包中寫入資源文件)。并且它們擁有獨立的命名空間。
- 如果你在應用和應用擴展之間通過框架共享代碼的話,可以有效減少文件大小。
- 在應用擴展中不同的框架文件夾:你可以框架進行分割,也可以擁有相同框架的兩個版本。通過分離包,就可以只允許在框架包當中通過名字訪問資源。
缺點:
- 優化變得很困難,因為在你構建好之后就已經建立關聯了。
- 由于靜態鏈接的問題,剝離死代碼變得很困難。你可以隨意導入對象文件,當它們不再需要或者不再被包含的時候只能手動刪除。
- 增加加載時間。
對于 CocoaPods 來說這意味著什么?(23:19)
對于 CocoaPods 來說,我們最終需要:
- 代碼擴展(比如說 Clang 模組)。要實現此功能,我們必須允許你在使用 CocoaPods 的時候對我們的代碼進行擴展(比如說在使用動態框架的時候)。我們此前這樣做的目的是確保人們直到他們正在使用動態框架(當您不再使用的時候我們最后會自行幫您刪除掉動態框架)。
- 我們需要集成腳本:在 Podfile 末尾加上這些信息是很有必要的,這樣可以讓使用過程變得清晰。
- 我們需要借助蘋果提供的工具。代碼簽名非常難以實現( 蘋果不會在它進行更新的時候發出任何通知 )。很明顯,我們不能在我們的持續繼承流程中加入最終部署到 App Store 的操作。我們需要了解用戶的反饋。
Orta:遷移(25:30)
我曾經將三大應用從 CocoaPods 靜態庫遷移到了 CocoaPods 動態框架。我使用一種可以將 CocoaPods 遷移到項目中的插件——Cocoapods-deintegrate。這樣當你下次運行 pod-install 的時候你就會有一個干凈的狀態。
總之,這個插件將會刪除 pods/ 文件夾,改變某些編譯階段設置,并且將所有 CocoaPods 生成的空 Xcode 組文件夾移除。這樣,執行下面三個步驟就會十分簡單:在 Podfile 中聲明動態框架,這樣就會在下次你執行 pod install 的時候將靜態庫切換為動態框架。接著,執行測試即可(從中你可以找到所有的失敗原因)。
常見錯誤(27:09)
有這么一些常見的錯誤:
- Pods 取決于靜態庫(比如說 Google Analytics、Flurry)。我為了讓兩個 pods 能夠同時工作,我最終需要添加代碼讓這兩個文件鏈接到一起。另一個問題是所有發布庫的人員,你需要開始發布動態框架。
- 庫會在 main bundle 中導入資源文件。它們會假設所有資源都已經存放在 main bundle 中了,這意味著你必須要處理丟失的資源文件(絕大多數時候)。
- 對于 UIFont Pods,我們需要使用動態鏈接(我們甚至不能依賴將字體名字寫入到info.plist文件當中)。
- Pods 通過使用 #define 來改變行為模式。
使用 CocoaPods 框架的應用(29:00)
有三個應用轉換了過來: Eidolon ,一個 Swift 應用; Energy ,花費了兩到三天來編輯 pods 文件以及改變內部結構; Eigen ,最為復雜(超過了60個pods文件)。我們對其進行了轉換,將其轉為使用動態框架進行工作。在應用中鏈接有60個 pods 文件,在啟動的時候發現它們并沒有工作。我的最終解決方案就是將它們轉為原來的靜態庫。作為一個應用,我們不使用 Swift 以及擴展——沒有任何理由能說服我們使用它。
幫助以及問答時刻(32:05)
問:你們對 Carthage 怎么看呢?為什么 CocoaPods 的依賴模型更好呢?
Marius:這個問題很難回答(至少對于后半部分來說,因為要花費很多時間)。我要說的是這兩個依賴庫管理器實現目標的方式是有很大差異的,CocoaPods 試圖完成所有的整合和管理工作,盡可能讓用戶明白我們的工作。我們的所有工作你都可以看到,因此當你在 Xcode 中使用 CocoaPods 的時候你可以對 CocoaPods 進行一些自定義操作。即時是編譯過程都是使用的 Xcode 自身提供的。因此這很容易忽略 Xcode 中這些依賴庫的存在,因為你不需要讓目前工作的 Xcode 項目持有我們的 pods 文件。如果你不喜歡在 Xcode 工作項目中內含第三方庫的話,pod specs 是你的最佳選擇。而 Carthage 則給出了另外一種實現方式。對于現有的四個平臺來說,我覺得我們更喜歡堅持我們現在所使用的方式。
問:沒錯,但是有人說通過將第三方項目直接放進項目的方式來管理依賴會增進用戶體驗,對此你們怎么看?
Orta:是的,Tumblr 應用向我們報告說它們不喜歡這么用,因為它們近百個依賴庫都要平均花費5到6秒的時間來編譯。當然,目前我們……
問:所以這個講座的潛臺詞是“如果能自己管理框架就自己管理咯”?
Orta:我想有很大一部分應用都會面臨這樣一個問題,因此我的回答是“或許吧”。對于某些框架或者靜態庫而言你必須要找到一個超級復雜的方法來解決它。
Marius:我覺得這取決于你的應用的實際情況,包括如何使用的依賴庫、有多少依賴庫、以及是否對你正在使用的外部項目有所了解。第三方依賴庫本質上并不是屬于“第三方”的,因為它們當中還包含了另外的開源項目。如果你的代碼中四處散落著各種小型庫的話,那么在加載過程中可能會發生沖突。但是如果一小部分大型依賴的話,那么你就會在加載過程中得到好處(一個大的可執行文件比小的但卻擁有大型依賴的可執行文件要花費更多時間加載)。
Orta:對的,就像多面體一樣,有很多面,但是體積依然很小,這是很合理的。我們仍舊致力于處理單個或多個的框架,但是目前并沒有打算將他們進行整合的計劃。
問:很高興你們嘗試在自己的應用中使用為 Swift 版本準備的 CocoaPods。那么你們有沒有一個使用所有 CocoaPods 版本的測試應用發布到App Store上了呢?這可以確保蘋果仍舊接受你們目前所做的工作?
Marius:沒有,我們不會這樣做,因為沒有一個比較好的方式來這樣做。
問:那這樣的話如果出現了問題,有人告訴你們“因為你們的東西蘋果拒絕了上架”的話怎么辦?
Marius:事實上,在我們的代碼團隊中有一批經驗豐富的開發者正進行著代碼核審工作,我們使用自己構建的技術來防止這種事親的發生。不過邊界情況還是會時有發生,尤其是使用了擴展以及諸如上一個版本提供的共享依賴之類的情況,在 watchKit 擴展或者 watchKit 應用中這些東西很難保證,我們無法通過一個大的集成方案來很好地測試它們。
問:有人有一個想法,如果你們有空的話,為什么不搭建一個服務器,檢查能夠進行編譯并分發給 App Store 的應用所使用最新的 CocoaPods 版本呢?
Marius:如果能夠從 iTunes Connect 拋棄版本的話,或許可以試試
Orta:因為你不能保證蘋果會……
Marius:尤其是對于一個工具來說,這樣收集用戶信息可能會是個大問題,不過如果遵循相關規定的話,這也是可以的。
Orta:我們可以讓 Felix 來做這個工作。如果我們禮貌地請求的話,他或許一天就可以搞定了。
問:另一個問題是 pods 文件是否應該經常進行檢查,不僅僅是包括支持文件的版本,而且還包括了 pod 文件本身?
Marius:這個問題我們倆都可以回答。我想說這得看情況,但是就我個人而言我更喜歡將 pods 文件放到根目錄下,因為這對持續繼承很友好。當運行 pod install 的時候,如果有新的額外版本的話,那么無需強制推送到 master 分植中,只需要用相同的版本提交一個 commit 即可。我們很依賴于遠程倉庫提供的幫助,當某些東西出問題的時候(比如說強制推送)那么你不應該改變任何核心東西。如果你不這樣做的話,如果代碼發生了改變,那么可能會發生一些不一樣的事情。你無法阻止這種事情發生,因為一旦你在目錄中存放了 pods 文件夾,那么它就可能無法再運行 CocoaPods 了,即時 CocoaPods 已經安裝了。因此這種做法最好是應該避免的,我知道一些開發者特別喜歡使用 Github 所使用的 pods 目錄模型。
Orta:在我們所有的應用當中,我們不會這么做;而在我們所有的庫當中,我們會這么做。對于庫來說,你可以很容易的得到 Carthage 的支持;在進行開發的很長一段時間后,你都可能要回到這個庫當中進行一些操作。同樣,你不能完全依賴于別人的代碼;檢查 pods 文件意味著你可以經常讓庫保證更新,并且保證這個庫對你的應用來說是有用的。比如說 推ter 突然決定替換掉 推ter pod,并且不再支持所有非官方老版本的 推ter SDK。如果你檢查過你的 pods 文佳后你會發現你仍然可以在你的 fork 中(或者 repo 中)使用這個 SDK。但是通常情況下,我們正在工作的應用都已經開發了很長很長的時間,并且仍然維持著更新。當 pod 消失的話,這很容易在第二天發現這個問題,我們會為這個問題找到替代方案。在你的代碼復查中并沒有這些亂糟糟的問題,同樣在我們 pods 文件夾中也包含有你應用的鍵值,在這里面我們也包含了不少有用的信息。對于人們為什么會問這個問題并沒有太大的疑問,在我看來,對于這些問題來說并沒有一個很好的答案。我們正致力于解決這個問題,通過一個圖表表明這么做的好處和壞處。您對此應該有一個自己的想法。
Orta:我想設立一個關于 CocoaPods 的新網站(我不想成為寫代碼的一份子,我只想做做設計,如果有人對 Ruby 網站有經驗的話,那么是再好不過了)。我想做一個專門談論 pods 的小網站。你可以在上面看到哪些 pods 是我認為可以共同工作或者會發生沖突的。上面也會有很多函數響應式編程的 pods,或者其他有價值的內容。不管怎樣這些內容都是要有價值的,這個網站將會成為人們發表意見的一個全新方式。這正是我所想看到的。
Marius:我想要看到大家問一些比較容易解決的問題,對于框架編寫來說也是一樣的。我們仍想要啟用“配置依賴 pod文件”;如果有這個玩意兒的話,我們就能夠用拷貝文件、資源文件來進行替代,然后讓 Xcode 來完成這個工作(這樣就可以不用我們所編寫的不穩定的腳本,我們嘗試完成這個工作,但是沒有任何消息透露出來我們如何實現這個功能)。它們必須復制所有有效的文件,然后在我們自己的 shell 腳本當中完成這些任務。在很早以前這就已經被證明是很容易出錯的了,尤其是當 Apple 改變了或者調整了內部的某些東西的時候。我想盡可能實現集成可以更加接近 Apple 所想實現的方式。
Orta:我對此還要補充一點。當你每次在 CocoaPods 項目中按下“構建”按鈕之后,你會發現這個運行的腳本階段。解決這個問題的話可以為大家解決一個心里的梗。有將近 40,000 個項目在使用 CocoaPods。如果你在使用我們的產品的話,我們一旦解決了這個問題,你就可以節省至少一秒的時間。如果每個人節省的時間加起來,就很可觀了。這完全值得有人為此建個紀念網站(當然首先要把我提出的那個網站完成)。謝謝大家!
Marius:謝謝大家!
See the discussion on Hacker News .
Sign up to be notified of new videos — we won’t email you for any other reason, ever.