CocoaPods 都作了什麼?

稍有 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 的人很難相信上面的這些文本是代碼的。數組

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 中只有對象和方法,這兩個概念,這也下降了咱們理解這門語言的複雜度:

  • 使用對象存儲狀態
  • 對象之間經過方法通訊

block

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

最後一個須要介紹的特性就是 eval 了,早在幾十年前的 Lisp 語言就有了 eval 這個方法,這個方法會將字符串當作代碼來執行,也就是說 eval 模糊了代碼與數據之間的邊界。

> eval "1 + 2 * 3"
 => 7複製代碼

有了 eval 方法,咱們就得到了更增強大的動態能力,在運行時,使用字符串來改變控制流程,執行代碼;而不須要去手動解析輸入、生成語法樹。

手動解析 Podfile

在咱們對 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複製代碼

由於這裏的 sourceplatformtarget 以及 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 的實現部分了。

CocoaPods 的實現

在上面簡單介紹了 Ruby 的一些語法以及如何解析 Podfile 以後,咱們開始深刻了解一下 CocoaPods 是如何管理 iOS 項目的依賴,也就是 pod install 到底作了些什麼。

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 installupdate 之間最大的區別,其中後者會無視已有的 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 中依賴的解析實際上是與咱們在手動解析 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! 就會使用這些信息安裝當前工程的依賴,而整個安裝依賴的過程大約有四個部分:

  • 解析 Podfile 中的依賴
  • 下載依賴
  • 建立 Pods.xcodeproj 工程
  • 集成 workspace
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 就是用於解決依賴關係的類。

解決依賴關係(Resolve Dependencies)

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 的下載流程。

生成 Pods.xcodeproj

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 工程
  • 將依賴中的文件加入工程
  • 將依賴中的 Library 加入工程
  • 設置目標依賴(Target Dependencies)

這幾件事情都離不開 CocoaPods 的另一個組件 Xcodeproj,這是一個能夠操做一個 Xcode 工程中的 Group 以及文件的組件,咱們都知道對 Xcode 工程的修改大多數狀況下都是對一個名叫 project.pbxproj 的文件進行修改,而 Xcodeproj 這個組件就是 CocoaPods 團隊開發的用於操做這個文件的第三方庫。

生成 workspace

最後的這一部分與生成 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

相關文章
相關標籤/搜索