1. 背景
某天被人問(wèn)到Cocoapods實(shí)現(xiàn)的原理,突然發(fā)現(xiàn)自己只是會(huì)用這個(gè)包管理器,但對(duì)其實(shí)現(xiàn)的原理并不清楚,而又剛巧我們最近在做一些代碼自動(dòng)化的工作,后續(xù)可能有將代碼自動(dòng)植入進(jìn)Xcode Project的需求,那CocoaPods的workflow中也包含這種添加代碼和庫(kù)的操作能力?;谝陨?,外加探索未知事物的好奇心,決定學(xué)習(xí)一下這個(gè)CocoaPods是如何游刃有余地管理和調(diào)度工程中的第三方庫(kù)的。
2. CocoaPods Source Code
老規(guī)矩:先放源碼,再講故事。
CocoaPods有64個(gè)Repositories, 這里只說(shuō)幾個(gè)比較核心或者比較常用的庫(kù):
2.1 CocoaPods庫(kù)
GitHub: https://github.com/CocoaPods
CocoaPods 官方源碼庫(kù)
2.2 CocoaPods/CocoaPod
GitHub: https://github.com/CocoaPods/CocoaPods
CocoaPods的主倉(cāng), 這是是一個(gè)面向用戶的組件,每當(dāng)執(zhí)行一個(gè) pod 命令時(shí),這個(gè)組件都將被激活。該組件包括了所有使用 CocoaPods 涉及到的功能,并且還能通過(guò)調(diào)用所有其它的 gems 來(lái)執(zhí)行任務(wù)。
2.3 CocoaPods/Core
GitHub: https://github.com/CocoaPods/Core
Core 組件提供支持與 CocoaPods 相關(guān)文件的處理,文件主要是 Podfile 和 podspecs。
2.4 CocoaPods/Xcodeproj
GitHub: https://github.com/CocoaPods/Xcodeproj
這個(gè) gem 組件負(fù)責(zé)所有工程文件的整合。它能夠?qū)?chuàng)建并修改 .xcodeproj 和 .xcworkspace 文件。它也可以作為單獨(dú)的一個(gè) gem 包使用。如果你想要寫(xiě)一個(gè)腳本來(lái)方便的修改工程文件,那么可以使用這個(gè) gem。
3. CocoaPods介紹
CocoaPods是個(gè)iOS的包管理工具,類(lèi)似于Android的Gradle,當(dāng)然你也可以用Gradle管理iOS的依賴包,參見(jiàn)Gradle Xcode Plugin。 各個(gè)語(yǔ)言都有自己的工具,比如管理node的npm,以及python常用的pip,easy_install. Ruby常用gems,Java的maven等。
CocoaPods是與Xcode深度耦合的一個(gè)工具,里面大量的代碼是對(duì)XcodeProj的直接操作,這也導(dǎo)致CocoaPods也只能用于iOS項(xiàng)目,甚至僅僅客戶端的項(xiàng)目。目前一些基于Swift的Server項(xiàng)目, 可以用Apple官方出的包管理工具:SPM: Swift Package Manager。
具體的使用中,創(chuàng)建Podfile、pod update這些都是基本操作,但這每一步操作的背后,Cocoapods都執(zhí)行了哪些操作,觸發(fā)了哪些模塊,很值得我們研究一下。
3.1 整體結(jié)構(gòu)
基于Xcode創(chuàng)建的一個(gè)新project,空空蕩蕩,但當(dāng)運(yùn)行了pod install之后,結(jié)構(gòu)會(huì)發(fā)生很大的改變,大致如下:
PolenTest
├── PolenTest
│ ├── PolenTest
│ ├── PolenTest.xcodeproj
│ ├── PolenTestTests
│ └── PolenTestUITests
├── PolenTest.xcworkspace
│ └── contents.xcworkspacedata
├── PodFile
├── Podfile.lock
├── Pods
│ ├── AFNetworking
│ ├── Headers
│ ├── Manifest.lock
│ ├── Pods.xcodeproj
│ └── Target\ Support\ Files
├── exportOptions.plist
└── wehere-dev-cloud.mobileprovision
會(huì)多出了很多額外的文件,比較大的是整個(gè)項(xiàng)目由xcodeproj上升為xcworkspace。Pod會(huì)單獨(dú)作為一個(gè)xcodeproj.
詳細(xì)再展開(kāi)看Pods的詳細(xì)目錄結(jié)構(gòu)及說(shuō)明如下:
Pods
├── Podfile # 指向根目錄下的Podfile 說(shuō)明依賴的第3方庫(kù)
├── Frameworks # 文件系統(tǒng)并沒(méi)有對(duì)應(yīng)的目錄 這只是1個(gè)虛擬的group 表示需要鏈接的frameowork
├── └── iOS # 文件系統(tǒng)并沒(méi)有對(duì)應(yīng)的目錄 這只是1個(gè)虛擬的group 這里表示是ios需要鏈接的framework
├── └── Xxx.framework # 鏈接的frameowork列表
├── Pods # 虛擬的group 管理所有第3方庫(kù)
│ └── AFNetwoking #AFNetworking庫(kù) 虛擬group 對(duì)應(yīng)文件系統(tǒng)Pods/AFNetworking/AFNetworking目錄下的內(nèi)容
│ ├── xxx.h #AFNetworking庫(kù)的頭文件 對(duì)應(yīng)文件系統(tǒng)Pods/AFNetworking/AFNetworking目錄下的所有頭文件
│ ├── xxx.m #AFNetworking庫(kù)的實(shí)現(xiàn)文件 對(duì)應(yīng)文件系統(tǒng)Pods/AFNetworking/AFNetworking目錄下的所有實(shí)現(xiàn)文件
│ └── Support Files # 虛擬group 支持文件 沒(méi)有直接對(duì)應(yīng)的文件系統(tǒng)目錄,該group下的文件都屬于目錄: Pods/Target Support Files/AFNetworking/
│ ├── AFNetworking.xcconfig # AFNetworking編譯的工程配置文件
│ ├── AFNetworking-prefix.pch # AFNetworking編譯用的預(yù)編譯頭文件
│ └── AFNetworking-dummy.m # 空實(shí)現(xiàn)文件
├── Products # 虛擬group
│ ├── libAFNetworking.a # AFNetworking target將生成的靜態(tài)庫(kù)
│ └── libPods-CardPlayer.a # Pods-CardPlayer target將生成的靜態(tài)庫(kù)
└── Targets Support Files # 虛擬group 管理支持文件
└── Pods-CardPlayer # 虛擬group Pods-CardPlayer target
├── Pods-CardPlayer-acknowledgements.markdown # 協(xié)議說(shuō)明文檔
├── Pods-CardPlayer-acknowledgements.plist # 協(xié)議說(shuō)明文檔
├── Pods-CardPlayer-dummy.m # 空實(shí)現(xiàn)
├── Pods-CardPlayer-frameworks.sh # 安裝framework的腳本
├── Pods-CardPlayer-resources.sh # 安裝resource的腳本
├── Pods-CardPlayer.debug.xcconfig # debug configuration 的 配置文件
└── Pods-CardPlayer.release.xcconfig # release configuration 的 配置文件
3.2 常用文件介紹
Pod里面有幾個(gè)常用的文件,需要了解一下,這樣平時(shí)在遇到問(wèn)題時(shí),也大概心里有概念是什么位置/什么級(jí)別的問(wèn)題。
pod.lock
當(dāng)前按照的第三方庫(kù)及其版本信息, 執(zhí)行pod install和pod update時(shí)候,有沒(méi)有pod.lock還是有一些細(xì)微的差異的, 直接看一下官方的解釋?zhuān)?/p>
pod install
This is to be used the first time you want to retrieve the pods for the project, but also every time you edit your Podfile to add, update or remove a pod.
Every time the pod install command is run — and downloads and install new pods — it writes the version it has installed, for each pods, in the Podfile.lock file. This file keeps track of the installed version of each pod and locks those versions.
When you run pod install, it only resolves dependencies for pods that are not already listed in the Podfile.lock.
For pods listed in the Podfile.lock, it downloads the explicit version listed in the Podfile.lock without trying to check if a newer version is available
For pods not listed in the Podfile.lock yet, it searches for the version that matches what is described in the Podfile (like in pod 'MyPod', '~>1.2')
pod update
When you run pod update PODNAME, CocoaPods will try to find an updated version of the pod PODNAME, without taking into account the version listed in Podfile.lock. It will update the pod to the latest version possible (as long as it matches the version restrictions in your Podfile).
If you run pod update with no pod name, CocoaPods will update every pod listed in your Podfile to the latest version possible.--- From 官方文檔: https://guides.cocoapods.org/using/pod-install-vs-update.html
mainfest.lock
這是每次運(yùn)行 pod install 命令時(shí)創(chuàng)建的 Podfile.lock 文件的副本。
如果你遇見(jiàn)過(guò)這樣的錯(cuò)誤 沙盒文件與 Podfile.lock 文件不同步 (The sandbox is not in sync with the Podfile.lock),這是因?yàn)?Manifest.lock 文件和 Podfile.lock 文件不一致所引起。由于 Pods 所在的目錄并不總在版本控制之下,這樣可以保證開(kāi)發(fā)者運(yùn)行 app 之前都能更新他們的 pods,否則 app 可能會(huì) crash,或者在一些不太明顯的地方編譯失敗。
pod中有一個(gè)check mainfest的命令,可以看一下這里有解釋?zhuān)?Check Pods Manifest.lock -- SatanWoo
Target Support Files
在Target Support Files目錄下每1個(gè)第3方庫(kù)都會(huì)有1個(gè)對(duì)應(yīng)的文件夾,比如AFNetworking,該目錄下有一個(gè)空實(shí)現(xiàn)文件,也有預(yù)定義頭文件用來(lái)優(yōu)化頭文件編譯速度,還會(huì)有1個(gè)xcconfig文件,該文件會(huì)在工程配置中使用,主要存放頭文件搜索目錄,鏈接的Flag(比如鏈接哪些庫(kù))
在Target Support Files目錄下還會(huì)有1個(gè)Pods-XXX的文件夾,該文件夾存放了第3方庫(kù)聲明文檔markdown文檔和plist文件,還有1個(gè)dummy的空實(shí)現(xiàn)文件,還有debug和release各自對(duì)應(yīng)的xcconfig配置文件,另外還有2個(gè)腳本文件,Pods-XXX-frameworks.sh腳本用于實(shí)現(xiàn)framework庫(kù)的鏈接,當(dāng)依賴的第3方庫(kù)是framework形式才會(huì)用到該腳本,另外1個(gè)腳本文件: Pods-XXX-resources.sh用于編譯storyboard類(lèi)的資源文件或者拷貝*.xcassets之類(lèi)的資源文件
--- From Cocoapods原理總結(jié) - Cloud Chou's Tech Blog
Podspec
這個(gè)是pod的版本一些基本信息說(shuō)明:比如比如版本號(hào)、source、license等。 如果要自己寫(xiě)一個(gè)開(kāi)源庫(kù),那么需要執(zhí)行:pod spec create xxxOPEN_SOURCEXXX, 自己創(chuàng)建一個(gè).podspec文件。
Pod::Spec.new do |spec|
spec.name = 'Reachability'
spec.version = '3.1.0'
spec.license = { :type => 'BSD' }
spec.homepage = 'https://github.com/tonymillion/Reachability'
spec.authors = { 'Tony Million' => 'tonymillion@gmail.com' }
spec.summary = 'ARC and GCD Compatible Reachability Class for iOS and OS X.'
spec.source = { :git => 'https://github.com/tonymillion/Reachability.git', :tag => 'v3.1.0' }
spec.source_files = 'Reachability.{h,m}'
spec.framework = 'SystemConfiguration'
end
當(dāng)然也有詳細(xì)的版本,可以看Podspec Syntax Reference
大概知道以上這些文件之后,然后再來(lái)看真?zhèn)€CocoaPods從零開(kāi)始的一個(gè)完整的流程:
4. CocoaPods的Workflow
簡(jiǎn)單來(lái)說(shuō),CocoaPods就是先識(shí)別下Podfile文件,然后基于Podfile文件,下載對(duì)應(yīng)的庫(kù),然后將這些庫(kù)插入到我們的項(xiàng)目中,各種依賴關(guān)系設(shè)置一下,然后更新一下本地記錄就差不多了。
具體來(lái)看:
4.1 命令識(shí)別
當(dāng)你輸入pod init或者pod update的時(shí)候,cocosPods會(huì)有個(gè)command模塊負(fù)責(zé)識(shí)別命令,常用命令如下:
Commands:
+ cache Manipulate the CocoaPods cache
+ deintegrate Deintegrate CocoaPods from your project
+ env Display pod environment
+ init Generate a Podfile for the current directory
+ install Install project dependencies according to versions from a
Podfile.lock
+ ipc Inter-process communication
+ lib Develop pods
+ list List pods
+ outdated Show outdated project dependencies
+ plugins Show available CocoaPods plugins
+ repo Manage spec-repositories
+ search Search for pods
+ setup Setup the CocoaPods environment
+ spec Manage pod specs
+ trunk Interact with the CocoaPods API (e.g. publishing new specs)
+ try Try a Pod!
+ update Update outdated project dependencies and create new Podfile.lock
當(dāng)我們輸入一個(gè)命令后,會(huì)由command模塊識(shí)別不同的命令,并轉(zhuǎn)化為相應(yīng)的響應(yīng)者去執(zhí)行這條命令,對(duì)于pod init這條命令,最終會(huì)觸發(fā) install.rb執(zhí)行如下代碼:
def run
verify_podfile_exists!
installer = installer_for_config
installer.repo_update = repo_update?(:default => false)
installer.update = false
installer.deployment = @deployment
installer.install!
end
上面的代碼就是一個(gè)install的基本過(guò)程,會(huì)new一個(gè)installer,
installer_for_config會(huì)初始化一個(gè)config,同時(shí)會(huì)解析Podfile文件.
pod update也會(huì)走這樣一段代碼只是,update屬性會(huì)設(shè)置為true
new 好installer之后,這個(gè)installer會(huì)開(kāi)始接下來(lái)的install的工作:
def install!
prepare
resolve_dependencies
download_dependencies
validate_targets
generate_pods_project
if installation_options.integrate_targets?
integrate_user_project
else
UI.section 'Skipping User Project Integration'
end
perform_post_install_actions
end
4.2 解析Podfile
首先會(huì)檢查Podfile是否存在,不存在直接報(bào)錯(cuò)
def verify_podfile_exists!
unless config.podfile
raise Informative, "No `Podfile' found in the project directory."
end
end
具體解析的過(guò)程是在installer_for_config里面開(kāi)始的,
但想看具體代碼,需要在CocoaPods/Core中的代碼中,才能看到讀取文件加解析的詳細(xì)過(guò)程, 如下:
podfile = Podfile.new(path) do
# rubocop:disable Lint/RescueException
begin
# rubocop:disable Eval
eval(contents, nil, path.to_s)
# rubocop:enable Eval
rescue Exception => e
message = "Invalid `#{path.basename}` file: #{e.message}"
raise DSLError.new(message, path, e, contents)
end
# rubocop:enable Lint/RescueException
end
這里需要注意的是,eval直接執(zhí)行podfile中的代碼,因?yàn)槭荄SL語(yǔ)言,且每個(gè)方法都已定義清楚了為前提, 所以這一步其實(shí)只有很簡(jiǎn)單的一行eval(contents, nil, path.to_s).
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
關(guān)于DSL想進(jìn)步一了解的,可以看一下:
其實(shí)解析文件,這些都屬于準(zhǔn)備工作,當(dāng)然準(zhǔn)備工作還包括一些路徑檢查、sandbox prepare、插件安裝等。這些不是重點(diǎn),就不做深入分析了,接下來(lái)才是真正的install工作:
4.3 解析依賴關(guān)系(resolve dependencies)
首先理清楚依賴關(guān)系:
def resolve_dependencies
plugin_sources = run_source_provider_hooks
analyzer = create_analyzer(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
UI.section 'Verifying no changes' do
verify_no_podfile_changes!
verify_no_lockfile_changes!
end if deployment?
analyzer
end
因?yàn)楹芏嘁蕾噹?kù)里面,自身又有一些其他依賴庫(kù),比如常見(jiàn)的,很多庫(kù)會(huì)對(duì)圖片的處理會(huì)使用SDWebImage, 或者基礎(chǔ)數(shù)據(jù)的處理,都會(huì)用Protobuf. 這就需要我們?cè)谙螺d所有依賴庫(kù)之前,先遞歸的對(duì)所有要下載的庫(kù)進(jìn)行依賴關(guān)系的整理,確保不需要重復(fù)下載相同的庫(kù),確保每個(gè)依賴庫(kù)的依賴庫(kù)的依賴庫(kù)都能被有效的下載和管理。
對(duì)于這樣一套機(jī)制,CocoaPods使用了一套Milinillo算法進(jìn)行實(shí)現(xiàn)。
算法實(shí)現(xiàn):Milinillo
算法源碼: https://github.com/CocoaPods/Molinillo
算法說(shuō)明:核心算法是采用了backtracking和forward checking。 會(huì)維護(hù)了一個(gè)棧進(jìn)行處理,棧中記錄和跟蹤了兩個(gè)狀態(tài):依賴、可能性。每個(gè)依賴庫(kù)的最新?tīng)顟B(tài)會(huì)push到棧上。每次一個(gè)已開(kāi)庫(kù)成功激活時(shí),會(huì)有個(gè)新?tīng)顟B(tài)push到站上代表激活狀態(tài)?;跅?shí)現(xiàn)的算法是backtracking(也叫unwinding)算法與出入棧的實(shí)現(xiàn)很類(lèi)似。
具體流程其官方文檔有了很詳細(xì)的介紹,有興趣的可以去實(shí)現(xiàn)以下。
這里面目前存在的一個(gè)問(wèn)題是,雖然這個(gè)算法解決了依賴遞歸的問(wèn)題,但是當(dāng)依賴的依賴遞歸層級(jí)非常深,其同一個(gè)庫(kù),在不同的依賴中有不同的版本場(chǎng)景下,這個(gè)算法的效率會(huì)有一定的下降。
所以在使用中,盡量避免依賴遞歸太深的模式出現(xiàn)。這個(gè)問(wèn)題美團(tuán)在處理移動(dòng)端的組件架構(gòu)過(guò)程中遇到過(guò),他們對(duì)此也做了一些優(yōu)化工作:
但是如果對(duì)依賴樹(shù)葉子節(jié)點(diǎn)的版本號(hào)控制不夠嚴(yán)密,或中間出現(xiàn)了循環(huán)依賴的情況,會(huì)導(dǎo)致回溯算法重復(fù)執(zhí)行了很多壓棧和出棧操作耗費(fèi)時(shí)間。美團(tuán)針對(duì)此類(lèi)問(wèn)題的做法是維護(hù)一套“去依賴的podspec源”,這個(gè)源中的dependency節(jié)點(diǎn)被清空了(下圖中間)。實(shí)際的所需依賴的全集在殼工程Podfile里平鋪,統(tǒng)一維護(hù)。這么做的好處是將之前的樹(shù)狀依賴(下圖左)壓平成一層(下圖右)
4.4 下載依賴庫(kù)(download dependencies)
上一步最終會(huì)返回一個(gè)需要下載的依賴庫(kù)列表root_specs,是個(gè)Specification泛型的數(shù)組[Array<Specification>]。接下來(lái)我們需要將這些依賴庫(kù)和本地版本進(jìn)行對(duì)比,然后將新版本下拉更新。代碼如下:
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
current_version = spec.version
previous_version = sandbox.manifest.version(spec.name)
has_changed_version = current_version != previous_version
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.casecmp(previous_spec_repo).zero?
title = "Installing #{spec.name} #{spec.version}"
title << " (was #{previous_version} and source changed to `#{current_repo}` from `#{previous_spec_repo}`)" if has_changed_version && has_changed_repo
title << " (was #{previous_version})" if has_changed_version && !has_changed_repo
title << " (source changed to `#{current_repo}` from `#{previous_spec_repo}`)" if !has_changed_version && has_changed_repo
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
對(duì)于已經(jīng)下載過(guò)的依賴庫(kù),會(huì)進(jìn)行版本比較,如需下載會(huì)執(zhí)行install_source_of_pod(spec.name), 如果是首次下載的依賴庫(kù),則會(huì)執(zhí)行create_pod_installer(spec.name), 別看這幾個(gè)方法簡(jiǎn)單,但是調(diào)用棧看起來(lái),其實(shí)很復(fù)雜, draveness同學(xué)的博客提供了完整的調(diào)用棧:
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
下載的實(shí)現(xiàn)是基于Downloader這個(gè)類(lèi)實(shí)現(xiàn)的,CocoaPods專(zhuān)門(mén)講這個(gè)庫(kù)開(kāi)源了CocoaPods/cocoapods-downloader,
從Cocoapods的代碼我們只能看到最終的下載代碼就2行:
downloader = Downloader.for_target(target, params)
downloader.download
如果繼續(xù)看cocoapods-downloader源碼,可以看到他支持了多種下載方式包括Git、Http等。基于不同的設(shè)置屬性,定義了不同的strategy。目前支持的下載strategy有Bazaar、Git、Mercurial、Http、Scp、Subversion.
# @return [Hash{Symbol=>Class}] The concrete classes of the supported
# strategies by key.
#
def self.downloader_class_by_key
{
:bzr => Bazaar,
:git => Git,
:hg => Mercurial,
:http => Http,
:scp => Scp,
:svn => Subversion,
}
end
基于工廠模式,每種下載方式,實(shí)現(xiàn)了自己的def download!方法, 以Class Git為例,其方法實(shí)現(xiàn)如下:
def download!
clone
checkout_commit if options[:commit]
end
def clone_arguments(force_head, shallow_clone)
command = ['clone', url, target_path, '--template=']
if shallow_clone && !options[:commit]
command += %w(--single-branch --depth 1)
end
unless force_head
if tag_or_branch = options[:tag] || options[:branch]
command += ['--branch', tag_or_branch]
end
end
command
end
可以看到Git就做了clone和checkout操作。我們經(jīng)常使用git clone其ruby實(shí)現(xiàn)是先組裝一個(gè)command,然后Hooks.execute_with_check("git", command, false)執(zhí)行g(shù)it 命令。
以上就是從代碼調(diào)用角度看到的下載流程。另外也有看到其他童鞋將下載的步驟整體畫(huà)了一個(gè)流程圖:

