CocoaPods 都做了什麼?

稍有 iOS 開發經驗的人應該都是用過 CocoaPods,而對於 CI、CD 有了解的同學也都知道 Fastlane。而這兩個在 iOS 開發中非常便捷的第三方庫都是使用 Ruby 來編寫的,這是爲什麼?

先拋開這個話題不談,我們來看一下 CocoaPods 和 Fastlane 是如何使用的,首先是 CocoaPods,在每一個工程使用 CocoaPods 的工程中都有一個 Podfile:

Ruby
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 對約束的描述其實是這樣的:

Ruby
source('https://github.com/CocoaPods/Specs.git')

target('Demo') do  
    pod('Mantle', '~> 1.5.1')
    ...
end  

Ruby 代碼在調用方法時可以省略括號。

Podfile 中對於約束的描述,其實都可以看作是對代碼簡寫,上面的代碼在解析時可以當做 Ruby 代碼來執行。

Fastlane 中的代碼 Fastfile 也是類似的:

Ruby
lane :beta do  
  increment_build_number
  cocoapods
  match
  testflight
  sh "./customScript.sh"
  slack
end  

使用描述性的”代碼“編寫腳本,如果沒有接觸或者使用過 Ruby 的人很難相信上面的這些文本是代碼的。

Ruby 概述

在介紹 CocoaPods 的實現之前,我們需要對 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 的,可以作爲參數傳遞,返回。

Ruby
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 代碼雖然並沒有什麼輸出,但是並沒有報出任何的錯誤,接下來我們就可以完善這些方法了:

在添加了這些方法的實現之後,再次運行腳本就會得到 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 文件,重新對依賴進行分析

Ruby

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 中就已經開始了:

Ruby

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 不同的類型選擇不同的調用路徑:

Ruby

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)
    rescueException=> e
      message ="Invalid `#{path.basename}` file:#{e.message}"
      raiseDSLError.new(message, path, e, contents)
    end
  end
  podfile
end 

在 Podfile 這個類的頂部,我們使用 Ruby 的 Mixin 的語法來混入 Podfile 中代碼執行所需要的上下文:

Ruby

include Pod::Podfile::DSL  
Podfile 中的所有你見到的方法都是定義在 DSL 這個模塊下面的:
Ruby
module Pod  
  class Podfile
    moduleDSL
      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 方法:

Ruby

def source(source)  
  hash_sources = get_hash_value('sources')|| []
  hash_sources << source
  set_hash_value('sources', hash_sources.uniq)
end  

該方法會將新的 source 加入已有的源數組中,然後更新原有的sources 對應的值。

稍微複雜一些的是 target 方法:

Ruby

def target(name, options = nil)  
  if options
    raiseInformative,"Unsupported options `#{options}` for " \
      "target `#{name}`."
  end

  parent = current_target_definition
  definition =TargetDefinition.new(name, parent)
  self.current_target_definition= definition
  yieldif block_given?
ensure  
  self.current_target_definition= parent
end 

這個方法會創建一個 TargetDefinition 類的實例,然後將當前環境系的target_definition 設置成這個剛剛創建的實例。這樣,之後使用pod 定義的依賴都會填充到當前的 TargetDefinition 中:

Ruby

def pod(name =nil,*requirements)  
  unless name
    raiseStandardError,'A dependency requires a name.'
  end

  current_target_definition.store_pod(name, *requirements)
end  

pod 方法被調用時,會執行store_pod 將依賴存儲到當前target 中的dependencies 數組中:

Ruby

def store_pod(name, *requirements)  
  returnif 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

Ruby

def install!  
  resolve_dependencies
  download_dependencies
  generate_pods_project
  integrate_user_project
end  

在上面的 install 方法調用的resolve_dependencies 會創建一個Analyzer 類的實例,在這個方法中,你會看到一些非常熟悉的字符串:

Ruby

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
  endif 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 中的依賴進行分析:

Ruby

def analyze(analyzer = create_analyzer)  
  analyzer.update = update
  @analysis_result= analyzer.analyze
  @aggregate_targets= analyzer.result.targets
end  
analyzer.analyze 方法最終會調用Resolver 的實例方法resolve:
Ruby
def resolve  
  dependencies = podfile.target_definition_list.flat_mapdo|target|
    target.dependencies.eachdo|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 方法會返回一個依賴圖,其內容大概是這樣的:

Ruby

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 分組:

Ruby

{
    #<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 的第二部分,下載依賴:

Ruby

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).eachdo|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)存儲已有依賴的數據,在更新現有的依賴時,會根據依賴的不同狀態顯示出不同的提示信息:

Ruby

-> UsingAFNetworking (3.1.0)

-> UsingAKPickerView (0.2.7)

-> UsingBlocksKit (2.2.5) was (2.2.4)

-> InstallingMBProgressHUD (1.0.0)

...

雖然這裏的提示會有三種,但是 CocoaPods 只會根據不同的狀態分別調用兩種方法:

  • install_source_of_pod
  • create_pod_installer

create_pod_installer 方法只會創建一個PodSourceInstaller 的實例,然後加入pod_installers 數組中,因爲依賴的版本沒有改變,所以不需要重新下載,而另一個方法的install_source_of_pod 的調用棧非常龐大:

Ruby

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!:
Ruby
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: http://draveness.me/cocoapods

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章