細聊 Cocoapods 與 Xcode 工程配置

前言

文章比較長,因此在文章的開頭我打算簡單介紹一下這篇文章將要講述的內容,讀者能夠選擇通篇細度,也能夠直接找到本身感興趣的部分。php

既然是談 Cocoapods,那首先要搞明白它出現的背景。有經驗的開發者都知道 Cocoapods 在實際使用中,常常遇到各類問題,存在必定的使用成本,所以衡量 Cocoapods 的成本和收益就顯得很關鍵。css

Cocoapods 的本質是一套自動化工具。那麼瞭解自動化流程背後的原理就很重要,若是咱們能手動的模擬 Cocoapods 的流程,不管是對 Cocoapods 仍是 Xcode 工程配置的學習都大有裨益。好比以前曾經和同事研究過靜態庫嵌套的問題,很遺憾當時沒能解決,如今想來仍是對相關知識理解還不夠到位。這一部分主要是介紹 Xcode 的工程配置,以及 target/project/workspace 等名詞的概念。ios

最後,我會結合實際的例子,談談如何發佈本身的 Pod,提供給別人使用。算是對 Cocoapods 的實踐總結。git

因爲實踐性的操做比較多,我爲本文製做了一個 demo,提交在 個人 Github: CocoaPodsDemo 上,感興趣的讀者能夠下載下來,研究一下提交歷史,或者本身操做一遍。友情提醒: 本文所涉及的靜態庫均爲模擬器製做,請勿真機運行。github

爲何要使用 Cocoapods

咱們知道,再大的項目最初都是從 Xcode 提供的一個很是簡單的工程模板慢慢演化來的。在項目的演化過程當中,爲了實現新的功能,不斷有新的類被建立,新的代碼被添加。不過除了本身添加代碼,咱們也常常會直接把第三方的開源代碼導入到項目中,從而避免重複造輪子,節約開發時間。swift

直接把代碼導入到項目中看起來很容易,但在實踐過程當中,會遇到諸多問題。這些問題會困擾代碼的使用者,大大的增長了集成代碼的難度。vim

使用者的困擾

最直接的問題就是代碼的後續維護。假設代碼的發佈者在將來的某一天更新了代碼,修復了一個重大 bug 或者提供了新的功能,那麼使用者就很難集成這些變更。xcode

代碼有增有刪,若是把代碼編譯成靜態庫再提供給使用者, 就能夠省掉不少問題。然而若是這麼作的話,就會遇到另外一個經典的問題: "Other linker flag"。緩存

舉個例子來講,能夠在 Demo 的 BSStaticLibraryOne 這個項目中看到,這個靜態庫一共有兩個類,其中一個是拓展 Extension。項目編譯後就會獲得一個 .a 文件。ruby

咱們都知道靜態庫的格式能夠是 .framework,也能夠是 .a。若是深究的話,.a 文件能夠理解爲一種歸檔文件,或者說是壓縮文件。其中存儲的是通過編譯的 .o 格式的目標文件。咱們能夠經過 ar -x 命令來證實這一點:

ar -x libBSStaticLibraryOne.a

解壓結果

須要提醒的一點是,光有 .a 文件還不夠,咱們還須要提供頭文件給使用者導入。爲了完成這一點,咱們須要在項目的 Build Phases 中新增一個 Headers Phase,而後把須要對外暴露的頭文件放到 Public 一欄中:


暴露頭文件

此時編譯後的頭文件會放在 .a 文件所在目錄下,usr/local/include 目錄中。

接下來打開 OtherLinkerFlag 這個殼工程,引入 .a 文件和頭文件,運行程序,結果必定是:

-[BSStaticLibraryOne sayOtherThing]: unrecognized selector sent to instance xxx

這就是經典的 linker flag 問題。首先,咱們知道 .a 實際上是編譯好的目標文件的集合,所以問題出在連接這一步,而非編譯。Objective-C 在使用靜態庫時,須要知道哪些文件須要連接進來,它依據的就是以前圖中所示的 __.SYMDEF SORTED 文件。

