聊聊 Xcode 專案檔案中的 project.pbxproj

發表於2016-10-09
project.pbxproj 檔案被包含於 Xcode 工程檔案 *.xcodeproj 之中,儲存著 Xcode 工程的各項配置引數。它本質上是一種舊風格的 Property List 檔案,歷史可追溯到 NeXT 的 OpenStep。其可讀性不如 xml 和 json,蘋果卻一直沿用至今,作為一家以創新聞名的公司可能這裡剩下的就是情懷吧。

本文談了下 project.pbxproj 的知識,並總結了一些操作工程檔案的優秀輪子,並在最後給出了自己的解決方案 pbxprojHelper

Property List 的歷史

想了解 project.pbxproj 檔案格式,就需要先了解 Property List。

Property List 有很多種表現方式,最古老的格式就是之前提到的 NeXTSTEP 所使用的格式。那時還算是可讀性很強的,仍需要手動編輯。與 json 最明顯的差別是:陣列用小括號括起來並用逗號隔開元素;字典用大括號括起來並用分號隔開鍵值對,鍵值之間用等號連線;二進位制資料用尖括號 括起來:

陣列:

字典:

這也是 project.pbxproj 檔案中所使用的格式。

後來出現的 GNUstep 沿用了 NeXTSTEP 格式,並新增了對 NSValueNSDate 物件的支援。到了蘋果的 Mac OS X 10.0 推出了新的 XML 格式,舊的 NeXTSTEP 被廢棄,只支援讀不支援寫。這也是為什麼使用 plutil 命令或者 Cocoa 的 NSPropertyListSerialization 寫入 OpenStep 格式時會報錯:Property list format kCFProperty ListOpenStepFormat not supported for writing

因為 XML 語法囉嗦很佔空間,蘋果在 Mac OS X 10.2 又推出了一種新格式,將 Property List 儲存於二進位制檔案中。雖然在 Mac OS X 10.7 JSON 格式出現了,但是跟 Property List 不相容。

於是乎 Property List 在蘋果家族的歷史上存在三種格式:OpenStep,XML 和 Binary。除了 OpenStep 被廢棄不支援寫入以外,其餘格式都提供 API 支援讀寫。

操作 Property List 的途徑

Unix 的 plutil 工具提供了處理 Property list 檔案的能力。 比如將 Property list 檔案轉成 XML 格式:

-convert 選項可以傳入的引數有: xml1, binary1 和 json。

當然 Cocoa 的 NSPropertyListSerialization 也提供了類似的功能,更物件導向。其實 plutilNSPropertyListSerialization 底層都是呼叫 CoreFoundationCFPropertyList 相關的 API,所以功能類似。

使用 NSPropertyListSerialization 讀入 project.pbxproj 檔案時,字典中鍵值對的順序會跟檔案中原始的順序不一致。這是因為字典為了實現快速查詢會將 key 按序儲存(比如字典序或用紅黑樹排序)。用 plutil 命令將 project.pbxproj 檔案轉成 xml 或 json 也會如此。

此外,plutil 命令也支援對某個 keypath 的增、刪、改操作。NSPropertyListSerialization 就更不用說了,在程式中隨意搞。

之前提到過不支援 OpenStep 寫入的問題,所以即便我們能在記憶體中操作 project.pbxproj 檔案,依然不能直接儲存。如果自己動手寫一個 OpenStep 格式生成程式,依然無法準確還原字典中鍵值對的順序。更何況 project.pbxproj 檔案中還插入了大量增強 human-readable 的註釋,這些註釋的生成是有特殊邏輯的,這個在後面會講。

簡要解析 project.pbxproj 檔案

既然表面上無法將修改過的工程檔案資料還原為 OpenStep 格式,Xcode 又是如何『開掛』做到的呢?這就得從 project.pbxproj 檔案內容說起了。

內容規則

project.pbxproj 使用 UUID 作為交叉引用的索引,保證每個配置資訊物件的唯一性。因為 UUID 根據機器硬體和時間戳生成,避免了多人在同一時間段操作修改工程檔案帶來的問題。也就是說工程中每項配置物件都有個唯一的 UUID,然後其他配置物件想引用某個配置物件直接使用它的 UUID 即可。這就跟我們程式設計時使用指標指向某個物件的地址一樣,其他物件的屬性想引用它,只需要給屬性傳個指標地址就行了。

可以把整個檔案的內容想象成一個字典,字典中的 Key 按照字典序來排列。字典的第一層級總共有 5 個鍵值對,Key 分別為:archiveVersionclassesobjectVersionobjectsrootObject。其中重要的 Key 是 objectsrootObject

所有的配置物件都放在 objects 對應的 Value 中,包括跟物件(rootObject)。 objects 對應的 Value 也是一個字典,Key 都為 UUID,Value 依然是個字典。可以將 rootObject 的值(是一個 UUID)作為 Key 在 objects 對應的字典中找到根物件。這個根物件的 isa 屬性為 PBXProjectisa = PBXProject)。讀懂 project.pbxproj 的最好方式就是順著 rootObject 的各個屬性對應的 UUID 在 objects 中找到對應的物件,然後一層層看下去。這樣整個檔案的配置資訊存放方式就慢慢摸清了。

objects 中的鍵值對被分成了若干個 section,雖然 section 的順序是 Xcode 私有 API 欽定的,但每個 section 內部的鍵值對會根據 Key 的字典序排列。

每個物件內部的屬性(也是鍵值對)會把 isa 排在最前面,其餘的按照字典序排列。

陣列內部的順序完全按照元素內容的字典序排列。

下面是 objectsPBXNativeTarget section 的一個物件,感受一下格式:

