文章比較長,所以在文章的開頭我打算簡單介紹1下這篇文章將要講述的內容,讀者可以選擇通篇細度,也能夠直接找到自己感興趣的部份。
既然是談 Cocoapods,那首先要弄明白它出現的背景。有經驗的開發者都知道 Cocoapods 在實際使用中,常常遇到各種問題,存在1定的使用本錢,因此衡量 Cocoapods 的本錢和收益就顯得很關鍵。
Cocoapods 的本質是1套自動化工具。那末了解自動化流程背后的原理就很重要,如果我們能手動的摹擬 Cocoapods 的流程,不管是對 Cocoapods 還是 Xcode 工程配置的學習都大有裨益。比如之前曾和同事研究過靜態庫嵌套的問題,很遺憾當時沒能解決,現在想來還是對相干知識理解還不夠到位。這1部份主要是介紹 Xcode 的工程配置,和 target/project/workspace 等名詞的概念。
最后,我會結合實際的例子,談談如何發布自己的 Pod,提供給他人使用。算是對 Cocoapods 的實踐總結。
由于實踐性的操作比較多,我為本文制作了1個 demo,提交在 我的 Github: CocoaPodsDemo 上,感興趣的讀者可以下載下來,研究1下提交歷史,或自己操作1遍。友誼提示: 本文所觸及的靜態庫均為摹擬器制作,請勿真機運行。
我們知道,再大的項目最初都是從 Xcode 提供的1個非常簡單的工程模板漸漸演變來的。在項目的演變進程中,為了實現新的功能,不斷有新的類被創建,新的代碼被添加。不過除自己添加代碼,我們也常常會直接把第3方的開源代碼導入到項目中,從而避免重復造輪子,節儉開發時間。
直接把代碼導入到項目中看起來很容易,但在實踐進程中,會遇到諸多問題。這些問題會困擾代碼的使用者,大大的增加了集成代碼的難度。
最直接的問題就是代碼的后續保護。假定代碼的發布者在未來的某1天更新了代碼,修復了1個重大 bug 或提供了新的功能,那末使用者就很難集成這些變動。
代碼有增有刪,如果把代碼編譯成靜態庫再提供給使用者, 就能夠省掉很多問題。但是如果這么做的話,就會遇到另外一個經典的問題: “Other linker flag”。
舉個例子來講,可以在 Demo 的 BSStaticLibraryOne
這個項目中看到,這個靜態庫1共有兩個類,其中1個是拓展 Extension。項目編譯后就會得到1個 .a
文件。
我們都知道靜態庫的格式可以是 .framework
,也能夠是 .a
。如果深究的話,.a
文件可以理解為1種歸檔文件,或說是緊縮文件。其中存儲的是經過編譯的 .o
格式的目標文件。我們可以通過 ar -x
命令來證明這1點:
ar -x libBSStaticLibraryOne.a
需要提示的1點是,光有 .a
文件還不夠,我們還需要提供頭文件給使用者導入。為了完成這1點,我們需要在項目的 Build Phases 中新增1個 Headers Phase,然后把需要對外暴露的頭文件放到 Public 1欄中:
此時編譯后的頭文件會放在 .a
文件所在目錄下,usr/local/include
目錄中。
接下來打開 OtherLinkerFlag
這個殼工程,引入 .a
文件和頭文件,運行程序,結果1定是:
-[BSStaticLibraryOne sayOtherThing]: unrecognized selector sent to instance xxx
這就是經典的 linker flag
問題。首先,我們知道 .a
實際上是編譯好的目標文件的集合,因此問題出在鏈接這1步,而非編譯。Objective-C 在使用靜態庫時,需要知道哪些文件需要鏈接進來,它根據的就是之前圖中所示的 __.SYMDEF SORTED
文件。
惋惜的是,這個文件不會包括所有的 .o
目標文件,而只是包括了定義了類的目標文件。我們可以履行 cat __.SYMDEF\ SORTED
來驗證1下,你會看到其中并沒有拓展類的信息。這樣1來,BSStaticLibraryOne+Extension.o
雖然存在,但是不被鏈接到終究的可履行文件中,從而致使了找不到方法的毛病。
解決上述問題的方法是調用者在 Build Settings
中找到 other linker flag
,并寫上 -ObjC
選項,這個選項會鏈接所有的目標文件。但是根據文檔描寫,如果靜態庫只有分類,而沒有類, 即便加了 -ObjC
選項也會報錯,應當使用 -force_load
參數。
由于第3方的代碼使用分類幾近是必定事件,因此幾近每一個使用者都要做如上配置,增加了復雜度和出錯的概率。
除此之外,第3方的代碼很有可能使用了系統的動態庫。因此使用者還必須手動引入這些動態庫(請記住這1點,靜態庫不支持遞歸援用,這是個很麻煩的事情,后面會介紹),我們以百度地圖 SDK 的集成為例,讀者可以自行對照手動導入和 Cocoapods 集成的步驟區分: 配置開發環境iOS SDK。
因此,我總結的使用 Cocoapods 的好處有以下幾個:
在我之前的1篇文章: 白話 Ruby 與 DSL 和在 iOS 開發中的應用 中簡單的介紹過,Cocoapods 是用 Ruby 開發的1套工具。每份代碼都是1個 Pod,安裝 Pod 時首先會分析庫的版本和依賴關系,這些都是在 Ruby 層面完成的,本文暫且不表。
我們首先假定已找到了要下載的代碼的地址(比如存在 Github 上),從這1步開始,接下來的工作都與 iOS 開發有關。
如果你手頭有1個 Cocoapods 項目,你應當會注意到以下幾個特點:
libPods.a
這個 Cocoapods 庫這樣做可以把引入第3方庫對主工程釀成的影響降到最低,不過沒法完全降為零。比如引入 Cocoapods 以后,項目不能不使用 xworkspace
來打開,后面會介紹緣由。
假定之前的 BSStaticLibraryOne
工程就是下載好的源碼,現在我們要做的就是把它集成到1個已有的工程,比如叫 ShellProject
中。
我們遇到的第1個問題是,在之前的 demo 中,需要把靜態庫和頭文件手動拖入到工程中。但這就和 Cocoapods 的效果不1致,畢竟我們希望主工程完全不受影響。
如果我們甚么都不做,固然不可能在殼工程中援用另外一個項目下的靜態庫和頭文件。但這個問題也能夠換個方式問:“Xcode 怎樣知道它們可以援用,還是不可以援用呢?”,答案在于 Build Settings 里面的 Search Paths 這1節。默許情況下,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 這1節中,找到 Public Headers Folder Path,將它的值從 usr/local/include 修改成 LibOne,然后重新編譯,這時候就會看到生成的頭文件位置產生了變化。
固然,這時候候還是沒法直接援用靜態庫的。由于我們只是告知 Xcode 可以去對應路徑去找,但并沒有明確聲明要用,所以需要在 Other Linker Flags 中添加1個選項: -l"BSStaticLibraryOne"
,引號中的內容就是靜態庫的工程名。
需要提示的是, 靜態庫編譯出來的 .a
文件會被手動加上 lib
前綴,在寫入到 Other Linker Flags 的時候千萬要注意去掉這個前綴,否則就會出現 Library not found 的毛病。
配置好以后的工程以下圖所示:
現在項目中沒有任何第3方的庫或代碼,仍然可以正常援用第3方的類并運行成功。
當我們的項目需要援用多個第3方庫的時候,就有兩種思路:
從直覺來看,第2種組織方式看上去更加集中,易于管理。斟酌后面我們還要解決庫的依賴問題,而且項目內的依賴處理比 workspace 中的依賴處理要容易很多(后面會介紹到),所以第2種組織方式更具有可行性。
如果讀者手頭有使用了 Cocoapods 的項目,可以看到它的文件組織結構以下:
- ShellProject(根目錄,殼工程)
- ShellProject (項目代碼)
- ShellProject.xcodeproj (項目文件)
- Pods (第3方庫的根目錄)
- Pods.xcodeproj (第3方庫的總工程)
- AFNetworking (某個第3方庫)
- Mantle (另外一個第3方庫)
- ……
而在我的 demo 中,為了偷懶,沒有把第3方庫放在殼工程目錄下,而是選擇和它平級。這其實沒有太大的區分,只是援用路徑不同而已,不用太關心。我們現在摹擬添加1個新的第3方庫,完成后的代碼結構以下:
- CocoaPodsDemo(根目錄)
- BSStaticLibraryOne (第3方庫總的文件夾,相當于 Pods,由于偷懶,名字就不改了)
- BSStaticLibraryOne (第1個第3方庫)
- BSStaticLibraryTwo (新增1個第3方庫)
- BSStaticLibraryOne.xcodeproj (第3方庫的項目文件)
- Build/Products/Debug-iphonesimulator (編譯結果的目錄)
- ShellProject (殼工程)
首先要新建1個文件夾 BSStaticLibraryTwo 并拖入到項目中,然后新增1個 Target(以下圖所示)。
在 Xcode 工程中,我們都接觸過 Project。打開 .xcodeproj 文件就是打開1個項目(Project)。Project 負責的是項目代碼管理。1個 Project 可以有多個 Target,這些 target 可使用不同的文件,最后也就能夠得出不同的編譯產物。
通過使用多個 target,我們可以用少量不同的代碼得到不同的 app,從而避免了開多個工程的必要。不過我們這里的幾個 target 其實不含有相同代碼,而是1個第3方庫對應1個 target。
接下來我們新建1個類,記得要加入到 BSStaticLibraryTwo 這個 target 下,記得和之前1樣修改 Public Headers Folder Path 并添加1個 Build Phase。
在左上角將 Scheme 選擇為 BSStaticLibraryTwo 再編譯,可以看到新的靜態庫已生成了。
對主工程來講,必須在子工程(第3方庫)編譯完后才開始編譯,或換句話說,我們在主工程中按下 Command + R/B 時,所有子工程必須先被編譯。對這類跨工程的庫依賴,我們沒法直接指明依賴關系,必須隱式的設置依賴關系,我們還是以 Cocoapods 工程舉例:
主工程中用到了 libPod.a 這個靜態庫,而且它其實不是在主工程中生成,而是在 Pods 這個項目中編譯生成。1旦存在這類援用關系,那末也就建立了隱式的依賴關系。在編譯主工程時,Xcode 會確保它援用的所有靜態庫都先被編譯。
之前我們討論過兩種管理多個靜態庫的方法,如果選擇第1種方法, 每一個靜態庫對應1個 Xcode 項目,雖然不是不可以,但主工程看上去就就會比較復雜,這主要是跨項目依賴致使的。
而在項目內部管理 target 的依賴相對而言就簡單很多了。我們只要新建1個總的 target,無妨也叫作 Pod。它甚么也不做,只需要依賴另外兩個靜態庫就能夠了,設置 Target Dependencies:
此時選擇 Pod 這個 target 編譯,另外兩個靜態庫也會被編譯。因此接下來的任務就是讓主工程直接依賴于 Pod 這個 target,自然也就間接依賴于真正有用的各個第3方靜態庫了。
接下來我們重復之前的步驟,設置好頭文件和靜態庫的搜索路徑,并在 Other Linker Flags 里面添加: -l"BSStaticLibraryTwo"
,就能夠使用第2個靜態庫了。
到目前為止,我們摹擬了多個靜態庫的組織,和如何在主工程中援用他們。不過還存在1些小瑕疵,我截了 Xcode 中的1幅圖:
從圖中可以很明顯的發現: 第3方庫中的代碼被認為是系統代碼,色彩為藍色。而正常的自定義方法應當綠色,會對開發者造成困擾。
除這個小瑕疵之外,在之前談到的跨項目依賴中,1個項目不單單需要援用另外一個項目的產物,還有1個先決條件: 把這兩個項目放入同1個 Workspace 中。Workspace 的作用是組織多個 Project,使得各個 Project 直接可以有援用依賴關系,同時也能讓 Xcode 辨認出各個 Project 中的代碼和頭文件。
按住 Command + Control + N 可以新建1個 Workspace:
完成以后就會看到1個完全空白的項目,在左邊按下右鍵,選擇 Add Files to:
然后選中靜態庫項目和主工程的 .xcodeproj 文件,把這兩個工程都加進來:
需要提示的是,切換到 Workspace 以后, Xcode 會把 Workspace 所在目錄當作項目根目錄,因此靜態庫的編譯結果會放在 /CocoaPodsDemo/Build/Products/…,而不再是之前的 /CocoaPodsDemo/BSStaticLibraryOne/Build/Products/…,因此需要手動對主工程中的搜索路徑做1下調劑。
做好上述改動后,即便我們刪除掉 BSStaticLibraryOne 這個項目的編譯結果,只在 Workspace 中編譯主項目,Xcode 也會自動為我們編譯被依賴的靜態庫。這就是為何我們只需要履行 pod install
下載好代碼,就能夠不用做別的操作,直接在主項目中運行。
固然,代碼色彩毛病的小問題也在 Workspace 恢復正常了。
到這里,基本上關于 Cocoapods 的工作原理就算是分析完了。上述操作除文件增加,基本上都是修改 .pbxproj 文件。所有的 Xcode 都會在該文件中得到反應,同理,只要修改該文件,也能到達上述手動操作的效果。而 Cocoapods 開發了1套 Ruby 工具,用來封裝這些修改,從而實現了自動化。
文章開頭,我們提到作為代碼提供者,如果自己的代碼還援用別的第3方庫,那末提供代碼會變得很麻煩,這主要是由于靜態庫不會遞歸援用致使的。我們已知道靜態庫其實就是1堆編譯好的目標文件(.o 文件)的打包情勢,它需要配合頭文件來使用。所謂的不會遞歸援用是指,假定項目 A 援用了靜態庫 B(或是動態庫,也是1樣),那末 A 編譯后得到的靜態庫中,其實不含有靜態庫 B 的目標文件。如果有人拿到這樣的靜態庫 A,就必須補齊靜態庫 B,否則就會遇到 “Undefined symbol” 毛病。
如果我們提供的代碼援用了系統的動態庫,問題還比較簡單,只要在文檔里面注明,讓使用者自己導入便可。但如果是第3方代碼,那末這簡直是1起災害。即便使用者找到了提供者使用的靜態庫,那個靜態庫也很有可能已進行了升級,而版本不1致的靜態庫可能具有完全不同的 API。也就是說代碼提供者還要在文檔中注明使用的靜態庫的版本,然后由使用者去找到這個版本。我想,這才是 Cocoapods 真正致力于解決的任務。
CocoaPods 的做法比較簡單,由于他有1套統1的版本表示規則,也能夠自動分析依賴關系,而且每一個版本的代碼都有記錄。后面會介紹 Cocoapods 的相干實踐,這里我們先思考1下如何手動解決靜態庫嵌套的問題。
既然靜態庫只是目標文件的打包情勢,那末我只需要找到被嵌套的靜態庫,拿到其中的目標文件,然后和外層的靜態庫放在1起重新打包便可。這個進程比較簡單, 我也就沒有做 demo,用代碼應當就能夠說明得很清楚。假定我們有靜態庫 A.a 和 B.a,其中 A 需要援用 B,現在我希望對外發布 A,并且集成 B:
lipo A.a -thin x86_64 output A_64.a # 如果是多 CPU 架構,先提取出某1種架構下的 .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 文件1起打包到 Together.a 中
這時候候 Together.a 文件就能夠當作完全版的靜態庫 A 給他人使用了。
本來 Cocoapods 的使用就比較簡單。特別是了解完原理后,使用起來應當更加得心應手了,對1些常見的毛病也有了分析能力。不過有個小細節還是需要注意1下:
關于 Cocoapods 文件是不是要加入版本控制并沒有明確的答案。我之前的習慣是不加入版本控制。由于這樣會讓提交歷史明顯變得復雜,如果不同分支上使用的不同版本的 pod,在合并分支時就會出現大量沖突。
但是官方的推薦是把它加入到版本控制中去。這樣他人不再需要履行 pod install
,而且能夠確保所有人的代碼1定1致。
但是雖然不強迫把全部 Pod 都加入版本控制,但是 Podfile.lock 不管如何必須添加到版本控制系統中。為了解釋這個問題,我們先來看看 Cocoapods 可能存在的問題。
假定我們在 Podfile 中寫上: pod 'AFNetWorking'
,那末默許是安裝 AFNetworking 的最新代碼。這就致使用戶 A 可能裝的是 3.0 版本,而用戶 B 再安裝就變成了 4.0 版本。即便我們在 Podfile 中指定了庫的具體版本,那也不能保證不出問題。由于1個第3方庫還有可能依賴其他的第3方庫,而且不保證它的依賴關系是具體到版本號的。
因此 Podfile.lock 存在的意義是將某1次 pod install
時使用的各個庫的版本,和這個庫依賴的其他第3方庫的版本記錄下來,以供他人使用。這樣1來,pod install
的流程實際上是:
而另外一個經常使用命令 pod update
其實不是1個平常更新命令。它的原理是疏忽 Podfile.lock 文件,完全使用 Podfile 中的配置,并且更新 Podfile.lock。1旦決定使用 pod update
,就必須所有團隊成員1起更新。因此在使用 update
前請務必了解其背后產生的事情和對團隊釀成的影響,并且確保有必要這么做。
很多教程都有介紹開源 Pod 的流程,我在實踐的時候主要參考了以下兩篇文章。相對來講比較詳細,條理清晰,也推薦給大家:
如果要創建公司內部的私有庫,首先要建立1個自己的倉庫,這個倉庫在本地也會有存儲:
如圖中所示,master 是官方倉庫,而 baidu 則是我用來測試的私有倉庫。倉庫中會存有所有 Pod 的信息,每一個文件夾下都依照版本號做了辨別,每一個版本對應1個 podspec 文件。從圖中可以看到,cocoapods 會緩存所有的 podspec 到本地,但不會緩存每一個 Pod 的具體代碼。每當我們履行 pod install
時,都會先從本地查找 podspec 緩存是不是存在,如果不存在則會去中央倉庫下載。
我們常常遇到的 pod install
很慢就是由于默許情況下會更新全部 master。此時 master 不單單存儲著本地使用 Pod 的 PodSpec 文件,而是存儲了所有的已有的 Pod。所以這個更新進程看起來異常緩慢。有些解決方案是使用:
pod install --verbose --no-repo-update
這實際上是治標不治本的遷就醫治方法,由于本地的倉庫早晚要被更新,否則就拿不到最新的 PodSpec。要想完全解決這1問題,除定期更新外,還可以選擇其他速度較快的鏡像倉庫。
podspec 文件是我們開源 Pod 時需要填寫的文件,主要是描寫了 Pod 的基礎信息。除1些無關緊要的配置和介紹信息外,最重要的填寫 source_files 和 dependency。前者用來規定哪些文件會對外公布,后者則指定此 Pod 依賴于哪些其他 Pod。比如在上圖中,我的 PrivatePod 就依賴于 CorePod,在公司內部的項目中使用 PodS 依賴可以大量簡化代碼的集成流程。1個典型的 PodSpec 可能長這樣:
填寫好上述信息后,我們只要先 lint 1下 podspec,確保格式無誤,就能夠提交了。