惋惜的是,這個文件不會包含全部的 .o 目標文件,而只是包含了定義了類的目標文件。咱們能夠執行 cat __.SYMDEF\ SORTED 來驗證一下,你會看到其中並無拓展類的信息。這樣一來,BSStaticLibraryOne+Extension.o 雖然存在,可是不被連接到最終的可執行文件中,從而致使了找不到方法的錯誤。

解決上述問題的方法是調用者在 Build Settings 中找到 other linker flag,並寫上 -ObjC 選項,這個選項會連接全部的目標文件。然而根據文檔描述,若是靜態庫只有分類,而沒有類, 即便加了 -ObjC 選項也會報錯,應該使用 -force_load 參數。

因爲第三方的代碼使用分類幾乎是必然事件,所以幾乎每一個使用者都要作如上配置,增長了複雜度和出錯的概率。

除此之外,第三方的代碼頗有可能使用了系統的動態庫。所以使用者還必須手動引入這些動態庫(請記住這一點,靜態庫不支持遞歸引用,這是個很麻煩的事情,後面會介紹),咱們以百度地圖 SDK 的集成爲例,讀者能夠自行對比手動導入和 Cocoapods 集成的步驟區別: 配置開發環境iOS SDK

所以,我總結的使用 Cocoapods 的好處有以下幾個:

  1. 避免直接導入文件的原始方式,方便後續代碼升級
  2. 簡化、自動化集成流程,避免沒必要要的配置
  3. 自動處理庫的依賴關係
  4. 簡化開發者發佈代碼流程

Cocoapods 工做原理

在我以前的一篇文章: 白話 Ruby 與 DSL 以及在 iOS 開發中的運用 中簡單的介紹過,Cocoapods 是用 Ruby 開發的一套工具。每一份代碼都是一個 Pod,安裝 Pod 時首先會分析庫的版本和依賴關係,這些都是在 Ruby 層面完成的,本文暫且不表。

咱們首先假設已經找到了要下載的代碼的地址(好比存在 Github 上),從這一步開始,接下來的工做都與 iOS 開發有關。

若是你手頭有一個 Cocoapods 項目,你應該會注意到如下幾個特色:

  1. 主工程中沒有導入第三方庫的代碼或靜態庫
  2. 主工程不顯式的依賴各個第三方庫,可是引用了 libPods.a 這個 Cocoapods 庫
  3. 不須要手動編譯第三方庫,直接運行主工程便可,隱式指定了編譯順序

這樣作能夠把引入第三方庫對主工程形成的影響降到最低,不過沒法徹底降爲零。好比引入 Cocoapods 之後,項目不得不使用 xworkspace 來打開,後面會介紹緣由。

假設以前的 BSStaticLibraryOne 工程就是下載好的源碼,如今咱們要作的就是把它集成到一個已有的工程,好比叫 ShellProject 中。

咱們遇到的第一個問題是,在以前的 demo 中,須要把靜態庫和頭文件手動拖入到工程中。但這就和 Cocoapods 的效果不一致,畢竟咱們但願主工程徹底不受影響。

靜態庫和頭文件導入

若是咱們什麼都不作,固然不可能在殼工程中引用另外一個項目下的靜態庫和頭文件。但這個問題也能夠換個方式問:「Xcode 怎麼知道它們能夠引用,仍是不能夠引用呢?」,答案在於 Build Settings 裏面的 Search Paths 這一節。默認狀況下,Header Search Path和 Library Search Path 都是空的,也就是說 Xcode 不會去任何目錄下找靜態庫和頭文件,除非他們被人爲的導入到工程中來。

所以,只要對上述兩個選項的值略做修改, Xcode 就能夠識別了。咱們目前的項目結構以下所示:

