CocoaPods歷險記這個專題是 Edmond和 冬瓜共同撰寫,對於 iOS / macOS 工程中版本管理工具 CocoaPods 的實現細節、原理、源碼、實踐與經驗的分享記錄,旨在幫助你們可以更加了解這個依賴管理工具,而不只侷限於pod install
和pod update
。
在上文 版本管理工具及 Ruby 工具鏈環境 中,咱們聊到如何統一管理團隊小夥伴的 CocoaPods 生產環境及使用到的 Ruby 工具鏈。今天讓咱們將目光轉到 CocoaPods 身上,一塊兒來聊聊它的主要構成,以及各個組件在整個 Pods 工做流的關係。html
爲了總體把握 CocoaPods 這個項目,建議你們去入門一下 Ruby 這門腳本語言。另外本文基於 CocoaPods 1.9.2 版本。
做爲包管理工具,CocoaPods 隨着 Apple 生態的蓬勃發展也在不斷迭代和進化,而且各部分核心功能也都演化出相對獨立的組件。這些功能獨立的組件,均拆分出一個個獨立的 Gem 包,而 CocoaPods 則是這些組件的「集大成者」。git
咱們知道在 Pod 管理的項目中,Podfile
文件裏描述了它所依賴的 dependencies,相似的 Gem 的依賴能夠在 Gemfile
中查看。那 CocoaPods 的 Gemfile
有哪些依賴呢?github
SKIP_UNRELEASED_VERSIONS = false # ... source 'https://rubygems.org' gemspec gem 'json', :git => 'https://github.com/segiddins/json.git', :branch => 'seg-1.7.7-ruby-2.2' group :development do cp_gem 'claide', 'CLAide' cp_gem 'cocoapods-core', 'Core', '1-9-stable' cp_gem 'cocoapods-deintegrate', 'cocoapods-deintegrate' cp_gem 'cocoapods-downloader', 'cocoapods-downloader' cp_gem 'cocoapods-plugins', 'cocoapods-plugins' cp_gem 'cocoapods-search', 'cocoapods-search' cp_gem 'cocoapods-stats', 'cocoapods-stats' cp_gem 'cocoapods-trunk', 'cocoapods-trunk' cp_gem 'cocoapods-try', 'cocoapods-try' cp_gem 'molinillo', 'Molinillo' cp_gem 'nanaimo', 'Nanaimo' cp_gem 'xcodeproj', 'Xcodeproj' gem 'cocoapods-dependencies', '~> 1.0.beta.1' # ... # Integration tests gem 'diffy' gem 'clintegracon' # Code Quality gem 'inch_by_inch' gem 'rubocop' gem 'danger' end group :debugging do gem 'cocoapods_debug' gem 'rb-fsevent' gem 'kicker' gem 'awesome_print' gem 'ruby-prof', :platforms => [:ruby] end
上面的 Gemfile
中咱們看到不少經過 cp_gem
裝載的 Gem 庫,其方法以下:算法
def cp_gem(name, repo_name, branch = 'master', path: false) return gem name if SKIP_UNRELEASED_VERSIONS opts = if path { :path => "../#{repo_name}" } else url = "https://github.com/CocoaPods/#{repo_name}.git" { :git => url, :branch => branch } end gem name, opts end
它是用於方便開發和調試,當 **SKIP_UNRELEASED_VERSIONS**
爲 false && path
爲 true
時會使用與本地的 CocoaPods 項目同級目錄下的 git 倉庫,不然會使用對應的項目直接經過 Gem 加載。shell
經過簡單的目錄分割和 Gemfile
管理,就實現了最基本又最直觀的熱插拔,對組件開發十分友好。因此你只要將多個倉庫以下圖方式排列,便可實現跨倉庫組件開發:json
$ ls -l lrwxr-xr-x 1 gua staff 31 Jul 30 21:34 CocoaPods lrwxr-xr-x 1 gua staff 26 Jul 31 13:27 Core lrwxr-xr-x 1 gua staff 31 Jul 31 10:14 Molinillo lrwxr-xr-x 1 gua staff 31 Aug 15 11:32 Xcodeproj lrwxr-xr-x 1 gua staff 42 Jul 31 10:14 cocoapods-downloader
經過上面對於 Gemfile
的簡單分析,能夠看出 CocoaPods 不只僅是一個倉庫那麼簡單,它做爲一個三方庫版本管理工具,對自身組件的管理和組件化也是十分講究的。咱們繼續來看這份 Gemfile
中的核心開發組件:swift
The CLAide gem is a simple command line parser, which provides an API that allows you to quickly create a full featured command-line interface.
CLAide 雖然是一個簡單的命令行解釋器,但它提供了功能齊全的命令行界面和 API。它不只負責解析咱們使用到的 Pods
命令,如:pod install
, pod update
等,還可用於封裝經常使用的一些腳本,將其打包成簡單的命令行小工具。xcode
PS: 所謂命令行解釋器就是從標準輸入或者文件中讀取命令並執行的程序。詳見 Wiki。
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
,以及全部的 .lock
文件中特殊的 YAML 文件。緩存
The Cocoapods-downloader gem is a small library that provides downloaders for various source control types (HTTP/SVN/Git/Mercurial). It can deal with tags, commits, revisions, branches, extracting files from zips and almost anything these source control system would use.
Cocoapods-Downloader 是用於下載源碼的小工具,它支持各類類型的版本管理工具,包括 HTTP / SVN / Git / Mercurial。它能夠提供 tags
、commites
,revisions
,branches
以及 zips
文件的下載和解壓縮操做。ruby
The Molinillo gem is a generic dependency resolution algorithm, used in CocoaPods, Bundler and RubyGems.
Molinillo 是 CocoaPods 對於依賴仲裁算法的封裝,它是一個具備前向檢察的回溯算法。不只在 Pods
中,Bundler
和 RubyGems
也是使用的這一套仲裁算法。
The Xcodeproj gem lets you create and modify Xcode projects from Ruby. Script boring management tasks or build Xcode-friendly libraries. Also includes support for Xcode workspaces (.xcworkspace) and configuration files (.xcconfig).
Xcodeproj 可經過 Ruby 來操做 Xcode 項目的建立和編輯等。可友好的支持 Xcode 項目的腳本管理和 libraries 構建,以及 Xcode 工做空間 (.xcworkspace) 和配置文件 .xcconfig
的管理。
CocoaPods plugin which shows info about available CocoaPods plugins or helps you get started developing a new plugin. Yeah, it's very meta.
cocoapods-plugins
插件管理功能,其中有 pod plugin
全套命令,支持對於 CocoaPods 插件的列表一覽(list)、搜索(search)、建立(create)功能。
固然,上面還有不少組件這裏就不一一介紹了。經過查看 Gemfile
能夠看出 Pod 對於組件的拆分粒度是比較細微的,經過對各類組件的組合達到如今的完整版本。這些組件中,筆者的瞭解也十分有限,不過咱們會在以後的一系列文章來逐一介紹學習。
接下來,結合 pod install
安裝流程來展現各個組件在 Pods
工做流中的上下游關係。
每當咱們輸入 pod xxx
命令時,系統會首先調用 pod
命令。全部的命令都是在 /bin
目錄下存放的腳本,固然 Ruby 環境的也不例外。咱們能夠經過 which pod
來查看命令所在位置:
$ which pod /Users/edmond/.rvm/gems/ruby-2.6.1/bin/pod
這裏的顯示路徑不是
/usr/local/bin/pod
的緣由是由於使用
RVM 進行版本控制的。
咱們經過 cat
命令來查看一下這個入口腳本執行了什麼
$ cat /Users/edmond/.rvm/gems/ruby-2.6.1/bin/pod
輸出以下:
#!/usr/bin/env ruby_executable_hooks require 'rubygems' version = ">= 0.a" str = ARGV.first if str str = str.b[/\A_(.*)_\z/, 1] if str and Gem::Version.correct?(str) version = str ARGV.shift end end if Gem.respond_to?(:activate_bin_path) load Gem.activate_bin_path('cocoapods', 'pod', version) else gem "cocoapods", version load Gem.bin_path("cocoapods", "pod", version) end
程序 CocoaPods 是做爲 Gem 被安裝的,此腳本用於喚起 CocoaPods。邏輯比較簡單,就是一個單純的命令轉發。Gem.activate_bin_path
和 Gem.bin_path
用於找到 CocoaPods 的安裝目錄 cocoapods/bin
,最終加載該目錄下的 /pod
文件:
#!/usr/bin/env ruby # ... 忽略一些對於編碼處理的代碼 require 'cocoapods' # 這裏手動輸出一下調用棧,來關注一下 puts caller # 若是環境配置中指定了 ruby-prof 配置文件,會對執行命令過程進行性能監控 if profile_filename = ENV['COCOAPODS_PROFILE'] require 'ruby-prof' # 依據配置文件類型加載不一樣的 reporter 解析器 # ... File.open(profile_filename, 'w') do |io| reporter.new(RubyProf.profile { Pod::Command.run(ARGV) }).print(io) end else Pod::Command.run(ARGV) end
一塊兒來查看一下 pod
命令的輸出結果:
$ pod /Users/edmond/.rvm/gems/ruby-2.6.1/bin/pod:24:in `load' /Users/edmond/.rvm/gems/ruby-2.6.1/bin/pod:24:in `<main>' /Users/edmond/.rvm/gems/ruby-2.6.1/bin/ruby_executable_hooks:24:in `eval' /Users/edmond/.rvm/gems/ruby-2.6.1/bin/ruby_executable_hooks:24:in `<main>'
ruby_executable_hooks 經過 bin
目錄下的 pod
入口喚醒,再經過 eval 的手段調起咱們須要的 CocoaPods 工程。這是 RVM 的自身行爲,它利用了 executable-hook 來注入 Gems 插件來定製擴展。
PS:大多數動態語言都支持
eval
這一神奇的函數。打 Lisp 開始就支持了,它經過接受一個字符串類型做爲參數,將其解析成語句並混合在當前做用域內運行。詳細能夠參考這篇
文章。
在入口的最後部分,經過調用 Pod::Command.run(ARGV)
,實例化了一個 CLAide::Command
對象,開始咱們的 CLAide 命令解析階段。這裏不對 CLAide
這個命令解析工具作過多的分析,這個是後面系列文章的內容。這裏咱們僅僅須要知道:
每一個 CLAide 命令的執行,最終都會對應到具體 Command Class 的
run
方法。
Pod 命令對應的 run 方法實現以下:
module Pod class Command class Install < Command # ... def run # 判斷是否存在 Podfile 文件,若是存在則進行 Podfile 的初始化 verify_podfile_exists! # 從 Config 中獲取一個 Instraller 實例 installer = installer_for_config # 默認是不執行 update installer.repo_update = repo_update?(:default => false) installer.update = false installer.deployment = @deployment # install 的真正過程 installer.install! end end end end
上述所見的 Command::Install
類對應的命令爲 pod install
。pod install
過程是依賴於 Podfile
文件的,因此在入口處會作檢測,若是不存在 Podfile
則直接拋出 No 'Podfile' found in the project directory 的異常 警告並結束命令。
在 installer
實例組裝完成以後,調用其 install!
方法,這時候才進入了咱們 pod install
命令的主體部分,流程以下圖:
對應的實現以下:
def install! prepare resolve_dependencies download_dependencies validate_targets if installation_options.skip_pods_project_generation? show_skip_pods_project_generation_message else integrate end write_lockfiles perform_post_install_actions end def integrate generate_pods_project if installation_options.integrate_targets? integrate_user_project else UI.section 'Skipping User Project Integration' end end
def prepare # 若是檢測出當前目錄是 Pods,直接 raise 終止 if Dir.pwd.start_with?(sandbox.root.to_path) message = 'Command should be run from a directory outside Pods directory.' message << "\n\n\tCurrent directory is #{UI.path(Pathname.pwd)}\n" raise Informative, message end UI.message 'Preparing' do # 若是 lock 文件的 CocoaPods 主版本和當前版本不一樣,將以新版本的配置對 xcodeproj 工程文件進行更新 deintegrate_if_different_major_version # 對 sandbox(Pods) 目錄創建子目錄結構 sandbox.prepare # 檢測 PluginManager 是否有 pre-install 的 plugin ensure_plugins_are_installed! # 執行插件中 pre-install 的全部 hooks 方法 run_plugins_pre_install_hooks end end
在 prepare
階段會將 pod install
的環境準備完成,包括版本一致性、目錄結構以及將 pre-install 的裝載插件腳本所有取出,並執行對應的 pre_install
hook。
def resolve_dependencies # 獲取 Sources plugin_sources = run_source_provider_hooks # 建立一個 Analyzer analyzer = create_analyzer(plugin_sources) # 若是帶有 repo_update **標記** UI.section 'Updating local specs repositories' do # 執行 Analyzer 的更新 Repo 操做 analyzer.update_repositories end if repo_update? UI.section 'Analyzing dependencies' do # 從 analyzer 取出最新的分析結果,@analysis_result,@aggregate_targets,@pod_targets analyze(analyzer) # 拼寫錯誤降級識別,白名單過濾 validate_build_configurations end # 若是 deployment? 爲 true,會驗證 podfile & lockfile 是否須要更新 UI.section 'Verifying no changes' do verify_no_podfile_changes! verify_no_lockfile_changes! end if deployment? analyzer end
依賴解析過程就是經過 Podfile
、Podfile.lock
以及沙盒中的 manifest
生成 Analyzer 對象。_Analyzer_ 內部會使用 Molinillo (具體的是 Molinillo::DependencyGraph
圖算法)解析獲得一張依賴關係表。
PS:經過 Analyzer 能獲取到不少依賴信息,例如 Podfile 文件的依賴分析結果,也能夠從 specs_by_target 來查看各個 target 相關的 specs。
另外,須要注意的是 analyze 的過程當中有一個 pre_download 的階段,即在 --verbose 下看到的 Fetching external sources 過程。這個 pre_download 階段不屬於依賴下載過程,而是在當前的依賴分析階段。
PS:該過程主要是解決當咱們在經過 Git 地址引入的 Pod 倉庫的狀況下,系統沒法從默認的 Source 拿到對應的 Spec,須要直接訪問咱們的 Git 地址下載倉庫的 zip 包,並取出對應的
podspec
文件,從而進行對比分析。
def download_dependencies UI.section 'Downloading dependencies' do # 初始化 sandbox 文件訪問器 create_file_accessors # 構造 Pod Source Installer install_pod_sources # 執行 podfile 定義的 pre install 的 hooks run_podfile_pre_install_hooks # 根據配置清理 pod sources 信息,主要是清理無用 platform 相關內容 clean_pod_sources end end
在 create_file_accessors
中會建立沙盒目錄的文件訪問器,經過構造 FileAccessor
實例來解析沙盒中的各類文件。接着是最重要的 install_pod_sources
過程,它會調用對應 Pod 的 install!
方法進行資源下載。
先來看看 install_pod_sources
方法的實現:
def install_pod_sources @installed_specs = [] # install 的 Pod 只須要這兩種狀態,added 和 changed 狀態的並集 pods_to_install = sandbox_state.added | sandbox_state.changed title_options = { :verbose_prefix => '-> '.green } puts "root_specs" root_specs.each do |item| puts item end # 將 Podfile 解析後排序處理 root_specs.sort_by(&:name).each do |spec| # 若是是 added 或 changed 狀態的 Pod if pods_to_install.include?(spec.name) # 若是是 changed 狀態而且 manifest 已經有記錄 if sandbox_state.changed.include?(spec.name) && sandbox.manifest # 版本更新 current_version = spec.version # 被更新版本記錄 previous_version = sandbox.manifest.version(spec.name) # 變更記錄 has_changed_version = current_version != previous_version # 找到第一個包含 spec.name 的 Pod,獲取對應的 Repo,其實就是 find 方法 current_repo = analysis_result.specs_by_source.detect { |key, values| break key if values.map(&:name).include?(spec.name) } # 獲取當前倉庫 current_repo &&= current_repo.url || current_repo.name # 獲取以前該倉庫的信息 previous_spec_repo = sandbox.manifest.spec_repo(spec.name) # 是否倉庫有變更 has_changed_repo = !previous_spec_repo.nil? && current_repo && (current_repo != previous_spec_repo) # 經過 title 輸出上面的詳細變動信息 title = ... else # 非 changed 狀態,展現 Installing 這個是常常見的 log title = "Installing #{spec}" end UI.titled_section(title.green, title_options) do # 經過 name 拿到對應的 installer,記錄到 @pod_installers 中 install_source_of_pod(spec.name) end else # 若是沒有 changed 狀況,直接展現 Using,也是常常見到的 log UI.titled_section("Using #{spec}", title_options) do # # 經過 sandbox, specs 的 platform 信息生成 Installer 實例,記錄到 @pod_installers 中 create_pod_installer(spec.name) end end end end # 經過緩存返回 PodSourceInstaller 實例 def create_pod_installer(pod_name) specs_by_platform = specs_for_pod(pod_name) # 當經過 pod_name 沒法找到對應的 pod_target 或 platform 配置,主動拋出錯誤信息 if specs_by_platform.empty? requiring_targets = pod_targets.select { |pt| pt.recursive_dependent_targets.any? { |dt| dt.pod_name == pod_name } } # message = "..." raise StandardError, message end # 經過 sandbox, specs 的 platform 信息生成 Installer 實例 pod_installer = PodSourceInstaller.new(sandbox, podfile, specs_by_platform, :can_cache => installation_options.clean?) pod_installers << pod_installer pod_installer end # 若是 resolver 聲明一個 Pod 已經安裝或者已經存在,將會將其刪除並從新安裝。若是不存在則直接安裝。 def install_source_of_pod(pod_name) pod_installer = create_pod_installer(pod_name) pod_installer.install! @installed_specs.concat(pod_installer.specs_by_platform.values.flatten.uniq) end
在方法的開始,root_specs
方法是經過 analysis_result
拿出全部根 spec
def root_specs analysis_result.specifications.map(&:root).uniq end
下面再來看看 pod_installer
中的 install!
方法,主要是經過調用 cocoapods-downloader
組件,將 Pod 對應的 Source 下載到本地。實現以下:
def install! download_source unless predownloaded? || local? PodSourcePreparer.new(root_spec, root).prepare! if local? sandbox.remove_local_podspec(name) unless predownloaded? || local? || external? end
用來驗證以前流程中的產物 (pod 所生成的 Targets) 的合法性。主要做用就是構造 TargetValidator
,並執行 validate!
方法:
def validate_targets validator = Xcode::TargetValidator.new(aggregate_targets, pod_targets, installation_options) validator.validate! end def validate! verify_no_duplicate_framework_and_library_names verify_no_static_framework_transitive_dependencies verify_swift_pods_swift_version verify_swift_pods_have_module_dependencies verify_no_multiple_project_names if installation_options.generate_multiple_pod_projects? end
驗證環節在整個 Install 過程當中僅佔很小的一部分。由於只是驗證部分,是徹底解耦的。
驗證是否有重名的 framework
,若是有衝突會直接拋出 frameworks with conflicting names
異常。
驗證動態庫中是否有靜態連接庫 (.a
或者 .framework
) 依賴,若是存在則會觸發 transitive dependencies that include static binaries...
錯誤。假設存在如下場景:
Weibo_SDK
組件 A 依賴組件 B,而組件 B 的 .podspec
文件中存在如下設置時,組件 B 將被斷定爲存在靜態庫依賴:
s.static_framework = true
s.dependency 'xxx_SDK
依賴了靜態庫 xxx_SDK
s.vendored_libraries = 'libxxx.a'
方式內嵌了靜態庫 libxxx
此時若是項目的
Podfile
設置了use_framework!
以動態連接方式打包的時,則會觸發該錯誤。
問題緣由
Podfile 中不使用use_frameworks!
時,每一個 pod 是會生成相應的 .a(靜態連接庫)文件,而後經過 static libraries 來管理 pod 代碼,在 Linked 時會包含該 pod 引用的其餘的 pod 的 .a 文件。
Podfile 中使用use_frameworks!
時是會生成相應的 .framework 文件,而後經過 dynamic frameworks 的方式來管理 pod 代碼,在 Linked 時會包含該 pod 引用的其餘的 pod 的 .framework 文件。
上述場景中雖然以 framework 的方式引用了 B 組件,然而 B 組件其實是一個靜態庫,須要拷貝並連接到該 pod 中,然而 dynamic frameworks 方式並不會這麼作,因此就報錯了。
解決方案
- 修改 pod 庫中
podspec
,增長pod_target_xcconfig
,定義好FRAMEWORK_SEARCH_PATHS
和OTHER_LDFLAGS
兩個環境變量;- hook
verify_no_static_framework_transitive_dependencies
的方法,將其幹掉!對應 issue- 修改 pod 庫中
podspec
,開啓 static_framework 配置s.static_framework = true
確保 Swift Pod 的 Swift 版本正確配置且互相兼容的。
檢測 Swift 庫的依賴庫是否支持了 module,這裏的 module 主要是針對 Objective-C 庫而言。
首先,Swift 是自然支持 module 系統來管理代碼的,Swift Module 是構建在 LLVM Module 之上的模塊系統。Swift 庫在解析後會生成對應的 modulemap
和 umbrella.h
文件,這是 LLVM Module 的標配,一樣 Objective-C 也是支持 LLVM Module。當咱們以 Dynamic Framework 的方式引入 Objective-C 庫時,Xcode 支持配置並生成 header,而靜態庫 .a 須要本身編寫對應的 umbrella.h
和 modulemap
。
其次,若是你的 Swift Pod 依賴了 Objective-C 庫,又但願以靜態連接的方式來打包 Swift Pod 時,就須要保證 Objective-C 庫啓用了 modular_headers
,這樣 CocoaPods 會爲咱們生成對應 modulemap
和 umbrella.h
來支持 LLVM Module。你能夠從這個地址 - http://blog.cocoapods.org/CocoaPods-1.5.0/ 查看到更多細節。
檢測是否全部的 Pod Target 中版本一致性問題。
用一個流程圖來歸納這一驗證環節:
工程文件的生成是 pod install
的最後一步,他會將以前版本仲裁後的全部組件經過 Project 文件的形式組織起來,而且會對 Project 中作一些用戶指定的配置。
def integrate generate_pods_project if installation_options.integrate_targets? # 集成用戶配置,讀取依賴項,使用 xcconfig 來配置 integrate_user_project else UI.section 'Skipping User Project Integration' end end def generate_pods_project # 建立 stage sanbox 用於保存安裝前的沙盒狀態,以支持增量編譯的對比 stage_sandbox(sandbox, pod_targets) # 檢查是否支持增量編譯,若是支持將返回 cache result cache_analysis_result = analyze_project_cache # 須要從新生成的 target pod_targets_to_generate = cache_analysis_result.pod_targets_to_generate # 須要從新生成的 aggregate target aggregate_targets_to_generate = cache_analysis_result.aggregate_targets_to_generate # 清理須要從新生成 target 的 header 和 pod folders clean_sandbox(pod_targets_to_generate) # 生成 Pod Project,組裝 sandbox 中全部 Pod 的 path、build setting、源文件引用、靜態庫文件、資源文件等 create_and_save_projects(pod_targets_to_generate, aggregate_targets_to_generate, cache_analysis_result.build_configurations, cache_analysis_result.project_object_version) # SandboxDirCleaner 用於清理增量 pod 安裝中的無用 headers、target support files 目錄 SandboxDirCleaner.new(sandbox, pod_targets, aggregate_targets).clean! # 更新安裝後的 cache 結果到目錄 `Pods/.project_cache` 下 update_project_cache(cache_analysis_result, target_installation_results) end
在 install
過程當中,除去依賴仲裁部分和下載部分的時間消耗,在工程文件生成也會有相對較大的時間開銷。這裏每每也是速度優化核心位置。
將依賴更新寫入 Podfile.lock
和 Manifest.lock
最後一步收尾工做,爲全部插件提供 post-installation 操做以及 hook。
def perform_post_install_actions # 調用 HooksManager 執行每一個插件的 post_install 方法 run_plugins_post_install_hooks # 打印過時 pod target 警告 warn_for_deprecations # 若是 pod 配置了 script phases 腳本,會主動輸出一條提示消息 warn_for_installed_script_phases # 輸出結束信息 `Pod installation complete!` print_post_install_message end
核心組件在 pod install
各階段的做用以下:
當咱們知道 CocoaPods 在 install 的大體過程後,咱們能夠對其作一些修改和控制。例如知道了插件的 pre_install
和 post_install
的具體時機,咱們就能夠在 Podfile
中執行對應的 Ruby 腳本,達到咱們的預期。同時瞭解 install 過程也有助於咱們進行每一個階段的性能分析,以優化和提升 Install 效率。
後續,將學習 CocoaPods 中每個組件的實現,將全部的問題在代碼中找到答案。
這裏羅列了四個問題用來考察你是否已經掌握了這篇文章,若是沒有建議你加入收藏再次閱讀:
pod
命令是如何找到並啓動 CocoaPods 程序的?resolve_dependencies
階段中的 pre_download
是爲了解決什麼問題?validate_targets
都作了哪些校驗工做?