理解Cocoapods

山河永寂發表於2016-05-01

對於做 iOS 開發的朋友來說,Cocoapods 是一件不必可少的得利工具,它是一個管理第三方庫,並且解決其依賴關係的工具,但是有很多朋友對其運作的機制知其然卻不知其所以然。筆者就在這裡簡單的講解一下。

新石器時代

對於任何一項程式設計來說,早期未形成工程化的時候,是不存在所謂依賴管理的概念的,iOS 也是一樣,早期 iOS 開發如果想要使用第三方庫,是非常繁瑣的一件事情。

  1. 將第三方庫原始碼複製到工程目錄,或者使用 git submodule 將其作為專案的一個模組

  2. 設定工程檔案,新增第三方庫依賴的系統庫

  3. 對於某些第三方庫,可能需要設定一些編譯選項( etc. -fno-objc-arc

  4. 最後就是管理第三方庫程式碼的更新

這些都是體力活,為了能省去這些工作,有沒有辦法呢?
答案是有的,iOS 開發終究還是很傳統的 Unix 開發,直接把第三方庫原始碼抽離出來,打包成靜態庫就行了(PS:在 iOS 8 之前蘋果只允許使用靜態庫,而 iOS 8 後就可以打包動態庫了,當然,實際上是一樣的。)。這樣的話就不需要擔心過多的原始碼匯入的繁瑣,也不需要擔心第三方庫究竟需不需要編譯選項,而且第三方庫更新只需要更新靜態庫就行了。簡直是美好的人生。

農耕時代

封裝成靜態庫或動態庫看起來確實美好了,方便快捷,這也是 Unix 下的解決方案。但是隻要仔細一想,問題一大堆。

  1. 某些第三方庫很可能直接依賴另一項第三方庫,依賴如果使用手工解決將會是很大的工作。

  2. 動態庫不能用於 iOS 7 及以下版本

  3. 工程依舊需要設定依賴的系統庫

  4. 仍然需要手工更新第三方庫版本

對於 Unix 環境來說,動態庫可以存放在系統預設搜尋路徑下,這樣所有的應用都可以共享同一個記憶體副本,而且升級動態庫的時候可以一起升級,不需要到各處尋找。但是 iOS 的動態庫實際上是縮水的,因為蘋果將動態庫限制在了沙盒內部,其他 App 完全不能訪問此動態庫。這就限制了動態庫的優點。還有一點原因就是指令集架構的不同,iOS 模擬器使用的是 x86 架構指令集,而真機則是 ARM64 等指令集,如果想要方便使用,最好需要打包通用架構的靜態庫,但是這是很繁瑣的工作。
由於以上原因,手工封裝靜態庫或者動態庫實際上在專案小的時候是可以的,但是專案規模一旦擴大就會導致效率低下。

工業時代

為了能夠自動化流水作業,就引出了依賴包管理的概念,也就是 Cocoapods。Cocoapods 本質上還是上面所說的封裝動態庫靜態庫,但是它解決的最大問題就是依賴管理。開發者不需要從 Github 的地方辛苦的尋找程式碼,只需要一條命令,就能下載整合第三方庫。這些優點都不必說,筆者下面就具體講解 Cocoapods 到底對我們的工程做了什麼事情。
首先使用 Xcode 建立了一個工程,使用 git init 命令將其納入版本控制,做第一次提交。然後寫 Podfile,直接跟隨 Cocoapods 官網文件寫最簡單的內容。

使用 framework

platform :ios, `8.0`
use_frameworks!

target `test` do
  pod `AFNetworking`, `~> 2.6`
end

然後輸入以下命令整合第三方庫。

> pod install

現在可以看一下修改的內容。

> git status

On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   test.xcodeproj/project.pbxproj

Untracked files:
  (use "git add <file>..." to include in what will be committed)

    Podfile.lock
    Pods/
    test.xcworkspace/

由於輸出過場,這裡就不貼 git diff 命令的輸出了,從 git diff 命令輸出可以看出,主要修改了 .xcodeproj/project.pbxproj 檔案的內容,並且將原先的檔案格式轉化為了 XML 檔案格式。並且,可以看出來,新增了一個 工程名.xcworkspace 的工作空間,將原先的工程檔案和新的 Pods 工程放到了同一個工作空間下。

<?xml version="1.0" encoding="UTF-8"?>
<Workspace
   version = "1.0">
   <FileRef
      location = "group:test.xcodeproj">
   </FileRef>
   <FileRef
      location = "group:Pods/Pods.xcodeproj">
   </FileRef>
</Workspace>

很顯然,所有的配置都被放到了工程檔案中,工作空間實際上只是一個工程的整合。
由於檔案格式不同,無法很好的對比反映工程檔案到底哪裡被修改了,但是從實際的工程開啟後觀察,也能稍微瞭解。總結如下:

  1. 在 General -> Linked Frameworks and Libraries 中新增了 Pods_test.framework 的依賴

  2. Build Settings -> Other Linker Flags 中新增了 -framework "AFNetworking"

  3. Build Settings -> RunPath Search Paths 中新增了 @executable_path/Frameworks

  4. Build Settings -> Other C Flags 新增 -iquote "$CONFIGURATION_BUILD_DIR/AFNetworking.framework/Headers" 和 Other C++ Flags 新增 $(OTHER_CFLAGS),也就是說,跟隨 Other C Flags 配置

  5. Build Settings -> Preprocessor Macros 新增 COCOAPODS=1

  6. Build Settings -> User-Defined -> MTL_ENABLE_DEBUG_INFO PODS_FRAMEWORK_BUILD_PATH PODS_ROOT 三個變數

  7. Build Phases -> Embed Pods Frameworks 新增 "${SRCROOT}/Pods/Target Support Files/Pods-test/Pods-test-frameworks.sh"

  8. Build Phases -> Copy Pods Resources 新增 "${SRCROOT}/Pods/Target Support Files/Pods-test/Pods-test-resources.sh"

從上面可以看出,如果在 Podfile 中指定了 use_frameworks! 則會使用動態庫封裝其內容,而上面所做的主要工作就是新增對另一個工程的依賴,新增連結器選項使其連結 framework,將動態庫執行時搜尋路徑設定為 @executable_path/Frameworks 也就是說 ${BUNDLE_ROOT}/Frameworks,設定 C 和 C++ 編譯器的標頭檔案搜尋路徑,將 framework 放置到沙盒目錄
然後再來看 Pods 工程,Pods 工程將每個第三方庫都使用一個 target 表示,還有一個 target 則是用於生成主要的庫檔案,需要注意的是,雖然第三方庫的 target 生成的是動態庫,但是最終的 target 生成的是靜態庫,但是隻是起一個集合的作用,實際的二進位制程式碼還是被存放在各個動態庫中,其主要修改了 Build Settings 內容

  1. 將 framework 的輸出路徑指定為 ${SRCROOT}/../build

  2. 指定 iOS Deployment Target 為原先 Podfile 中指定的版本,這是一個容易被人忽視的地方,很多朋友都以為 Podfile 中 platform 指令指定的是主工程的 deployment target,實際上是指定 Pods 工程的每個 target

主要就是這兩點,其他都是一些細碎的細節,比如將輸出型別指定為動態庫,設定第三方庫依賴。
然後就是 Pods-工程名-frameworks.shPods-工程名-resources.sh,這兩個檔案實際上就是將封裝通用架構動態庫檔案和將動態庫複製到 bundle 指定目錄下。有興趣的朋友可以仔細看看。

不使用 framework

framework 只能在 iOS 8 以上平臺使用,但是大多數情況下,工程都是需要相容 iOS 7 版本的,所以 framework 功能只能捨棄掉,因為目前相容版本都是以 UI 特徵來劃分相容版本,iOS 6 及以前版本都算擬物化 UI,而 iOS 7 及以後版本則是扁平化風格。
這裡先將 Podfile 中的 use_frameworks! 刪除掉,再 pod install
通過 git diff 命令可以看到,實際上兩種方式差別不大,只是不使用動態庫而是使用靜態庫替代了所有的動態庫,然後在 Other Linker Flags 中新增了 -ObjC和連結器命令連結所依賴的系統 framework,原先則是在 Pods 工程具體的 target 中才存在連結器命令連結系統 framework。而且還發現,在 Header Search Path 中新增了標頭檔案的搜尋路徑。

後記

Cocoapods 雖然侵入性很強,但是對於新手來說,遮蔽了很多底層的操作,非常便於實踐,對於重複的工作也得到了很好的替代,如果有朋友想要補充,可以在下方留言指正。