- CocoaPodsDemo(根目錄) - BSStaticLibraryOne (被引用的靜態庫) - Build/Products/Debug-iphonesimulator (編譯結果的目錄) - libBSStaticLibraryOne.a (靜態庫) - usr/local/include (頭文件目錄) - BSStaticLibraryOne.h - BSStaticLibraryOne+Extension.h - ShellProject (殼工程)

所以咱們要作的是讓殼工程的 Library Search Path 指向CocoaPodsDemo/BSStaticLibraryOne/Build/Products/Debug-iphonesimulator 這個目錄:

Library Search Path = $PROJECT_DIR/../BSStaticLibraryOne/Build/Products/Debug-iphonesimulator/

這裏記得寫相對路徑,Xcode 會自動轉成絕對路徑。而後 Header Search Path 也如法炮製:

Header Search Path = $PROJECT_DIR/../BSStaticLibraryOne/Build/Products/Debug-iphonesimulator/LibOne

細心的讀者也許會發現, LibOne 這個文件夾徹底不存在。是這樣的,由於我以爲usr/local/include 這個路徑太深,太醜,因此能夠在靜態庫的項目配置中,在Packaging 這一節中,找到 Public Headers Folder Path,將它的值從usr/local/include 修改成 LibOne,而後從新編譯,這時就會看到生成的頭文件位置發生了變化。

固然,這時候仍是沒法直接引用靜態庫的。由於咱們只是告訴 Xcode 能夠去對應路徑去找,但並無明確聲明要用,因此須要在 Other Linker Flags 中添加一個選項: -l"BSStaticLibraryOne",引號中的內容就是靜態庫的工程名。

須要提醒的是, 靜態庫編譯出來的 .a 文件會被手動加上 lib 前綴,在寫入到 Other Linker Flags 的時候千萬要注意去掉這個前綴,不然就會出現 Library not found 的錯誤。

配置好之後的工程以下圖所示:


配置搜索路徑

如今項目中沒有任何第三方的庫或者代碼,依然能夠正常引用第三方的類並運行成功。

引用多個第三方庫

當咱們的項目須要引用多個第三方庫的時候,就有兩種思路:

  1. 每份第三方代碼做爲一個工程,分別打出一個靜態庫和頭文件。
  2. 全部第三方代碼放在同一個工程中,創建多個 target,每一個 target 對應一個靜態庫。

從直覺來看,第二種組織方式看上去更加集中,易於管理。考慮後面咱們還要解決庫的依賴問題,並且項目內的依賴處理比 workspace 中的依賴處理要容易不少(後面會介紹到),因此第二種組織方式更具備可行性。

若是讀者手頭有使用了 Cocoapods 的項目,能夠看到它的文件組織結構以下:

- ShellProject(根目錄,殼工程) - ShellProject (項目代碼) - ShellProject.xcodeproj (項目文件) - Pods (第三方庫的根目錄) - Pods.xcodeproj (第三方庫的總工程) - AFNetworking (某個第三方庫) - Mantle (另外一個第三方庫) - ……

而在個人 demo 中,爲了偷懶,沒有把第三方庫放在殼工程目錄下,而是選擇和它平級。這其實沒有太大的區別,只是引用路徑不一樣而已,不用太關心。咱們如今模擬添加一個新的第三方庫,完成後的代碼結構以下:

- CocoaPodsDemo(根目錄) - BSStaticLibraryOne (第三方庫總的文件夾,至關於 Pods,由於偷懶,名字就不改了) - BSStaticLibraryOne (第一個第三方庫) - BSStaticLibraryTwo (新增一個第三方庫) - BSStaticLibraryOne.xcodeproj (第三方庫的項目文件) - Build/Products/Debug-iphonesimulator (編譯結果的目錄) - ShellProject (殼工程)

首先要新建一個文件夾 BSStaticLibraryTwo 並拖入到項目中,而後新增一個 Target(以下圖所示)。


新增 target

