在上文 CocoaPods 命令解析 中,咱們經過對 CLAide 的源碼分析,瞭解了 CocoaPods 是如何處理 pod
命令,多級命令又是如何組織和嵌套的,並解釋了命令行輸出所表明的含義。今天咱們開始學習 Podfile
。html
大多 iOS 工程師最早接觸到的 CocoaPods 概念應該是 Podfile
,而 Podfile
屬於 cocoapods-core
(如下簡稱 Core) 的兩大概念之一。另一個則是 Podspec
(用於描述 Pod Library 的配置文件),只有當你須要開發 Pod 組件的時候纔會接觸。ios
在介紹 Podfile 的內容結構以前,必需要談談 Xcode 的工程結構。git
咱們先來看一個極簡 Podfile 聲明:github
target 'Demo' do
pod 'Alamofire', :path => './Alamofire'
end
複製代碼
它編譯後的工程目錄以下:算法
如你所見 Podfile 的配置是圍繞 Xcode 的這些工程結構:Workspace、Project、Target 及 Build Setting 來展開的。 做爲包管理工具 CocoaPods 將所管理的 Pods 依賴庫組裝成一個個 Target,統一放入 Pods.project
中的 Demo target
,並自動配置好 Target 間的依賴關係。swift
以後將 Example.project
主工程和 Pods.project
工程一塊兒打包到新建的 Example.workspace
,配好主工程與 Pods
工程之間的依賴,完成最終轉換。vim
接下來,咱們來聊一聊這些 Xcode 結構:數組
A target specifies a product to build and contains the instructions for building the product from a set of files in a project or workspace.xcode
首先是 Target,它做爲工程中最小的可編譯單元,根據 Build Phases 和 Build Settings 將源碼做爲輸入,經編譯後輸出結果產物。 其輸出結果能夠是連接庫、可執行文件或者資源包等,具體細節以下:ruby
An Xcode project is a repository for all the files, resources, and information required to build one or more software products.
Project 就是一個獨立的 Xcode 工程,做爲一個或多個 Targets 的資源管理器,自己沒法被編譯。 Project 所管理的資源都來自它所包含的 Targets。特色以下:
下圖爲 Project 與所包含對 Targets 的關係:
A workspace is an Xcode document that groups projects
做爲純粹的項目容器,Workspace 不參與任何編譯連接過程,僅用於管理同層級的 Project,其特色:
workspace build directory
;An Xcode scheme defines a collection of targets to build, a configuration to use when building, and a collection of tests to execute.
Scheme 是對於整個 Build 過程的一個抽象,它描述了 Xcode 應該使用哪一種 Build Configurations 、執行什麼任務、環境參數等來構建咱們所需的 Target。
Scheme 中預設了六個主要過程: Build、Run、Test、Profile、Analyze、Archive。包括了咱們對 Target 的全部操做,每個過程均可以單獨配置。
The CocoaPods-Core gem provides support to work with the models of CocoaPods, for example the Podspecs or the Podfile.
CocoaPods-Core 用於 CocoaPods 中配置文件的解析,包括 Podfile
、Podspec
以及解析後的依賴鎖存文件,如 Podfile.lock 等。
照例,咱們先經過入口文件 lib/cocoapods-core.rb
來一窺 Core 項目的主要文件:
module Pod
require 'cocoapods-core/gem_version'
class PlainInformative < StandardError; end
class Informative < PlainInformative; end
require 'pathname'
require 'cocoapods-core/vendor'
# 用於存儲 PodSpec 中的版本號
autoload :Version, 'cocoapods-core/version'
# pod 的版本限制
autoload :Requirement, 'cocoapods-core/requirement'
# 配置 Podfile 或 PodSpec 中的 pod 依賴
autoload :Dependency, 'cocoapods-core/dependency'
# 獲取 Github 倉庫信息
autoload :GitHub, 'cocoapods-core/github'
# 處理 HTTP 請求
autoload :HTTP, 'cocoapods-core/http'
# 記錄最終 pod 的依賴信息
autoload :Lockfile, 'cocoapods-core/lockfile'
# 記錄 SDK 的名稱和 target 版本
autoload :Platform, 'cocoapods-core/platform'
# 對應 Podfile 文件的 class
autoload :Podfile, 'cocoapods-core/podfile'
# 管理 PodSpec 的集合
autoload :Source, 'cocoapods-core/source'
# 管理基於 CDN 來源的 PodSpec 集合
autoload :CDNSource, 'cocoapods-core/cdn_source'
# 管理基於 Trunk 來源的 PodSpec 集合
autoload :TrunkSource, 'cocoapods-core/trunk_source'
# 對應 PodSpec 文件的 class
autoload :Specification, 'cocoapods-core/specification'
# 將 pod 信息轉爲 .yml 文件,用於 lockfile 的序列化
autoload :YAMLHelper, 'cocoapods-core/yaml_helper'
# 記錄 pod 依賴類型,是靜態庫/動態庫
autoload :BuildType, 'cocoapods-core/build_type'
...
Spec = Specification
end
複製代碼
將這些 Model 類按照對應的依賴關係進行劃分,層級以下:
先來了解 Podfile 的主要數據結構
The Specification provides a DSL to describe a Pod. A pod is defined as a library originating from a source. A specification can support detailed attributes for modules of code through subspecs.
Specification 即存儲 PodSpec
的內容,是用於描述一個 Pod 庫的源代碼和資源將如何被打包編譯成連接庫或 framework,後續將會介紹更多的細節。
The TargetDefinition stores the information of a CocoaPods static library. The target definition can be linked with one or more targets of the user project.
TargetDefinition
是一個多叉樹結構,每一個節點記錄着 Podfile
中定義的 Pod 的 Source 來源、Build Setting、Pod 子依賴等。該樹的根節點指向 Podfile
,而 Podfile
中的 root_target_definitions
則記錄着全部的 TargetDefinition
的根節點,正常狀況下該 list 中只有一個 root 即 Pods.project
。
爲了便於閱讀,簡化了大量的 DSL 配置相關的方法和屬性並對代碼順序作了調整,大體結構以下:
module Pod
class Podfile
class TargetDefinition
# 父節點: TargetDefinition 或者 Podfile
attr_reader :parent
# 子節點: TargetDefinition
attr_reader :children
# 記錄 tareget 的配置信息
attr_accessor :internal_hash
def root?
parent.is_a?(Podfile) || parent.nil?
end
def root
if root?
self
else
parent.root
end
end
def podfile
root.parent
end
# ...
end
end
複製代碼
對應上一節 Xcode 工程結構中的 Podfile
關係以下:
CocoaPods 正是巧妙利用了 Xcode 工程結構的特色,引入 Pods.project
這一中間層,將主工程的 Pods 依賴所有轉接到 Pods.project
上,最後再將 Pods.project
做爲主項目的依賴。儘管這麼作也受到了一些質疑和詬病(所謂的侵入性太強),但筆者的觀點是,正得益於 Pods.project
這一設計隔絕了第三方依賴庫對於主項目的頻繁更改,也便於後續的管理和更新,體現了軟件工程中的開放-關閉原則。
好比,在 Pod 1.7.0 版本中支持的 Multiple Xcodeproj Generation 就是解決隨着項目的迭代而日益增大的 Pods.project
的問題。試想當你的項目中存在上百個依賴庫,每一個依賴庫的變動都會影響到你的主工程,這將是很是可怕的事情。
The Podfile is a specification that describes the dependencies of the targets of one or more Xcode projects.
Podfile
是用於描述一個或多個 Xcode Project 中各個 Targets 之間的依賴關係。
這些 Targets 的依賴關係對應的就是 TargetDefinition
樹中的各子節點的層級關係。如前面所說,有了 Podfile
這個根節點的指向,僅需對依賴樹進行遍歷,就能輕鬆獲取完整的依賴關係。
有了這層依賴樹,對於某個 Pod
庫的更新便是對樹節點的更新,即可輕鬆的分析出這次更新涉及的影響。
簡化調整後的 Podfile 代碼以下:
require 'cocoapods-core/podfile/dsl'
require 'cocoapods-core/podfile/target_definition'
module Pod
class Podfile
include Pod::Podfile::DSL
# podfile 路徑
attr_accessor :defined_in_file
# 全部的 TargetDefinition 的根節點, 正常只有一個,即 Pods.project target
attr_accessor :root_target_definitions
# 記錄 Pods.project 項目的配置信息
attr_accessor :internal_hash
# 當前 DSL 解析使用的 TargetDefinition
attr_accessor :current_target_definition
# ...
end
end
複製代碼
直接看 dsl.rb
,該文件內部定義了 Podfile DSL 支持的全部方法。經過 include 的使用將 Pod::Podfile::DSL
模塊 Mix-in 後插入到 Podfile 類中。 想了解更多 Mix-in 特性,移步 Ruby 特性之 Mix-in。
The Lockfile stores information about the pods that were installed by CocoaPods.
Lockfile,顧名思義是用於記錄最後一次 CocoaPods 所安裝的 Pod 依賴庫版本的信息快照。也就是生成的 Podfile.lock
。
在 pod install
過程,Podfile 會結合它來確認最終所安裝的 Pod 版本,固定 Pod 依賴庫版本防止其自動更新。Lockfile 也做爲 Pods 狀態清單 (mainfest),用於記錄安裝過程的中哪些 Pod 須要被刪除或安裝或更新等。
以開頭的 Podfile 經 pod install
所生成的 Podfile.lock
爲例:
PODS:
- Alamofire (4.6.0)
DEPENDENCIES:
- Alamofire (from `./Alamofire`)
EXTERNAL SOURCES:
Alamofire:
:path: "./Alamofire"
SPEC CHECKSUMS:
Alamofire: 0dda98a0ed7eec4bdcd5fe3cdd35fcd2b3022825
PODFILE CHECKSUM: da12cc12a30cfb48ebc5d14e8f51737ab65e8241
COCOAPODS: 1.10.0.beta.2
複製代碼
咱們來分析一下,經過該 Lockfile 可以獲取哪些信息:
Key | 含義 |
---|---|
PODS | 記錄全部 Pod 庫的具體安裝版本號 |
DEPENDENCIES | 記錄各 Pod 庫之間的相互依賴關係,因爲這裏只有 Alamofire 且它無其餘依賴,暫時無關看出區別 |
EXTERNAL SOURCES | 記錄部分經過外部源的 Pod 庫(Git 引入、Path 引入) |
SPEC CHECKSUMS | 記錄當前各 Pod 庫的 Podspec 文件 Hash 值,其實就是文件的 md5 |
PODFILE CHECKSUM | 記錄 Podfile 文件的 Hash 值,一樣是 md5,確認是否有變動 |
COCOAPODS | 記錄上次所使用的 CocoaPods 版本 |
你能夠在 CocoaPods 的 /lib/cocoapods/config.rb
找到 Podfile 所支持的文件類型:
PODFILE_NAMES = [
'CocoaPods.podfile.yaml',
'CocoaPods.podfile',
'Podfile',
'Podfile.rb',
].freeze
複製代碼
CocoaPods 按照上述命名優先級來查找工程目錄下所對應的 Podfile 文件。當發現目錄中存在 CocoaPods.podfile.yaml 文件時會優先加載。不少同窗可能只知道到 Podfile 支持 Ruby 的文件格式,而不瞭解它還支持了 YAML 格式。YAML 是 YAML Ain't Markup Language
的縮寫,其 官方定義:
YAML is a human friendly data serialization standard for all programming languages.
它是一種面向工程師友好的序列化語言。咱們的 Lockfile 文件就是以 YAML 格式寫入 Podfile.lock
中的。
回到 lib/cocoapods-core/podfile.rb
來看讀取方法:
module Pod
class Podfile
include Pod::Podfile::DSL
def self.from_file(path)
path = Pathname.new(path)
unless path.exist?
raise Informative, "No Podfile exists at path `#{path}`."
end
# 這裏咱們能夠看出,Podfile 目前已經支持告終尾是 .podfile 和 .rb 後綴的文件名
# 實際上是爲了改善不少編譯器使用文件後綴來確認 filetype,好比 vim
# 相比與 Podfile 這個文件名要更加的友好
case path.extname
when '', '.podfile', '.rb'
Podfile.from_ruby(path)
when '.yaml'
# 如今也支持了 .yaml 格式
Podfile.from_yaml(path)
else
raise Informative, "Unsupported Podfile format `#{path}`."
end
end
end
複製代碼
from_file
在 pod install
命令執行後的 verify_podfile_exists!
中被調用的:
def verify_podfile_exists!
unless config.podfile
raise Informative, "No `Podfile' found in the project directory."
end
end
複製代碼
而 Podfile 文件的讀取就是 config.podfile
裏觸發的,代碼在 CocoaPods 的 config.rb
文件中:
def podfile_path_in_dir(dir)
PODFILE_NAMES.each do |filename|
candidate = dir + filename
if candidate.file?
return candidate
end
end
nil
end
def podfile_path
@podfile_path ||= podfile_path_in_dir(installation_root)
end
def podfile
@podfile ||= Podfile.from_file(podfile_path) if podfile_path
end
複製代碼
這裏的方法 podfile
和 podfile_path
都是 lazy 加載的。最後 Core 的 from_file
將依據目錄下的 Podfile
文件類型選擇調用 from_yaml
或者 from_ruby
。
從 Pod::Command::Install
命令到 Podfile 文件加載的調用棧以下:
當咱們經過 pod init
來初始化 CocoaPods 項目時,默認生成的 Podfile 名稱就是 Podfile
,那就從 Podfile.from_ruby
開始。
def self.from_ruby(path, contents = nil)
# ①
contents ||= File.open(path, 'r:utf-8', &:read)
# 兼容 1.9 版本的 Rubinius 中的編碼問題
if contents.respond_to?(:encoding) && contents.encoding.name != 'UTF-8'
contents.encode!('UTF-8')
end
# 對 Podfile 中不規範的單引號或雙引號進行檢查,並進行自動修正,及拋出錯誤
if contents.tr!('「」‘’‛', %(""'''))
CoreUI.warn "..."
end
# ②
podfile = Podfile.new(path) do
begin
eval(contents, nil, path.to_s)
rescue Exception => e
message = "Invalid `#{path.basename}` file: #{e.message}"
raise DSLError.new(message, path, e, contents)
end
end
podfile
end
複製代碼
① 是對 Podfile 內容的讀取和編碼,同時對可能出現的單引號和雙引號的匹配問題進行了修正。 ② 以 path
和 block
爲入參進行 podfile
類的初始化並將其放回,保存在全局的 config.podfile
中。
Tips: 若是要在 Ruby 對象的初始化中傳入參數,須要重載 Object 的 initialize 方法,這裏的 Podfile.new(...) 本質上是
initialize
的方法調用。
initialize
方法所傳入的尾隨閉包 block
的核心在於內部的 eval
函數(在 CocoaPods 核心組件 中有提到):
eval(contents, nil, path.to_s)
複製代碼
它將 Podfile 中的文本內容轉化爲方法執行,也就是說裏面的參數是一段 Ruby 的代碼字符串,經過 eval
方法能夠直接執行。 繼續看 Podfile 的 initialize
方法:
def initialize(defined_in_file = nil, internal_hash = {}, &block)
self.defined_in_file = defined_in_file
@internal_hash = internal_hash
if block
default_target_def = TargetDefinition.new('Pods', self)
default_target_def.abstract = true
@root_target_definitions = [default_target_def]
@current_target_definition = default_target_def instance_eval(&block)
else
@root_target_definitions = []
end
end
複製代碼
它定義了三個參數:
參數 | 定義 |
---|---|
defined_in_file | Podfile 文件路徑 |
internal_hash | 經過 yaml 序列化獲得的 Podfile 配置信息,保存在 internal_hash 中 |
block | 用於映射 Podfile 的 DSL 配置 |
須要注意的是,經過
from_ruby
初始化的Podfile
只傳入了參數 1 和 3,參數 2internal_hash
則是提供給from_yaml
的。
當 block
存在,會初始化名爲 Pods
的 TargetDefinition 對象,用於保存 Pods project
的相關信息和 Pod 依賴。而後調用 instance_eval 執行傳入的 block
,將 Podfile 的 DSL 內容轉換成對應的方法和參數,最終將參數存入 internal_hash
和對應的 target_definitions
中。
Tips: 在 Ruby 中存在兩種不一樣的方式來執行代碼塊
block
,分別是instance_eval
和class_eval
。class_eval
的執行上下文與調用類相關,調用者是類名或者模塊名,而instance_eval
的調用者能夠是類的實例或者類自己。細節看 StackoverFlow。
YAML 格式的 Podfile 加載須要藉助 YAMLHelper 類來完成,YAMLHelper 則是基於 yaml 的簡單封裝。
def self.from_yaml(path)
string = File.open(path, 'r:utf-8', &:read)
# 爲了解決 Rubinius incomplete encoding in 1.9 mode
# https://github.com/rubinius/rubinius/issues/1539
if string.respond_to?(:encoding) && string.encoding.name != 'UTF-8'
string.encode!('UTF-8')
end
hash = YAMLHelper.load_string(string)
from_hash(hash, path)
end
def self.from_hash(hash, path = nil)
internal_hash = hash.dup
target_definitions = internal_hash.delete('target_definitions') || []
podfile = Podfile.new(path, internal_hash)
target_definitions.each do |definition_hash|
definition = TargetDefinition.from_hash(definition_hash, podfile)
podfile.root_target_definitions << definition
end
podfile
end
複製代碼
經過 from_yaml
將文件內容轉成 Ruby hash 後轉入 from_hash
方法。
區別於 from_ruby
,這裏調用的 initialize
將讀取的 hash 直接存入 internal_hash
,而後利用 TargetDefinition.from_hash
來完成的 hash 內容到 targets 的轉換,所以,這裏無需傳入 block 進行 DSL 解析和方法轉換。
前面提到 Podfile 的內容最終保存在 internal_hash
和 target_definitions
中,本質上都是使用了 hash
來保存數據。因爲 YAML 文件格式的 Podfile 加載後就是 hash 對象,無需過多加工。惟一須要處理的是遞歸調用 TargetDefinition 的 from_hash
方法來解析 target 子節點的數據。
所以,接下來的內容解析主要針對 Ruby 文件格式的 DSL 解析,咱們以 pod
方法爲例:
target 'Example' do
pod 'Alamofire'
end
複製代碼
當解析到 pod 'Alamofire'
時,會先經過 eval(contents, nil, path.to_s
將其轉換爲 dsl.rb
中的方法:
def pod(name = nil, *requirements)
unless name
raise StandardError, 'A dependency requires a name.'
end
current_target_definition.store_pod(name, *requirements)
end
複製代碼
name 爲 Alamofire,因爲咱們沒有指定對應的 Alamofire 版本,默認會使用最新版本。requirements
是控制 該 pod 來源獲取或者 pod target 的編譯選項等,例如:
pod 'Alamofire', '0.9'
pod 'Alamofire', :modular_headers => true
pod 'Alamofire', :configurations => ['Debug', 'Beta']
pod 'Alamofire', :source => 'https://github.com/CocoaPods/Specs.git'
pod 'Alamofire', :subspecs => ['Attribute', 'QuerySet']
pod 'Alamofire', :testspecs => ['UnitTests', 'SomeOtherTests']
pod 'Alamofire', :path => '~/Documents/AFNetworking'
pod 'Alamofire', :podspec => 'https://example.com/Alamofire.podspec'
pod 'Alamofire', :git => 'https://github.com/looseyi/Alamofire.git', :tag => '0.7.0'
複製代碼
Tips:requirements 最終是以 Gem::Requirement 對象來保存的。關於 pod 詳細說明請移步:Podfile 手冊。
對 name 進行校驗後,直接轉入 current_target_definition
畢竟 Pod 庫都是存在 Pods.project
之下:
def store_pod(name, *requirements)
return if parse_subspecs(name, requirements) # This parse method must be called first
parse_inhibit_warnings(name, requirements)
parse_modular_headers(name, requirements)
parse_configuration_whitelist(name, requirements)
parse_project_name(name, requirements)
if requirements && !requirements.empty?
pod = { name => requirements }
else
pod = name
end
get_hash_value('dependencies', []) << pod
nil
end
def get_hash_value(key, base_value = nil)
unless HASH_KEYS.include?(key)
raise StandardError, "Unsupported hash key `#{key}`"
end
internal_hash[key] = base_value if internal_hash[key].nil?
internal_hash[key]
end
def set_hash_value(key, value)
unless HASH_KEYS.include?(key)
raise StandardError, "Unsupported hash key `#{key}`"
end
internal_hash[key] = value
end
複製代碼
通過一系列檢查以後,調用 get_hash_value
獲取 internal_hash
的 dependencies
,並將 name 和 requirements
選項存入。
這裏的 dependencies
key 是定義在 TargetDefinition 文件的 **HASH_KEYS**
,表示 Core 所支持的配置參數:
# freeze 表示該數組不可修改。另外,%w 用於表示其中元素被單引號括起的數組。
# %W(#{foo} Bar Bar\ with\ space) => ["Foo", "Bar", "Bar with space"]
# 對應的還有 %W 表示其中元素被雙引號括起的數組。
HASH_KEYS = %w( name platform podspecs exclusive link_with link_with_first_target inhibit_warnings use_modular_headers user_project_path build_configurations project_names dependencies script_phases children configuration_pod_whitelist uses_frameworks swift_version_requirements inheritance abstract swift_version ).freeze
複製代碼
整個映射過程以下:
最後一節讓咱們來展現一下 💪,看看 Podfile
所謂的 targets
之間的依賴關係能夠玩出什麼花來 😂。
最簡單的 Podfile
就是文章開頭所展現的,不過在 Podfile
中還能夠對 Target 進行嵌套使用。假設在咱們的主工程同時維護了三個項目,它們都依賴了 Alamofire,經過俄羅斯套娃就能輕鬆知足條件:
target 'Demo1' do
pod 'Alamofire'
target 'Demo2' do
target 'Demo3' do
end
end
end
複製代碼
編譯後的 Pods.project
項目結構以下:
咱們知道,CocoaPods 在 Pods.project
中爲每一個在 Podfile 中聲明的 Target 生成一個與之對應的專屬 Target 來集成它的 Pod 依賴。對於有依賴關係的 Target 其生成的專屬 Target 名稱則會按照依賴關係疊加來命名,如 target Demo3
的專屬 Target 名稱爲 Pods-Demo1-Demo2-Demo3。安裝完成後主項目將會引入該專屬 Target 來完成依賴關聯,如 Demo3:
關於 Target 嵌套,一個父節點是能夠有多個子節點的:
target 'Demo1' do
pod 'Alamofire'
target 'Demo2' do
pod 'RxSwift'
end
target 'Demo3' do
pod 'SwiftyJSON'
end
end
複製代碼
上面例子中,因爲 Demo1 與 Demo2 都須要依賴 Alamofire,咱們經過 Target 嵌套讓 Demo2 來繼承 Demo1 的 Pods 庫依賴。這麼作可能會有一個限制,就是當 Demo1 的 Pod 依賴並不是 Demo2 所須要的時候,就會有依賴冗餘。此時就須要 Abstract Target
登場了。例如:
abstract_target 'Networking' do
pod 'Alamofire'
target 'Demo1' do
pod 'RxSwift'
end
target 'Demo2' do
pod 'ReactCocoa'
end
target 'Demo3' do
end
end
複製代碼
將網絡請求的 pod 依賴抽象到 Networking
target 中,這樣就能避免 Demo2 對 RxSwift 的依賴。這種方式配置所生成的 Pods.project
並不會存在名稱爲 Networking
的 Target,它僅會在主工程的專屬 Target 中留下印記:
本文結合 Xcode 工程結構來展開 CocoaPods-Core 的 Podfile 之旅,主要感覺以下:
這裏羅列了四個問題用來考察你是否已經掌握了這篇文章,若是沒有建議你加入收藏再次閱讀: