稍有 iOS 開發經驗的人應該都是用過 CocoaPods,而對於 CI、CD 有了解的同窗也都知道 Fastlane。而這兩個在 iOS 開發中很是便捷的第三方庫都是使用 Ruby 來編寫的,這是爲何?ios
先拋開這個話題不談,咱們來看一下 CocoaPods 和 Fastlane 是如何使用的,首先是 CocoaPods,在每個工程使用 CocoaPods 的工程中都有一個 Podfile:git
source 'https://github.com/CocoaPods/Specs.git'
target 'Demo' do
pod 'Mantle', '~> 1.5.1'
pod 'SDWebImage', '~> 3.7.1'
pod 'BlocksKit', '~> 2.2.5'
pod 'SSKeychain', '~> 1.2.3'
pod 'UMengAnalytics', '~> 3.1.8'
pod 'UMengFeedback', '~> 1.4.2'
pod 'Masonry', '~> 0.5.3'
pod 'AFNetworking', '~> 2.4.1'
pod 'Aspects', '~> 1.4.1'
end複製代碼
這是一個使用 Podfile 定義依賴的一個例子,不過 Podfile 對約束的描述實際上是這樣的:github
source('https://github.com/CocoaPods/Specs.git')
target('Demo') do
pod('Mantle', '~> 1.5.1')
...
end複製代碼
Ruby 代碼在調用方法時能夠省略括號。算法
Podfile 中對於約束的描述,其實均可以看做是對代碼簡寫,上面的代碼在解析時能夠當作 Ruby 代碼來執行。shell
Fastlane 中的代碼 Fastfile 也是相似的:編程
lane :beta do
increment_build_number
cocoapods
match
testflight
sh "./customScript.sh"
slack
end複製代碼
使用描述性的」代碼「編寫腳本,若是沒有接觸或者使用過 Ruby 的人很難相信上面的這些文本是代碼的。數組
在介紹 CocoaPods 的實現以前,咱們須要對 Ruby 的一些特性有一個簡單的瞭解,在向身邊的朋友「傳教」的時候,我每每都會用優雅這個詞來形容這門語言(手動微笑)。xcode
除了優雅以外,Ruby 的語法具備強大的表現力,而且其使用很是靈活,能快速實現咱們的需求,這裏簡單介紹一下 Ruby 中的一些特性。ruby
在許多語言,好比 Java 中,數字與其餘的基本類型都不是對象,而在 Ruby 中全部的元素,包括基本類型都是對象,同時也不存在運算符的概念,所謂的 1 + 1
,其實只是 1.+(1)
的語法糖而已。框架
得益於一切皆對象的概念,在 Ruby 中,你能夠向任意的對象發送 methods
消息,在運行時自省,因此筆者在每次忘記方法時,都會直接用 methods
來「查文檔」:
2.3.1 :003 > 1.methods
=> [:%, :&, :*, :+, :-, :/, :<, :>, :^, :|, :~, :-@, :**, :<=>, :<<, :>>, :<=, :>=, :==, :===, :[], :inspect, :size, :succ, :to_s, :to_f, :div, :divmod, :fdiv, :modulo, :abs, :magnitude, :zero?, :odd?, :even?, :bit_length, :to_int, :to_i, :next, :upto, :chr, :ord, :integer?, :floor, :ceil, :round, :truncate, :downto, :times, :pred, :to_r, :numerator, :denominator, :rationalize, :gcd, :lcm, :gcdlcm, :+@, :eql?, :singleton_method_added, :coerce, :i, :remainder, :real?, :nonzero?, :step, :positive?, :negative?, :quo, :arg, :rectangular, :rect, :polar, :real, :imaginary, :imag, :abs2, :angle, :phase, :conjugate, :conj, :to_c, :between?, :instance_of?, :public_send, :instance_variable_get, :instance_variable_set, :instance_variable_defined?, :remove_instance_variable, :private_methods, :kind_of?, :instance_variables, :tap, :is_a?, :extend, :define_singleton_method, :to_enum, :enum_for, :=~, :!~, :respond_to?, :freeze, :display, :send, :object_id, :method, :public_method, :singleton_method, :nil?, :hash, :class, :singleton_class, :clone, :dup, :itself, :taint, :tainted?, :untaint, :untrust, :trust, :untrusted?, :methods, :protected_methods, :frozen?, :public_methods, :singleton_methods, :!, :!=, :__send__, :equal?, :instance_eval, :instance_exec, :__id__]複製代碼
好比在這裏向對象 1
調用 methods
就會返回它能響應的全部方法。
一切皆對象不只減小了語言中類型的不一致,消滅了基本數據類型與對象之間的邊界;這一律念同時也簡化了語言中的組成元素,這樣 Ruby 中只有對象和方法,這兩個概念,這也下降了咱們理解這門語言的複雜度:
Ruby 對函數式編程範式的支持是經過 block,這裏的 block 和 Objective-C 中的 block 有些不一樣。
首先 Ruby 中的 block 也是一種對象,全部的 Block 都是 Proc 類的實例,也就是全部的 block 都是 first-class 的,能夠做爲參數傳遞,返回。
def twice(&proc)
2.times { proc.call() } if proc
end
def twice
2.times { yield } if block_given?
end複製代碼
yield
會調用外部傳入的 block,block_given?
用於判斷當前方法是否傳入了block
。
在這個方法調用時,是這樣的:
twice do
puts "Hello"
end複製代碼
最後一個須要介紹的特性就是 eval
了,早在幾十年前的 Lisp 語言就有了 eval
這個方法,這個方法會將字符串當作代碼來執行,也就是說 eval
模糊了代碼與數據之間的邊界。
> eval "1 + 2 * 3"
=> 7複製代碼
有了 eval
方法,咱們就得到了更增強大的動態能力,在運行時,使用字符串來改變控制流程,執行代碼;而不須要去手動解析輸入、生成語法樹。
在咱們對 Ruby 這門語言有了一個簡單的瞭解以後,就能夠開始寫一個簡易的解析 Podfile 的腳本了。
在這裏,咱們以一個很是簡單的 Podfile 爲例,使用 Ruby 腳本解析 Podfile 中指定的依賴:
source 'http://source.git'
platform :ios, '8.0'
target 'Demo' do
pod 'AFNetworking'
pod 'SDWebImage'
pod 'Masonry'
pod "Typeset"
pod 'BlocksKit'
pod 'Mantle'
pod 'IQKeyboardManager'
pod 'IQDropDownTextField'
end複製代碼
由於這裏的 source
、platform
、target
以及 pod
都是方法,因此在這裏咱們須要構建一個包含上述方法的上下文:
# eval_pod.rb
$hash_value = {}
def source(url)
end
def target(target)
end
def platform(platform, version)
end
def pod(pod)
end複製代碼
使用一個全局變量 hash_value
存儲 Podfile 中指定的依賴,而且構建了一個 Podfile 解析腳本的骨架;咱們先不去完善這些方法的實現細節,先嚐試一下讀取 Podfile 中的內容並執行會不會有什麼問題。
在 eval_pod.rb
文件的最下面加入這幾行代碼:
content = File.read './Podfile'
eval content
p $hash_value複製代碼
這裏讀取了 Podfile 文件中的內容,並把其中的內容當作字符串執行,最後打印 hash_value
的值。
$ ruby eval_pod.rb複製代碼
運行這段 Ruby 代碼雖然並無什麼輸出,可是並無報出任何的錯誤,接下來咱們就能夠完善這些方法了:
def source(url)
$hash_value['source'] = url
end
def target(target)
targets = $hash_value['targets']
targets = [] if targets == nil
targets << target
$hash_value['targets'] = targets
yield if block_given?
end
def platform(platform, version)
end
def pod(pod)
pods = $hash_value['pods']
pods = [] if pods == nil
pods << pod
$hash_value['pods'] = pods
end複製代碼
在添加了這些方法的實現以後,再次運行腳本就會獲得 Podfile 中的依賴信息了,不過這裏的實現很是簡單的,不少狀況都沒有處理:
$ ruby eval_pod.rb
{"source"=>"http://source.git", "targets"=>["Demo"], "pods"=>["AFNetworking", "SDWebImage", "Masonry", "Typeset", "BlocksKit", "Mantle", "IQKeyboardManager", "IQDropDownTextField"]}複製代碼
CocoaPods 中對於 Podfile 的解析與這裏的實現其實差很少,接下來就進入了 CocoaPods 的實現部分了。
在上面簡單介紹了 Ruby 的一些語法以及如何解析 Podfile 以後,咱們開始深刻了解一下 CocoaPods 是如何管理 iOS 項目的依賴,也就是 pod install
到底作了些什麼。
pod install
這個命令到底作了什麼?首先,在 CocoaPods 中,全部的命令都會由 Command
類派發到將對應的類,而真正執行 pod install
的類就是 Install
:
module Pod
class Command
class Install < Command
def run
verify_podfile_exists!
installer = installer_for_config
installer.repo_update = repo_update?(:default => false)
installer.update = false
installer.install!
end
end
end
end複製代碼
這裏面會從配置類的實例 config
中獲取一個 Installer
的實例,而後執行 install!
方法,這裏的 installer
有一個 update
屬性,而這也就是 pod install
和 update
之間最大的區別,其中後者會無視已有的 Podfile.lock 文件,從新對依賴進行分析:
module Pod
class Command
class Update < Command
def run
...
installer = installer_for_config
installer.repo_update = repo_update?(:default => true)
installer.update = true
installer.install!
end
end
end
end複製代碼
Podfile 中依賴的解析實際上是與咱們在手動解析 Podfile 章節所介紹的差很少,整個過程主要都是由 CocoaPods-Core 這個模塊來完成的,而這個過程早在 installer_for_config
中就已經開始了:
def installer_for_config
Installer.new(config.sandbox, config.podfile, config.lockfile)
end複製代碼
這個方法會從 config.podfile
中取出一個 Podfile
類的實例:
def podfile
@podfile ||= Podfile.from_file(podfile_path) if podfile_path
end複製代碼
類方法 Podfile.from_file
就定義在 CocoaPods-Core 這個庫中,用於分析 Podfile 中定義的依賴,這個方法會根據 Podfile 不一樣的類型選擇不一樣的調用路徑:
Podfile.from_file
`-- Podfile.from_ruby |-- File.open `-- eval複製代碼
from_ruby
類方法就會像咱們在前面作的解析 Podfile 的方法同樣,從文件中讀取數據,而後使用 eval
直接將文件中的內容當作 Ruby 代碼來執行。
def self.from_ruby(path, contents = nil)
contents ||= File.open(path, 'r:utf-8', &:read)
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 這個類的頂部,咱們使用 Ruby 的 Mixin
的語法來混入 Podfile 中代碼執行所須要的上下文:
include Pod::Podfile::DSL複製代碼
Podfile 中的全部你見到的方法都是定義在 DSL
這個模塊下面的:
module Pod
class Podfile
module DSL
def pod(name = nil, *requirements) end
def target(name, options = nil) end
def platform(name, target = nil) end
def inhibit_all_warnings! end
def use_frameworks!(flag = true) end
def source(source) end
...
end
end
end複製代碼
這裏定義了不少 Podfile 中使用的方法,當使用 eval
執行文件中的代碼時,就會執行這個模塊裏的方法,在這裏簡單看一下其中幾個方法的實現,好比說 source
方法:
def source(source)
hash_sources = get_hash_value('sources') || []
hash_sources << source
set_hash_value('sources', hash_sources.uniq)
end複製代碼
該方法會將新的 source
加入已有的源數組中,而後更新原有的 sources
對應的值。
稍微複雜一些的是 target
方法:
def target(name, options = nil)
if options
raise Informative, "Unsupported options `#{options}` for " \
"target `#{name}`."
end
parent = current_target_definition
definition = TargetDefinition.new(name, parent)
self.current_target_definition = definition
yield if block_given?
ensure
self.current_target_definition = parent
end複製代碼
這個方法會建立一個 TargetDefinition
類的實例,而後將當前環境系的 target_definition
設置成這個剛剛建立的實例。這樣,以後使用 pod
定義的依賴都會填充到當前的 TargetDefinition
中:
def pod(name = nil, *requirements)
unless name
raise StandardError, 'A dependency requires a name.'
end
current_target_definition.store_pod(name, *requirements)
end複製代碼
當 pod
方法被調用時,會執行 store_pod
將依賴存儲到當前 target
中的 dependencies
數組中:
def store_pod(name, *requirements)
return if parse_subspecs(name, requirements)
parse_inhibit_warnings(name, requirements)
parse_configuration_whitelist(name, requirements)
if requirements && !requirements.empty?
pod = { name => requirements }
else
pod = name
end
get_hash_value('dependencies', []) << pod
nil
end複製代碼
總結一下,CocoaPods 對 Podfile 的解析與咱們在前面作的手動解析 Podfile 的原理差很少,構建一個包含一些方法的上下文,而後直接執行 eval
方法將文件的內容當作代碼來執行,這樣只要 Podfile 中的數據是符合規範的,那麼解析 Podfile 就是很是簡單容易的。
Podfile 被解析後的內容會被轉化成一個 Podfile
類的實例,而 Installer
的實例方法 install!
就會使用這些信息安裝當前工程的依賴,而整個安裝依賴的過程大約有四個部分:
Pods.xcodeproj
工程def install!
resolve_dependencies
download_dependencies
generate_pods_project
integrate_user_project
end複製代碼
在上面的 install
方法調用的 resolve_dependencies
會建立一個 Analyzer
類的實例,在這個方法中,你會看到一些很是熟悉的字符串:
def resolve_dependencies
analyzer = create_analyzer
plugin_sources = run_source_provider_hooks
analyzer.sources.insert(0, *plugin_sources)
UI.section 'Updating local specs repositories' do
analyzer.update_repositories
end if repo_update?
UI.section 'Analyzing dependencies' do
analyze(analyzer)
validate_build_configurations
clean_sandbox
end
end複製代碼
在使用 CocoaPods 中常常出現的 Updating local specs repositories
以及 Analyzing dependencies
就是從這裏輸出到終端的,該方法不只負責對本地全部 PodSpec 文件的更新,還會對當前 Podfile
中的依賴進行分析:
def analyze(analyzer = create_analyzer)
analyzer.update = update
@analysis_result = analyzer.analyze
@aggregate_targets = analyzer.result.targets
end複製代碼
analyzer.analyze
方法最終會調用 Resolver
的實例方法 resolve
:
def resolve
dependencies = podfile.target_definition_list.flat_map do |target|
target.dependencies.each do |dep|
@platforms_by_dependency[dep].push(target.platform).uniq! if target.platform
end
end
@activated = Molinillo::Resolver.new(self, self).resolve(dependencies, locked_dependencies)
specs_by_target
rescue Molinillo::ResolverError => e
handle_resolver_error(e)
end複製代碼
這裏的 Molinillo::Resolver
就是用於解決依賴關係的類。
CocoaPods 爲了解決 Podfile 中聲明的依賴關係,使用了一個叫作 Milinillo 的依賴關係解決算法;可是,筆者在 Google 上並無找到與這個算法相關的其餘信息,推測是 CocoaPods 爲了解決 iOS 中的依賴關係創造的算法。
Milinillo 算法的核心是 回溯(Backtracking) 以及 向前檢查(forward check)),整個過程會追蹤棧中的兩個狀態(依賴和可能性)。
在這裏並不想陷入對這個算法執行過程的分析之中,若是有興趣能夠看一下倉庫中的 ARCHITECTURE.md 文件,其中比較詳細的解釋了 Milinillo 算法的工做原理,並對其功能執行過程有一個比較詳細的介紹。
Molinillo::Resolver
方法會返回一個依賴圖,其內容大概是這樣的:
Molinillo::DependencyGraph:[
Molinillo::DependencyGraph::Vertex:AFNetworking(#<Pod::Specification name="AFNetworking">),
Molinillo::DependencyGraph::Vertex:SDWebImage(#<Pod::Specification name="SDWebImage">),
Molinillo::DependencyGraph::Vertex:Masonry(#<Pod::Specification name="Masonry">),
Molinillo::DependencyGraph::Vertex:Typeset(#<Pod::Specification name="Typeset">),
Molinillo::DependencyGraph::Vertex:CCTabBarController(#<Pod::Specification name="CCTabBarController">),
Molinillo::DependencyGraph::Vertex:BlocksKit(#<Pod::Specification name="BlocksKit">),
Molinillo::DependencyGraph::Vertex:Mantle(#<Pod::Specification name="Mantle">),
...
]複製代碼
這個依賴圖是由一個結點數組組成的,在 CocoaPods 拿到了這個依賴圖以後,會在 specs_by_target
中按照 Target
將全部的 Specification
分組:
{
#<Pod::Podfile::TargetDefinition label=Pods>=>[],
#<Pod::Podfile::TargetDefinition label=Pods-Demo>=>[
#<Pod::Specification name="AFNetworking">,
#<Pod::Specification name="AFNetworking/NSURLSession">,
#<Pod::Specification name="AFNetworking/Reachability">,
#<Pod::Specification name="AFNetworking/Security">,
#<Pod::Specification name="AFNetworking/Serialization">,
#<Pod::Specification name="AFNetworking/UIKit">,
#<Pod::Specification name="BlocksKit/Core">,
#<Pod::Specification name="BlocksKit/DynamicDelegate">,
#<Pod::Specification name="BlocksKit/MessageUI">,
#<Pod::Specification name="BlocksKit/UIKit">,
#<Pod::Specification name="CCTabBarController">,
#<Pod::Specification name="CategoryCluster">,
...
]
}複製代碼
而這些 Specification
就包含了當前工程依賴的全部第三方框架,其中包含了名字、版本、源等信息,用於依賴的下載。
在依賴關係解決返回了一系列 Specification
對象以後,就到了 Pod install 的第二部分,下載依賴:
def install_pod_sources
@installed_specs = []
pods_to_install = sandbox_state.added | sandbox_state.changed
title_options = { :verbose_prefix => '-> '.green }
root_specs.sort_by(&:name).each do |spec|
if pods_to_install.include?(spec.name)
if sandbox_state.changed.include?(spec.name) && sandbox.manifest
previous = sandbox.manifest.version(spec.name)
title = "Installing #{spec.name} #{spec.version} (was #{previous})"
else
title = "Installing #{spec}"
end
UI.titled_section(title.green, title_options) do
install_source_of_pod(spec.name)
end
else
UI.titled_section("Using #{spec}", title_options) do
create_pod_installer(spec.name)
end
end
end
end複製代碼
在這個方法中你會看到更多熟悉的提示,CocoaPods 會使用沙盒(sandbox)存儲已有依賴的數據,在更新現有的依賴時,會根據依賴的不一樣狀態顯示出不一樣的提示信息:
-> Using AFNetworking (3.1.0)
-> Using AKPickerView (0.2.7)
-> Using BlocksKit (2.2.5) was (2.2.4)
-> Installing MBProgressHUD (1.0.0)
...複製代碼
雖然這裏的提示會有三種,可是 CocoaPods 只會根據不一樣的狀態分別調用兩種方法:
install_source_of_pod
create_pod_installer
create_pod_installer
方法只會建立一個 PodSourceInstaller
的實例,而後加入 pod_installers
數組中,由於依賴的版本沒有改變,因此不須要從新下載,而另外一個方法的 install_source_of_pod
的調用棧很是龐大:
installer.install_source_of_pod
|-- create_pod_installer
| `-- PodSourceInstaller.new `-- podSourceInstaller.install!
`-- download_source `-- Downloader.download
`-- Downloader.download_request `-- Downloader.download_source
|-- Downloader.for_target
| |-- Downloader.class_for_options
| `-- Git/HTTP/Mercurial/Subversion.new |-- Git/HTTP/Mercurial/Subversion.download `-- Git/HTTP/Mercurial/Subversion.download!
`-- Git.clone複製代碼
在調用棧的末端 Downloader.download_source
中執行了另外一個 CocoaPods 組件 CocoaPods-Download 中的方法:
def self.download_source(target, params)
FileUtils.rm_rf(target)
downloader = Downloader.for_target(target, params)
downloader.download
target.mkpath
if downloader.options_specific?
params
else
downloader.checkout_options
end
end複製代碼
方法中調用的 for_target
根據不一樣的源會建立一個下載器,由於依賴可能經過不一樣的協議或者方式進行下載,好比說 Git/HTTP/SVN 等等,組件 CocoaPods-Downloader 就會根據 Podfile 中依賴的參數選項使用不一樣的方法下載依賴。
大部分的依賴都會被下載到 ~/Library/Caches/CocoaPods/Pods/Release/
這個文件夾中,而後從這個這裏複製到項目工程目錄下的 ./Pods
中,這也就完成了整個 CocoaPods 的下載流程。
CocoaPods 經過組件 CocoaPods-Downloader 已經成功將全部的依賴下載到了當前工程中,這裏會將全部的依賴打包到 Pods.xcodeproj
中:
def generate_pods_project(generator = create_generator)
UI.section 'Generating Pods project' do
generator.generate!
@pods_project = generator.project
run_podfile_post_install_hooks
generator.write
generator.share_development_pod_schemes
write_lockfiles
end
end複製代碼
generate_pods_project
中會執行 PodsProjectGenerator
的實例方法 generate!
:
def generate!
prepare
install_file_references
install_libraries
set_target_dependencies
end複製代碼
這個方法作了幾件小事:
Pods.xcodeproj
工程這幾件事情都離不開 CocoaPods 的另一個組件 Xcodeproj,這是一個能夠操做一個 Xcode 工程中的 Group 以及文件的組件,咱們都知道對 Xcode 工程的修改大多數狀況下都是對一個名叫 project.pbxproj
的文件進行修改,而 Xcodeproj 這個組件就是 CocoaPods 團隊開發的用於操做這個文件的第三方庫。
最後的這一部分與生成 Pods.xcodeproj
的過程有一些類似,這裏使用的類是 UserProjectIntegrator
,調用方法 integrate!
時,就會開始集成工程所須要的 Target:
def integrate!
create_workspace
integrate_user_targets
warn_about_xcconfig_overrides
save_projects
end複製代碼
對於這一部分的代碼,也不是很想展開來細談,簡單介紹一下這裏的代碼都作了什麼,首先會經過 Xcodeproj::Workspace
建立一個 workspace,以後會獲取全部要集成的 Target 實例,調用它們的 integrate!
方法:
def integrate!
UI.section(integration_message) do
XCConfigIntegrator.integrate(target, native_targets)
add_pods_library
add_embed_frameworks_script_phase
remove_embed_frameworks_script_phase_from_embedded_targets
add_copy_resources_script_phase
add_check_manifest_lock_script_phase
end
end複製代碼
方法將每個 Target 加入到了工程,使用 Xcodeproj 修改 Copy Resource Script Phrase
等設置,保存 project.pbxproj
,整個 Pod install 的過程就結束了。
最後想說的是 pod install 和 pod update 區別仍是比較大的,每次在執行 pod install 或者 update 時最後都會生成或者修改 Podfile.lock
文件,其中前者並不會修改 Podfile.lock
中顯示指定的版本,然後者會會無視該文件的內容,嘗試將全部的 pod 更新到最新版。
CocoaPods 工程的代碼雖然很是多,不過代碼的邏輯很是清晰,整個管理並下載依賴的過程很是符合直覺以及邏輯。
Github Repo:iOS-Source-Code-Analyze
Follow: Draveness · GitHub
Source: draveness.me/cocoapods