在 Xcode 工程中,咱們都接觸過 Project。打開 .xcodeproj 文件就是打開一個項目(Project)。Project 負責的是項目代碼管理。一個 Project 能夠有多個 Target,這些 target 可使用不一樣的文件,最後也就能夠得出不一樣的編譯產物。

經過使用多個 target,咱們能夠用少量不一樣的代碼獲得不一樣的 app,從而避免了開多個工程的必要。不過咱們這裏的幾個 target 並不含有相同代碼,而是一個第三方庫對應一個 target。

接下來咱們新建一個類,記得要加入到 BSStaticLibraryTwo 這個 target 下,記得和以前同樣修改 Public Headers Folder Path 並添加一個 Build Phase。


代碼添加到另外一個 Target

在左上角將 Scheme 選擇爲 BSStaticLibraryTwo 再編譯,能夠看到新的靜態庫已經生成了。

項目內依賴

對於主工程來講,必須在子工程(第三方庫)編譯完後纔開始編譯,或者換句話說,咱們在主工程中按下 Command + R/B 時,全部子工程必須先被編譯。對於這種跨工程的庫依賴,咱們沒法直接指明依賴關係,必須隱式的設置依賴關係,咱們仍是以 Cocoapods 工程舉例:


跨工程依賴

主工程中用到了 libPod.a 這個靜態庫,並且它並非在主工程中生成,而是在 Pods 這個項目中編譯生成。一旦存在這種引用關係,那麼也就創建了隱式的依賴關係。在編譯主工程時,Xcode 會確保它引用的全部靜態庫都先被編譯。

以前咱們討論過兩種管理多個靜態庫的方法,若是選擇第一種方法, 每一個靜態庫對應一個 Xcode 項目,雖然不是不能夠,但主工程看上去就就會比較複雜,這主要是跨項目依賴致使的。

而在項目內部管理 target 的依賴相對而言就簡單不少了。咱們只要新建一個總的 target,不妨也叫做 Pod。它什麼也不作,只須要依賴另外兩個靜態庫就能夠了,設置 Target Dependencies:

此時選擇 Pod 這個 target 編譯,另外兩個靜態庫也會被編譯。所以接下來的任務就是讓主工程直接依賴於 Pod 這個 target,天然也就間接依賴於真正有用的各個第三方靜態庫了。

接下來咱們重複以前的步驟,設置好頭文件和靜態庫的搜索路徑,並在 Other Linker Flags 裏面添加: -l"BSStaticLibraryTwo",就可使用第二個靜態庫了。

Workspace

到目前爲止,咱們模擬了多個靜態庫的組織,以及如何在主工程中引用他們。不過還存在一些小瑕疵,我截了 Xcode 中的一幅圖:


Xcode 識別有問題

從圖中能夠很明顯的發現: 第三方庫中的代碼被認爲是系統代碼,顏色爲藍色。而正常的自定義方法應該綠色,會對開發者形成困擾。

除了這個小瑕疵之外,在以前談到的跨項目依賴中,一個項目不只僅須要引用另外一個項目的產物,還有一個先決條件: 把這兩個項目放入同一個 Workspace 中。Workspace 的做用是組織多個 Project,使得各個 Project 直接能夠有引用依賴關係,同時也能讓 Xcode 識別出各個 Project 中的代碼和頭文件。

按住 Command + Control + N 能夠新建一個 Workspace:


新建 Workspace

完成之後就會看到一個徹底空白的項目,在左側按下右鍵,選擇 Add Files to:


添加文件

而後選中靜態庫項目和主工程的 .xcodeproj 文件,把這兩個工程都加進來:

須要提醒的是,切換到 Workspace 之後, Xcode 會把 Workspace 所在目錄當作項目根目錄,所以靜態庫的編譯結果會放在 /CocoaPodsDemo/Build/Products/...,而再也不是以前的 /CocoaPodsDemo/BSStaticLibraryOne/Build/Products/...,所以須要手動對主工程中的搜索路徑作一下調整。