可以根據 A45018751D9D68D60002869D 找到對應的 buildConfigurationList 物件的內容,所以說 project.pbxproj 使用 UUID 作為交叉引用的索引。通過這種關係,可以遞迴構建一張有向圖,每個物件都是一個節點。

內容型別

在 Xcode 中能看見所有的公共配置資訊都存在於 project.pbxproj 中。主要包含跟檔案相關的 BuildFile,Group 和 FileReference;跟編譯相關的 BuildPhase 和 Build Configuration(List);以及一些列 Target 和 TargetDependency。

objects 的鍵值對根據內容型別被分成了若干個 section,採用註釋的方式分節也使得可讀性更強。section 的數量跟工程有關,尤其是每個工程的 BuildPhase 和 Target 差別都很大。下面列出了一個section 列表(非完整):

每個 section 中的物件型別都是相同的,物件的型別是靠 isa 的值區分的。物件內部的屬性型別以及含義可以參照這篇文章提供的對照表:Xcode Project File Format

操作 project.pbxproj 檔案

我收集了一些可以操作 project.pbxproj 檔案的優秀輪子,原理大都是用 plutil 轉成 json 或 xml 後進行處理,不僅功能非常侷限,且都無法完美還原為 OpenStep 格式的內容:

  • Xcodeproj CocoaPods 寫的 Ruby 解析庫,用於修改引入 CocoaPods 的工程檔案並儲存為 XML 格式。CocoaPods 本身是很強大的,還可以用來操作 Xcode workspaces (.xcworkspace), configuration files (.xcconfig) 和 Xcode Scheme files (.xcscheme).
  • mod-pbxproj 強大的 Python 解析庫,支援一定的修改操作,可輸出 OpenStep 格式,但是順序和註釋內容無法完美還原,有些雞肋。
  • xUnique 用 Python 寫的統一多裝置生成的 UUID 的工具,主要用途是統一工程在多裝置上生成的 UUID,避免工程檔案衝突。
  • pbxplorer Ruby 寫的解析庫。
  • node-xcode Cordova 基於它管理 Xcode 工程

不過 Xcode 可以開啟 XML 格式的 project.pbxproj,一旦在 Xcode 介面上修改工程配置就會重新將 project.pbxproj 轉成 OpenStep 風格。解鈴還須繫鈴人,經過多番對比之後發現最終還是 Xcode 自己才能將 XML 完美還原成原來的 OpenStep 格式,且 diff 對比毫無差錯。原因很簡單,Xcode 使用的私有 API 的匯出結果是個黑盒,外界無論怎麼猜都會有瑕疵。所以還是匯出為 XML 後手動在 Xcode 介面中觸發下吧。既然這樣的話,如果能夠簡單高效地生成出 XML 檔案作為工程檔案就好了。基於此想法我開發了一款叫做 pbxprojHelper 的 Mac App:

11mainwindow2x

操作簡單粗暴:

  1. 選擇一個工程檔案然後內容會自動解析在下面的 Outline 列表中,Filter 輸入框便於過濾檢視內容。
  2. 單擊 Outline 列表中的文字即可複製內容到剪貼簿,雙擊複製整個keypath!
  3. 對 project.pbxproj 檔案的增刪改操作都配置在 json 檔案中,每次想對工程進行修改只需選擇對應的 json 配置檔案然後點選 “Apply” 即可完成寫入替換哦!
  4. 不小心誤操作的話還可以點 “Revert” 回滾到上個版本哦!
  5. 什麼?懶得寫 json 配置檔案?下面這個附帶的 json 配置生成器可以幫你直接生成一個哦!使用 ⇧⌘0 快捷鍵即可召喚此神器!選擇兩個工程檔案和 json 儲存路徑後輕輕一點 “Generate” 就搞定咯:

12generatorwindow2x

所以處理工程檔案的正確姿勢是:

  1. 拷貝出一份原始的 project.pbxproj 檔案
  2. 在 Xcode 介面上修改工程配置,比如修改編譯選項,使用自己的證照等
  3. 使用 pbxprojHelper 的 JSON Configuration Generator 來對比修改後的工程檔案和原始的工程檔案,自動生成 JSON 配置檔案
  4. 以後想要在工程檔案上施加自己的修改時,只需要應用之前生成好的 JSON 配置檔案即可

pbxprojHelper 的優勢在於可以自由地增刪改查任意屬性,原生 UI 降低了使用門檻。功能強大的同時人性化的設計使得更快捷瀏覽工程檔案中的內容。無需寫任何程式碼即可一鍵配置自己想要的工程檔案

此外還提供了命令列工具 pbxproj, 它具有 pbxprojHelper.app 具有的大部分功能:

可以使用 pbxproj 搭配 DevToolsCore 私有 framework 來完成修改工程檔案並轉化成 OpenStep 格式的一條龍自動化程式。

你可以在 GitHub 上下載最新的 Release 版。或者在 App Store 中下載:https://itunes.apple.com/cn/app/pbxprojhelper/id1160801848?mt=12

本專案完全手擼,沒依賴上面提到的任何輪子?。但由於使用 Swift 3 來開發,所以最低只能支援 macOS 10.12 系統。

想了解更多資訊請檢視 GitHub 主頁:https://github.com/yulingtianxia/pbxprojHelper

Reference

https://en.wikipedia.org/wiki/Property_list
http://www.monobjc.net/xcode-project-file-format.html
http://stackoverflow.com/questions/1452707/library-to-read-write-pbxproj-xcodeproj-files
https://github.com/CocoaPods/Xcodeproj/issues/52

相關文章