這里就不展開(kāi)分析, 可以直接去看這篇博客: Cocoapods私有庫(kù)管理實(shí)現(xiàn)
4.5 校驗(yàn)依賴庫(kù)(validate targets)
下載好之后,接下來(lái)是對(duì)本地Xcodeproj的操作。
日常規(guī)則: 任何事之前,先驗(yàn)證一下,這里也不意外,Xcode的target需要先做一些校驗(yàn)工作
def validate_targets
validator = Xcode::TargetValidator.new(aggregate_targets, pod_targets)
validator.validate!
end
驗(yàn)證的工作主要就是沒(méi)有duplicate的frame或者library,一些靜態(tài)庫(kù)的校驗(yàn)、Swift庫(kù)的版本校驗(yàn)等。
4.6 創(chuàng)建Pod.xcodeproj (generate pods project)
首先有個(gè)很重要的前提是:我們需要了解Xcode的project.pbxproj的內(nèi)部結(jié)構(gòu),這個(gè)我之前在XcodeProject的內(nèi)部結(jié)構(gòu)分析 中有過(guò)介紹。
那么,如何修改Xcodeproj的文件呢?
除了之前介紹的GitHub: mjmsmith/pbxplorer, Cocoapods自己實(shí)現(xiàn)了一個(gè)開(kāi)源庫(kù)CocoaPods/Xcodeproj
創(chuàng)建的過(guò)程大體流程基本是:創(chuàng)建Pod.xcodeproj工程、添加文件、添加庫(kù)、設(shè)置庫(kù)依賴。代碼如下:
def generate!
prepare
install_file_references
@target_installation_results = install_targets
integrate_targets(@target_installation_results.pod_target_installation_results)
app_hosts_by_host_key = install_app_hosts
wire_target_dependencies(@target_installation_results, app_hosts_by_host_key)
@target_installation_results
end
CocoaPods在第一步prepare中通過(guò)Pod::Project.new創(chuàng)建了一個(gè)pod的project
def create_project
if object_version = aggregate_targets.map(&:user_project).compact.map { |p| p.object_version.to_i }.min
Pod::Project.new(sandbox.project_path, false, object_version)
else
Pod::Project.new(sandbox.project_path)
end
end
然后將第三庫(kù)的代碼和資源文件加入工程(實(shí)際就是修改PBXFileReference), 這里自己可以打開(kāi)Xcode看一下Pod增加的文件是引用模式,而非create group模式(就是目錄與物理目錄不對(duì)應(yīng))。
然后添加Target、設(shè)置Target依賴關(guān)系。添加的Target除了本身Podfile讀取的依賴庫(kù),還添加了一個(gè)Pods-xxx你的項(xiàng)目名xxx Target。這個(gè)Target依賴了其他全部的第三方庫(kù)。
4.7 創(chuàng)建Xcode.workspace
def integrate_user_project
UI.section "Integrating client #{'project'.pluralize(aggregate_targets.map(&:user_project_path).uniq.count)}" do
installation_root = config.installation_root
integrator = UserProjectIntegrator.new(podfile, sandbox, installation_root, aggregate_targets)
integrator.integrate!
end
end
...
def integrate!
create_workspace
integrate_user_targets
warn_about_xcconfig_overrides
save_projects
end
這一步是創(chuàng)建workspace, 如果已經(jīng)創(chuàng)建過(guò)了,邏輯會(huì)去判斷,就不會(huì)走到這里來(lái)。
然后會(huì)重新整理一下項(xiàng)目的Target,將不用的依賴刪除,移除引用等。deintegrator.deintegrate_target(target)
然后檢查一些xcconfig文件,是否有寫(xiě)$(inherited), 沒(méi)寫(xiě)的話,會(huì)覆蓋掉系統(tǒng)的一些config條件,所以會(huì)出一些警告。我們之前項(xiàng)目中,在CI腳本中,因?yàn)镃onfig沒(méi)寫(xiě)$(inherited), 也導(dǎo)致出現(xiàn)了依賴庫(kù)的宏定義不可用的情況。
def perform_post_install_actions
run_plugins_post_install_hooks
warn_for_deprecations
warn_for_installed_script_phases
print_post_install_message
end
最后一波收尾操作,run一些plugin,script, 對(duì)于deprecated的pod做一些警告??之類(lèi)的。
然后告訴你:
Pod installation complete!
4.8 總結(jié)
看了一波又一波的代碼,應(yīng)該是有心理有個(gè)大致的印象和流程了,我們?cè)俸?jiǎn)單整理下,CocoaPods的完整過(guò)程, 其實(shí)再回頭看前面放的一段代碼:
def install!
prepare
resolve_dependencies
download_dependencies
validate_targets
generate_pods_project
if installation_options.integrate_targets?
integrate_user_project
else
UI.section 'Skipping User Project Integration'
end
perform_post_install_actions
end
- CocoaPods先讀取了Podfile的文件,知道了用戶需要下載哪些依賴庫(kù),
- 由
Analyzer基于Milinillo算法處理復(fù)雜的遞歸依賴關(guān)系,得到依賴圖譜DependencyGraph. - 開(kāi)始下載這些依賴,下載支持多種strategy,本質(zhì)上講就是,執(zhí)行各種git clone命令。
- 下載成功后,然后生成pods.project,講文件和資源加入項(xiàng)目工程目錄.
- 生成 xcode的workspace.
- 設(shè)置Target的依賴關(guān)系,Config文件,framework的路徑等,完成這個(gè)xcode的配置工作.
以上就是CocoaPods的workflow, 關(guān)于這部分想深入學(xué)習(xí)的同學(xué),可以看CocoaPods 都做了什么?, 目前看過(guò)的博客里介紹的最深度的一篇。
5. 背后的邏輯
5.1 如果我們自己來(lái)實(shí)現(xiàn)
看完了CocoaPods的完整流程,想一想,步驟1,2,3,4,5... 讓我們閉上眼思考下:如果要我們自己去實(shí)現(xiàn)一個(gè)CocoaPods,那我們需要哪些步驟,以及會(huì)遇到哪些問(wèn)題呢?
- 首先我們需要定制一個(gè)DSL語(yǔ)言,面向開(kāi)發(fā)者可以寫(xiě)出很簡(jiǎn)單的依賴庫(kù)的定制需求。如果你對(duì)ruby比較熟悉,那么這應(yīng)該不會(huì)是一個(gè)難點(diǎn)。
- 然后解析和下載,以及依賴關(guān)系的處理,雖然看起來(lái)麻煩,假設(shè)不考慮最優(yōu)算法的話,我們用簡(jiǎn)單的邏輯也可以實(shí)現(xiàn),如果你寫(xiě)遞歸算法得心應(yīng)手,寫(xiě)個(gè)網(wǎng)絡(luò)請(qǐng)求和下載管理輕車(chē)熟路,那么這應(yīng)該也可以解決。
- 面對(duì)下載好的代碼和資源文件,如何讓他一鍵加入到工程中,這個(gè)對(duì)很多人來(lái)說(shuō),可能略有麻煩,首先,你需要深入理解project.pbxproj內(nèi)部復(fù)雜的層級(jí)結(jié)構(gòu)以及定義的各種引用和依賴關(guān)系(不知道的看這里)。然后知道如何為每個(gè)文件生成唯一的UUID, 并在相應(yīng)project工程文件中添加文件信息,路徑信息等。這一步假設(shè)純徒手開(kāi)發(fā)的話,可能需要一定的時(shí)間和功底去研究,遇到的困難也會(huì)多一些。
- 那假設(shè)以上步驟,最終都看似跌跌撞撞,但總歸順利般的完成了。那也已經(jīng)很厲害了。
接下來(lái)。已有projectXXX,是我們的當(dāng)前的原始項(xiàng)目,他想依賴3-4個(gè)第三方庫(kù)(并已經(jīng)下載到本地),如何讓他可以找到這幾個(gè)三方庫(kù)。這一步同樣需要對(duì)project.pbxproj的操作,但是我們?cè)俚?步,已經(jīng)對(duì)這個(gè)操作游刃有余,這一步需要考慮的是Xcode中靜態(tài)庫(kù)/動(dòng)態(tài)庫(kù)與Project之前的依賴引用問(wèn)題。這一步的復(fù)雜與否,核心在于你對(duì)Xcode中workspace、Project、Target、Framework/Library之間的關(guān)系理解程度,如果很清晰的理解,就可以很快的實(shí)現(xiàn),如果理解不深,就需要研究清楚才能知道怎么做。最后的臨門(mén)一腳,就是基于我們之前已經(jīng)完成的這個(gè)一個(gè)個(gè)節(jié)點(diǎn),將這些節(jié)點(diǎn)串起來(lái)成為一條完成的workflow
5.2 關(guān)于Workspace/Target依賴的想法
說(shuō)一下,自己對(duì)最后一步的理解。需求是: 已有projectXXX,是我們的當(dāng)前的原始項(xiàng)目,他想依賴3-4個(gè)第三方庫(kù)(并已經(jīng)下載到本地),如何讓他可以找到這幾個(gè)三方庫(kù)。
如果頭腦風(fēng)暴一下,可以產(chǎn)生一些基本思路:
方案A:
我們可以直接將三方庫(kù)的代碼加入到項(xiàng)目中,完全避開(kāi)Target的概念,就像開(kāi)發(fā)者自己多寫(xiě)了幾個(gè)文件一樣,要用到這些代碼,直接import即可(如果是swift,甚至都不用import,直接用)
方案B:
建立一個(gè)新的Target, 這個(gè)Target直接添加在原始項(xiàng)目中,其內(nèi)部添加所有的第三方庫(kù)。
方案C:
為每個(gè)第三方庫(kù)都建議一個(gè)獨(dú)立的Target,然后這些Target再統(tǒng)一與原始項(xiàng)目建立依賴關(guān)系。
方案D:
為每個(gè)Target建立一個(gè)獨(dú)立的Project,基于workspace對(duì)于所有proj建立依賴關(guān)系。
方案E:
...
很顯然,方案A過(guò)于簡(jiǎn)單粗暴,且不方便后續(xù)的第三方庫(kù)的版本管理,不是一個(gè)很好的選擇。方案B是個(gè)可接受的方案,但是所有的第三方庫(kù)都在一個(gè)Target中,那么需要很復(fù)雜的去修改config building中Link Binary With Libraries以及部分search path,所有第三方庫(kù)都只能共用同一個(gè)target的config,擴(kuò)展性受限。那對(duì)比方案C和方案D, 先說(shuō)方案D,假設(shè)我們引用了20多個(gè)第三方庫(kù),那么就有20多個(gè)project,這個(gè)想想是個(gè)多么可怕的事情,所有projec直接的引用和依賴關(guān)系,以及整個(gè)項(xiàng)目的復(fù)雜度,都會(huì)復(fù)雜很多,所以方案D顯然有點(diǎn)“用力過(guò)猛”。那么方案C是看似可行的一個(gè)方案,每個(gè)第三方庫(kù)建里一個(gè)自己的Target的好處是,每個(gè)庫(kù)可以配置自己的config,然后target彼此之間的依賴關(guān)系,可以通過(guò)配置Build Phases中的Target Dependencies實(shí)現(xiàn)。那么接下來(lái)的問(wèn)題是這些Target放到哪里?
最簡(jiǎn)單的是將這些Target是全部放到原始的Project中,雖然可行,但是和原始Project耦合度太高,尤其是對(duì)于原始Project本身就有多個(gè)自有Target的場(chǎng)景,所以更好的策略是: 可以參考方案D,我們?cè)诟叩膶用嫔?,建立一個(gè)workspace,內(nèi)部新建一個(gè)第三方的Project,將這些第三方的Target統(tǒng)一放在一起。最后只需要將2個(gè)Project(原始項(xiàng)目的Project和我們新建的第三方Project)建立關(guān)聯(lián)就可以。目前CocoaPods實(shí)現(xiàn)的方式是,會(huì)建立一個(gè)Pods-xxxx你項(xiàng)目名xxx的Target,這個(gè)Target依賴了其他所有的第三方庫(kù),然后基于這個(gè)Target的framework:Pods_xxxx你項(xiàng)目名xxx.framework,會(huì)嵌入到原始Project的Link Binary With Libraries中。
簡(jiǎn)單點(diǎn)說(shuō),就是所有第三方庫(kù)都放在一個(gè)Pod的Project中,然后建立一個(gè)中間件Target,承上啟下,關(guān)聯(lián)起了2個(gè)Project之間的引用。
基于我們對(duì)方案C的擴(kuò)展實(shí)現(xiàn),最終串聯(lián)起來(lái)了整個(gè)第三方庫(kù)在項(xiàng)目中的依賴結(jié)構(gòu)。
想進(jìn)一步學(xué)習(xí),可以參看:
- 細(xì)聊 Cocoapods 與 Xcode 工程配置 : 講了一些介紹 Xcode 的工程配置,以及 target/project/workspace 等名詞的概念,cocoapods引用靜態(tài)庫(kù)方面的問(wèn)題等
- iOS開(kāi)發(fā)——?jiǎng)?chuàng)建你自己的Framework : 關(guān)于構(gòu)建一個(gè)framework有著很詳細(xì)很詳細(xì)很詳細(xì)的介紹
6. 有用的輪子
6.1 XcodeProject
研究完CocoaPods, 看看里面有用的輪子,首當(dāng)其沖的就是XcodeProject了。這里直接說(shuō)怎么用吧。
(注意: 以下都是ruby代碼哈)
6.1.1 讀取我們的project:
require 'xcodeproj'
project_path = '' # 工程的全路徑
project = Xcodeproj::Project.open(project_path)
6.1.2 讀取所有的Target
project.targets.each do |target|
puts target.name
end
6.1.3 group中添加文件
target = project.targets.first
group = project.main_group.find_subpath(File.join('testXcodeproj','newGroup'), true)
group.set_source_tree('SOURCE_ROOT')
# 文件加入到reference中
file_ref = group.new_reference("xxxPath/xx.h")
# 文件加入到build phase中 (.h文件只加到reference中即可,.m或者.swift文件還需要加入到build phase才能編譯)
target.add_file_references([file_ref])
project.save
6.1.4 引入framework或靜態(tài)庫(kù)
#添加xx.framework的引用
file_ref = project.frameworks_group.new_file('xxPath/xx.framework')
target.frameworks_build_phases.add_file_reference(file_ref)
#添加xx.a的引用
file_ref = project.frameworks_group.new_file('xxPath/xx.a')
target.frameworks_build_phases.add_file_reference(file_ref)
#添加xx.bundle的引用
file_ref = project.frameworks_group.new_file('xxPath/xx.bundle')
target.resources_build_phase.add_file_reference(file_ref)
project.save
進(jìn)一步學(xué)習(xí)可參考:
以上只介紹了XcodeProject這個(gè)輪子,當(dāng)然CocoaPods也有很多其他優(yōu)秀的輪子,以后如果用到了,我會(huì)在這里補(bǔ)充更新。
7. One more
Cocoapods 的親歷者說(shuō):The Road to CocoaPods 1.0
這個(gè)是CocoaPods的開(kāi)發(fā)者之一Samuel E. Giddins在2016年發(fā)布CocoaPods1.0版本時(shí)的一篇總結(jié)性博客,記錄了開(kāi)發(fā)中的一些小故事, 有興趣的同學(xué)可以去看看。
參考資料
- GitHub: CocoaPods 官方源碼
- GitHub: mjmsmith/pbxplorer
- Xcode Project File Format: 對(duì).pbxproj文件每個(gè)參數(shù)的詳細(xì)介紹
- CocoaPods 都做了什么?- 目前看過(guò)的博客里介紹的最深度的一篇
- XCode工程文件結(jié)構(gòu)及Xcodeproj框架的使用( 二 )
- 使用代碼為 Xcode 工程添加文件
- 添加iOS文件鏈接,操作.xcodeproj
- Xcode: Frameworks, By Thomas, January 23, 2017
- What is the difference between Embedded Binaries and Linked Frameworks
- XCODE, FRAMEWORKS, AND EMBEDDED FRAMEWORKS
- 細(xì)聊 Cocoapods 與 Xcode 工程配置
- 深入理解 CocoaPods
- CocoaPods建立自己的Podspec(三)
- Cocoapods原理總結(jié) - 掘金
- Podfile.lock背后的那點(diǎn)事 - 2015 Startry
- XcodeProject的內(nèi)部結(jié)構(gòu)分析