作好上述改動後,即便咱們刪除掉 BSStaticLibraryOne 這個項目的編譯結果,只在 Workspace 中編譯主項目,Xcode 也會自動爲咱們編譯被依賴的靜態庫。這就是爲何咱們只須要執行 pod install 下載好代碼,就能夠不用作別的操做,直接在主項目中運行。

固然,代碼顏色錯誤的小問題也在 Workspace 恢復正常了。

靜態庫嵌套

到這裏,基本上關於 Cocoapods 的工做原理就算是分析完了。上述操做除了文件增長,基本上都是修改 .pbxproj 文件。全部的 Xcode 都會在該文件中獲得反映,同理,只要修改該文件,也能達到上述手動操做的效果。而 Cocoapods 開發了一套 Ruby 工具,用來封裝這些修改,從而實現了自動化。

文章開頭,咱們提到做爲代碼提供者,若是本身的代碼還引用別的第三方庫,那麼提供代碼會變得很麻煩,這主要是因爲靜態庫不會遞歸引用致使的。咱們已經知道靜態庫其實就是一堆編譯好的目標文件(.o 文件)的打包形式,它須要配合頭文件來使用。所謂的不會遞歸引用是指,假設項目 A 引用了靜態庫 B(或者是動態庫,也是同樣),那麼 A 編譯後獲得的靜態庫中,並不含有靜態庫 B 的目標文件。若是有人拿到這樣的靜態庫 A,就必須補齊靜態庫 B,不然就會遇到 "Undefined symbol" 錯誤。

若是咱們提供的代碼引用了系統的動態庫,問題還比較簡單,只要在文檔裏面註明,讓使用者本身導入便可。但若是是第三方代碼,那麼這簡直是一塊兒災難。即便使用者找到了提供者使用的靜態庫,那個靜態庫也頗有可能已經進行了升級,而版本不一致的靜態庫可能具備徹底不一樣的 API。也就是說代碼提供者還要在文檔中註明使用的靜態庫的版本,而後由使用者去找到這個版本。我想,這纔是 Cocoapods 真正致力於解決的任務。

CocoaPods 的作法比較簡單,由於他有一套統一的版本表示規則,也能夠自動分析依賴關係,並且每一個版本的代碼都有記錄。後面會介紹 Cocoapods 的相關實踐,這裏咱們先思考一下如何手動解決靜態庫嵌套的問題。

既然靜態庫只是目標文件的打包形式,那麼我只須要找到被嵌套的靜態庫,拿到其中的目標文件,而後和外層的靜態庫放在一塊兒從新打包便可。這個過程比較簡單, 我也就沒有作 demo,用代碼應該就能夠說明得很清楚。假設咱們有靜態庫 A.a 和 B.a,其中 A 須要引用 B,如今我但願對外發布 A,而且集成 B:

lipo A.a -thin x86_64 output A_64.a # 若是是多 CPU 架構,先提取出某一種架構下的 .a 文件 lipo B.a -thin x86_64 output B_64.a ar -x A_64.a # 解壓 A 中的目標文件 ar -x B_64.a # 解壓 B 中的目標文件 libtool -static -o Together.a *.o # 把全部 .o 文件一塊兒打包到 Together.a 中

這時候 Together.a 文件就能夠當作完整版的靜態庫 A 給別人使用了。

Cocoapods 使用

原本 Cocoapods 的使用就比較簡單。尤爲是瞭解完原理後,使用起來應該更加駕輕就熟了,對於一些常見的錯誤也有了分析能力。不過有個小細節仍是須要注意一下:

Podfile.lock

關於 Cocoapods 文件是否要加入版本控制並無明確的答案。我之前的習慣是不加入版本控制。由於這樣會讓提交歷史明顯變得複雜,若是不一樣分支上使用的不一樣版本的 pod,在合併分支時就會出現大量衝突。

然而官方的推薦是把它加入到版本控制中去。這樣別人再也不須要執行 pod install,並且可以確保全部人的代碼必定一致。

然而雖然不強制把整個 Pod 都加入版本控制,可是 Podfile.lock 不管如何須須添加到版本控制系統中。爲了解釋這個問題,咱們先來看看 Cocoapods 可能存在的問題。

假設咱們在 Podfile 中寫上: pod 'AFNetWorking',那麼默認是安裝 AFNetworking 的最新代碼。這就致使用戶 A 可能裝的是 3.0 版本,而用戶 B 再安裝就變成了 4.0 版本。即便咱們在 Podfile 中指定了庫的具體版本,那也不能保證不出問題。由於一個第三方庫還有可能依賴其餘的第三方庫,並且不保證它的依賴關係是具體到版本號的。

所以 Podfile.lock 存在的意義是將某一次 pod install 時使用的各個庫的版本,以及這個庫依賴的其餘第三方庫的版本記錄下來,以供別人使用。這樣一來,pod install 的流程實際上是:

  1. 判斷 Podfile.lock 是否存在,若是不存在,按照 Podfile 中指定的版本安裝
  2. 若是 Podfile.lock 存在,檢查 Podfile 中每個 Pod 在 Podfile.lock 中是否存在
  3. 若是存在, 則忽略 Podfile 中的配置,使用 Podfile.lock 中的配置(實際上就是什麼都不作)
  4. 若是不存在,則使用 Podfile 中的配置,並寫入 Podfile.lock 中

而另外一個經常使用命令 pod update 並非一個平常更新命令。它的原理是忽略 Podfile.lock 文件,徹底使用 Podfile 中的配置,而且更新 Podfile.lock。一旦決定使用 pod update,就必須全部團隊成員一塊兒更新。所以在使用 update 前請務必瞭解其背後發生的事情和對團隊形成的影響,而且確保有必要這麼作。

發佈本身的 Pod

不少教程都有介紹開源 Pod 的流程,我在實踐的時候主要參考瞭如下兩篇文章。相對來講比較詳細,條理清晰,也推薦給你們:

  1. Cocoapods系列教程(二)——開源主義接班人
  2. Cocoapods系列教程(三)——私有庫管理和模塊化管理

若是要建立公司內部的私有庫,首先要創建一個本身的倉庫,這個倉庫在本地也會有存儲:


倉庫

如圖中所示,master 是官方倉庫,而 baidu 則是我用來測試的私有倉庫。倉庫中會存有全部 Pod 的信息,每一個文件夾下都按照版本號作了區分,每一個版本對應一個 podspec 文件。從圖中能夠看到,cocoapods 會緩存全部的 podspec 到本地,但不會緩存每一個 Pod 的具體代碼。每當咱們執行 pod install 時,都會先從本地查找 podspec 緩存是否存在,若是不存在則會去中央倉庫下載。

咱們常常遇到的 pod install 很慢就是由於默認狀況下會更新整個 master。此時 master 不只僅存儲着本地使用 Pod 的 PodSpec 文件,而是存儲了全部的已有的 Pod。因此這個更新過程看起來異常緩慢。有些解決方案是使用:

pod install --verbose --no-repo-update

這實際上是治標不治本的姑息治療方法,由於本地的倉庫早晚要被更新,不然就拿不到最新的 PodSpec。要想完全解決這一問題,除了按期更新外,還能夠選擇其餘速度較快的鏡像倉庫。

podspec 文件是咱們開源 Pod 時須要填寫的文件,主要是描述了 Pod 的基礎信息。除了一些可有可無的配置和介紹信息外,最重要的填寫 source_files 和 dependency。前者用來規定哪些文件會對外公佈,後者則指定此 Pod 依賴於哪些其餘 Pod。好比在上圖中,個人 PrivatePod 就依賴於 CorePod,在公司內部的項目中使用 PodS 依賴能夠大量簡化代碼的集成流程。一個典型的 PodSpec 可能長這樣:

填寫好上述信息後,咱們只要先 lint 一下 podspec,確保格式無誤,就能夠提交了。

相關文章
相關標籤/搜索