介紹
CocoaPods plugin which allows you to generate a framework or static library from a podspec.
This plugin is for CocoaPods developers, who need to distribute their Pods not only via CocoaPods, but also as frameworks or static libraries for people who do not use Pods.
作為 CococaPods 的官方插件之一,CocoaPods Packager 為 Pod 提供了 package 命令來(lái)生成 framework or static library。你可以僅憑一個(gè) podspec 文件就能完成一個(gè) framework 或 library 的生成。
那 Packager 的使用場(chǎng)景有哪些?網(wǎng)上有特別多詳細(xì)的使用說(shuō)明文檔,而分析實(shí)現(xiàn)的比較少。這也是這篇文章的初衷,想知道它是如何 work 的。本身插件也不是很復(fù)雜,這里會(huì)更挖一些細(xì)節(jié)。之前簡(jiǎn)單寫(xiě)了一篇文章:淺析 Cocoapods-Binary 實(shí)現(xiàn),主要是這兩個(gè)插件的核心邏輯類(lèi)似,感興趣的可以拿來(lái)對(duì)比。它們的區(qū)別在于 package 的對(duì)象是針對(duì)單個(gè) pod 還是整個(gè) project 的。
CocoaPods Plugins
這次我們先簡(jiǎn)單談一談 CococaPods 的插件原理。作為一款社區(qū)產(chǎn)品,僅有的少數(shù)核心開(kāi)發(fā)者是無(wú)法滿足大量開(kāi)發(fā)者的各種需求。因此,在 2013 CcocoaPods 開(kāi)始支持 Plugins 擴(kuò)展,使得大家可以自行支持所需要的功能。
What can CocoaPods Plugins do?
- 可以 Hook pod install 過(guò)程,包括 pre_install 與 post_install;
- 可以添加新的 pod 命令;
- Ruby 作為 dynamic language,你完全可以隨心所欲;
RubyGems
開(kāi)始解釋源碼錢(qián),需要說(shuō)一說(shuō)包管理,不得不提 RubyGems 和 Bundler。CocoaPods 背后的原型就是基于它們倆,當(dāng)然也會(huì)參照其他語(yǔ)言的包管理工具如 npm、Gradle。
RubyGems is a hosted ruby library service. It centralizes where you look for a library, and installing ruby libraries / apps.
Bundler provides a consistent environment for Ruby projects by tracking and installing the exact gems and versions that are needed.
Bundler is an exit from dependency hell, and ensures that the gems you need are present in development, staging, and production. Starting work on a project is as simple as bundle install.
RubyGems 是為 ruby library 提供集中代碼托管的服務(wù)。Bundler 則是針對(duì)當(dāng)前項(xiàng)目來(lái)管理 Gem 版本的工具,
Bundler 依據(jù)項(xiàng)目中的 Gemfiles 文件來(lái)管理 Gem,就好比 CocoaPods 通過(guò) Podfile 來(lái)管理 Pod 的版本一樣。Gemfile 長(zhǎng)這樣:
source 'https://gems.example.com' do
gem 'cocoapods', '1.8.4'
gem 'another_gem', :git => 'https://looseyi.github.io.git', :branch => 'master'
end
很熟悉的感覺(jué),有木有。是的,Podfile 的 DSL 和 Gemfile 如出一轍。
那什么情況會(huì)用到 Gemfile 呢?比如,公司級(jí)項(xiàng)目中可以通過(guò) gemfile 來(lái)統(tǒng)一 CocoaPods 的版本,不然大家各自為政會(huì)導(dǎo)致提交代碼會(huì)因?yàn)?CocoaPods 版本不同導(dǎo)致對(duì)項(xiàng)目的配置產(chǎn)生各種差異,導(dǎo)致最終的 PR 有大量 conflict 或 change,當(dāng)然還可以管理 CocoaPods 的插件版本,可以指向你自己的定制版本。
Bundle 的使用也很簡(jiǎn)單,在 gem install bundler
后,通過(guò)添加 bundle exec
前綴來(lái)執(zhí)行 pod 命令。這時(shí)會(huì)讀取安裝在本地 .bundle/ 目錄或全局目錄下所指定的 Gem 包來(lái)執(zhí)行 pod 命令。
bundle install #安裝 gemfile 中的包
bundle exec pod install
The software package is called a “gem” which contains a packaged Ruby application or library.
Gem 則是包含 Ruby 代碼的 application 或者 library,而它就是我們今天的主角,CocoaPods Plugin 背后的支柱。應(yīng)該說(shuō) CocoaPods Plugin 本質(zhì)上就是 Gem。
那我們來(lái)看一眼 Gem 的文件結(jié)構(gòu):
tree CocoaPods -L 2
CocoaPods
├── Rakefile
├── cocoapods.gemspec
├── bin
│ ├── pod
│ └── sandbox-pod
├── lib
│ ├── cocoapods
│ ├── ...
└── spec
│ ├── cocoapods-integration-specs
│ ...
- bin:可執(zhí)行文件目錄,當(dāng) gem install 的時(shí)候,會(huì)被加載到用戶的 PATH 路徑下;
- lib:gem 的源代碼目錄;
- sepc:gem 的測(cè)試代碼目錄;
- Rakefile:是自動(dòng)化測(cè)試程序 rake 的配置文件,也可用于生成代碼或者其他任務(wù);
- gemspec:描述了 gem 的關(guān)鍵信息,下面會(huì)提到;
The gemspec specifies the information about a gem such as its name, version, description, authors and homepage.
既然 CocoaPods 也是 Gem,它的 GemSpec 包含哪些信息呢:
Gem::Specification.new do |s|
s.name = "cocoapods"
s.version = Pod::VERSION
s.files = Dir["lib/**/*.rb"] + %w{ bin/pod bin/sandbox-pod README.md LICENSE CHANGELOG.md }
s.executables = %w{ pod sandbox-pod }
s.require_paths = %w{ lib }
s.add_runtime_dependency 'cocoapods-core', "= #{Pod::VERSION}"
s.add_runtime_dependency 'claide', '>= 1.0.2', '< 2.0'
s.add_runtime_dependency 'xcodeproj', '>= 1.14.0', '< 2.0'
...
end
依據(jù)那么熟悉的感覺(jué),如果你有搞過(guò) Pod library 的話。PodSpec 類(lèi)比 Gemspec,就不多介紹了。
CocoaPods Plugins
了解完 Gem,再看 Plugins 會(huì)清晰很多。作為 CocoaPods 的 Plugin,CocoaPods 為我們提供了方便生成 plugin 模版的命令。
pod plugins create NAME [TEMPLATE_URL]
生成 plugin 模版的文件目錄與 gem 相差無(wú)幾,這里直接貼 cocoapods-packager 的文件目錄:
cocoapods-packager
├── Gemfile
├── Rakefile
├── cocoapods-packager.gemspec
├── lib
│ ├── cocoapods-packager
│ ├── cocoapods_packager.rb
│ ├── cocoapods_plugin.rb
│ └── pod
└── spec
├── command
├── fixtures
├── integration
├── spec_helper.rb
└── unit
...
確實(shí)沒(méi)啥特別的,只不過(guò)會(huì)在 gemfile 中,幫你指定好 cocoapods 的依賴,以及根據(jù)提供的 NAME 來(lái)生成通用文件。
CocoaPods Packager
接下來(lái)我們提到的文件都是在 lib 文件夾下,即 Packager 的源代碼所在地。開(kāi)始前照例看一下腦圖,有一個(gè)整體的認(rèn)識(shí):
這里基本對(duì)應(yīng)了 Packager 的主要文件,作者已經(jīng)分的比較清楚了,整個(gè) Package 的入口是 /lib/pod/command/package.rb
文件,對(duì)應(yīng)腦圖的 Command Run 分類(lèi)。各個(gè)分類(lèi)下面對(duì)應(yīng)的就是各個(gè)文件內(nèi)部提供的主要方法,接下來(lái)我們就從 Package 文件說(shuō)起。
Package
本文 Demo 所展示的是基于 Packager 內(nèi)部提供的測(cè)試 spec 來(lái)做示例,啟動(dòng)命令如下:
bundle exec pod package ${workspaceRoot}/cocoapods-packager/spec/fixtures/KFData.podspec --dynamic
PS:調(diào)試所用的工具是 VSCode,如果你沒(méi)有用過(guò),絕對(duì)不要錯(cuò)過(guò)了。
執(zhí)行上面代碼前,我們需要先在 package.rb
的 initialize
方法內(nèi)打個(gè)斷點(diǎn)。然后我們來(lái)看 Package 類(lèi):
module Pod
class Command
class Package < Command
# functions ...
end
end
Package Command 繼承自 CocoaPods 內(nèi)部所提供的命令工具模塊 CLAide::Command。所有擴(kuò)展 Pod 的命令都需要繼承它,同時(shí)需要重載它的 options
、validate
、initialize
和 run
四個(gè)方法。
options
A list of option name and description tuples.
執(zhí)行命令所需的可選參數(shù),是 [Array<Array>] 的元組,由參數(shù)和對(duì)應(yīng)的描述組成,選項(xiàng)如下:
def self.options
[
['--force', 'Overwrite existing files.'],
['--no-mangle', 'Do not mangle symbols of depedendant Pods.'],
['--embedded', 'Generate embedded frameworks.'],
['--library', 'Generate static libraries.'],
['--dynamic', 'Generate dynamic framework.'],
['--local', 'Use local state rather than published versions.'],
['--bundle-identifier', 'Bundle identifier for dynamic framework'],
['--exclude-deps', 'Exclude symbols from dependencies.'],
['--configuration', 'Build the specified configuration (e.g. Debug). Defaults to Release'],
['--subspecs', 'Only include the given subspecs'],
['--spec-sources=private,https://github.com/CocoaPods/Specs.git', 'The sources to pull dependent ' \
'pods from (defaults to https://github.com/CocoaPods/Specs.git)']
]
end
validate
校驗(yàn)所傳參數(shù)有效性,如果參數(shù)中帶有 --help
選項(xiàng),則會(huì)直接拋出幫助提示。它會(huì)在 run 方法執(zhí)行前被調(diào)用。重載前需要先調(diào)用 super
,代碼如下:
def validate!
super
help! 'A podspec name or path is required.' unless @spec
help! 'podspec has binary-only depedencies, mangling not possible.' if @mangle && binary_only?(@spec)
help! '--bundle-identifier option can only be used for dynamic frameworks' if @bundle_identifier && !@dynamic
...
end
initialize
解析參數(shù),然后初始化打包所需的變量。這里著重介紹幾個(gè)核心參數(shù):
- @config:xcodebuild configuration,值為 Debug、Release,當(dāng)然也可以自定義,默認(rèn)為 Release;
- @package_type:所生成的包類(lèi)型,可以是靜態(tài)庫(kù)或者動(dòng)態(tài)庫(kù),默認(rèn)為 static_framework;
- @subspecs:支持所打出的包僅包含部分的 subspec,如果對(duì) subspes 有興趣,請(qǐng)轉(zhuǎn)官方文檔;
- @exclude_deps:打包的時(shí)候踢除 dependencies 的符號(hào)表,配合
--no-mangle
一起使用,解決靜態(tài)庫(kù)打包時(shí)有其他靜態(tài)庫(kù)依賴的問(wèn)題; - @mangle:是否開(kāi)自動(dòng)修改類(lèi)名等符號(hào),默認(rèn)開(kāi)啟。
這里稍微展開(kāi)一下 @package_type ,它其實(shí)是由多個(gè)參數(shù)來(lái)決定的,--embeded
、--dynamic
、--library
:
if @embedded
:static_framework
elsif @dynamic
:dynamic_framework
elsif @library
:static_library
else
:static_framework
end
可能有部分同學(xué)對(duì)于 framework 和 libray 的概念比較模糊,這里再說(shuō)一句,詳細(xì)可以看 Apple Doc:
A framework is a hierarchical directory that encapsulates shared resources, such as a dynamic shared library, nib files, image files, localized strings, header files, and reference documentation in a single package.
簡(jiǎn)單來(lái)說(shuō) framework 就一文件夾,你要說(shuō)和 Bundle 一樣也無(wú)妨。它是靜態(tài)庫(kù)還是動(dòng)態(tài)庫(kù),重點(diǎn)看它所包含的 library 類(lèi)型。如果包含的是 static share library 那就是 static_framework。如果包含的是 dynamic share library 則是 dynamic_framework。而我們平時(shí)說(shuō)的 libxxx.a
則是靜態(tài)庫(kù)。
run
if @spec.nil?
help! "Unable to find a podspec with path or name `#{@name}`."
return
end
target_dir, work_dir = create_working_directory
return if target_dir.nil?
build_package
`mv "#{work_dir}" "#{target_dir}"`
Dir.chdir(@source_dir)
run
方法作為入口比較簡(jiǎn)單,首先就是檢查是否取到所要編譯的 podspec 文件。然后針對(duì)它創(chuàng)建對(duì)應(yīng)的 working_directory
和 target_directory
。
working_directory 為打包所在的臨時(shí)目錄,like this:
/var/folders/zn/1p8f0yls66b5788lshsjrd6c0000gn/T/cocoapods-rp699asa
target_directory 為最終生成 package 的所在目錄,是當(dāng)前 source code 目錄下新開(kāi)的:
${workspaceRoot}/KFData-1.0.5
在 create_working_directory
后,會(huì)主動(dòng)切換當(dāng)前 ruby 的運(yùn)行目錄至 working_directory
目錄下,正式開(kāi)始編譯。build_package
結(jié)束后,將編譯產(chǎn)物 copy 到 target_directory
下,同時(shí)切換回最初執(zhí)行命令所在目錄 @source_dir
。
build_package
builder = SpecBuilder.new(@spec, @source, @embedded, @dynamic)
newspec = builder.spec_metadata
@spec.available_platforms.each do |platform|
build_in_sandbox(platform)
newspec += builder.spec_platform(platform)
end
newspec += builder.spec_close
File.open(@spec.name + '.podspec', 'w') { |file| file.write(newspec) }
首先,創(chuàng)建一個(gè) SpecBuilder,其作用是用于生成描述最終產(chǎn)物的 podspec 文件,SpecBuilder 就是一個(gè)模版文件生成器。bundler 調(diào)用 spec_metadata
方法遍歷指定的 podspec
文件復(fù)刻出對(duì)應(yīng)的配置并返回新生成的 podspec
文件。
然后,根據(jù) target 所支持的 platform,iOS / Mac / Watch 依次執(zhí)行 build_in_sandbox
編譯,同時(shí)將 platform 信息寫(xiě)入 newspec,以 iOS 為例:
s.ios.deployment_target = '8.0'
s.ios.vendored_framework = 'ios/A.embeddedframework/A.framework'
最后,將 podspec
寫(xiě)入 target_directory
編譯結(jié)束。
build_in_sandbox
config.installation_root = Pathname.new(Dir.pwd)
config.sandbox_root = 'Pods'
static_sandbox = build_static_sandbox(@dynamic)
static_installer = install_pod(platform.name, static_sandbox)
if @dynamic
dynamic_sandbox = build_dynamic_sandbox(static_sandbox, static_installer)
install_dynamic_pod(dynamic_sandbox, static_sandbox, static_installer, platform)
end
begin
perform_build(platform, static_sandbox, dynamic_sandbox, static_installer)
ensure # in case the build fails; see Builder#xcodebuild.
Pathname.new(config.sandbox_root).rmtree
FileUtils.rm_f('Podfile.lock')
end
Config
首先, config 與 initialize 的屬性 @config 完全是兩個(gè)東西,@config 只是一個(gè) 'Debug' or 'Release' 的字符串,而這里的 config 是 Pod::Config
的實(shí)例,默認(rèn)讀取自 ~/.cocoapods/config.yaml
目錄下的配置。
Package 在這里指定了 config 的安裝目錄 working_dirctory
和沙盒目錄 ./Pods
。Config 類(lèi)是保存在 /path/to/cocoapods/lib/cocoapods/config.rb
中。它非常重要,包括我們 CocoaPods 平時(shí)的 install 過(guò)程以及這個(gè) Package 的 build 過(guò)程均是讀取的全局的 config。作為全局需要訪問(wèn)的對(duì)象,一定是個(gè) share instance 咯。在 CocoaPods 中是這么實(shí)現(xiàn)的;
def self.instance
@instance ||= new
end
module Mixin
def config
Config.instance
end
end
各個(gè)模塊通過(guò) include 來(lái)引入,比如 Command 類(lèi)中:
include Config::Mixin
然后就能愉快的使用了。
有了 config 之后,就可以創(chuàng)建沙盒來(lái)執(zhí)行 pod install。install 過(guò)程會(huì)針對(duì) static 和 dynamic 分別 install 一次。最后,基于安裝后的工程,創(chuàng)建 builder 開(kāi)始最終的構(gòu)建操作。沙盒創(chuàng)建和 install 是在 pod_utils.rb
,build 在 builder.rb
中,接下來(lái)會(huì)單獨(dú)展開(kāi)。
Pod Utils
整個(gè) pods_utils 文件均聲明為 Package 的 private 方法,主要做的是 build sandbox 和 pod install。install 會(huì)區(qū)分 static 和 dynamic。按照 build_in_sandbox
調(diào)用順序展開(kāi)聊聊。
build_static_sandbox
通過(guò) Pathname 先生成 static_sandbox_root
,然后返回 Sandbox.new(static_sandbox_root)
。static_sandbox_root 會(huì)根據(jù)參數(shù) dynamic 來(lái)判斷,是否需要?jiǎng)?chuàng)建二級(jí)目錄 /static
:
if dynamic
Pathname.new(config.sandbox_root + '/Static')
else
Pathname.new(config.sandbox_root)
end
這么區(qū)分是由于,如果為動(dòng)態(tài)庫(kù),還會(huì)生成一個(gè) dynamic sandbox,其 path 為:
dynamic_sandbox_root = Pathname.new(config.sandbox_root + '/Dynamic')
介紹一下 SandBox ,其定義如下:
The sandbox provides support for the directory that CocoaPods uses for an installation. In this directory the Pods projects, the support files and the sources of the Pods are stored.
所以,sandbox 就是用于管理 pod install 目錄。
install_pod
既然是 pod install 當(dāng)然需要來(lái)一個(gè) podfile 了。package 會(huì)根據(jù)指定的 spec 來(lái)手動(dòng)創(chuàng)建 podfile。
def podfile_from_spec(path, spec_name, platform_name, deployment_target, subspecs, sources)
options = {}
if path
if @local
options[:path] = path
else
options[:podspec] = path
end
end
options[:subspecs] = subspecs if subspecs
Pod::Podfile.new do
sources.each { |s| source s }
platform(platform_name, deployment_target)
pod(spec_name, options)
install!('cocoapods', integrate_targets: false, deterministic_uuids: false)
target('packager') do
inherit! :complete
end
end
end
我們知道 Podfile 作為 DSL 文件,其每一行配置背后都對(duì)應(yīng)具體的方法,上面的這個(gè)就是簡(jiǎn)化版的 podfile 對(duì)應(yīng)的命令,options 就是我們配置的 pod
方法所需的參數(shù),對(duì)應(yīng)的是:pod(spec_name, options)
。反轉(zhuǎn)過(guò)來(lái)對(duì)對(duì)應(yīng)的 podfile DSL 應(yīng)該是這樣的:
platform :ios, '8.0'
target 'packager' do
pod "#{spec_name}", :path => "#{path}"# or :podspec => "#{path}"
end
這里如果參數(shù)中指定了 path 則不會(huì)通過(guò) spec 指定的 source
去download 源碼下來(lái)。
接著就是 pod install 了:
# podfile = podfile_from_spec
...
static_installer = Installer.new(sandbox, podfile)
static_installer.install!
unless static_installer.nil?
static_installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['CLANG_MODULES_AUTOLINK'] = 'NO'
config.build_settings['GCC_GENERATE_DEBUGGING_SYMBOLS'] = 'NO'
end
end
static_installer.pods_project.save
end
static_installer
這里重點(diǎn)就是 build_settings 的兩個(gè)配置。當(dāng) static_installer install 成功后會(huì)修改它們,作用是為了支持后續(xù)dynamic_framework 的編譯。GCC_GENERATE_DEBUGGING_SYMBOLS
這個(gè) config 應(yīng)該都知道吧?
我們來(lái)看另一個(gè) CLANG_MODULES_AUTOLINK,在 stackoverflow 上有一個(gè)解釋:
When you create a new project, Xcode will set the
Link Frameworks Automatically
flag to YES. Then the clang linking flag will be set toCLANG_MODULES_AUTOLINK = YES
. With this option, clang will linking the framework for you automatically.
就是為了避免 install 后,項(xiàng)目 autolink。
install_dynamic_pod
build_dynamic_sandbox
前面已經(jīng)說(shuō)過(guò)了,我們直接看動(dòng)態(tài)庫(kù)的 install:
# 1 Create a dynamic target for only the spec pod.
dynamic_target = build_dynamic_target(dynamic_sandbox, static_installer, platform)
# 2. Build a new xcodeproj in the dynamic_sandbox with only the spec pod as a target.
project = prepare_pods_project(dynamic_sandbox, dynamic_target.name, static_installer)
# 3. Copy the source directory for the dynamic framework from the static sandbox.
copy_dynamic_target(static_sandbox, dynamic_target, dynamic_sandbox)
# 4. Create the file references.
install_file_references(dynamic_sandbox, [dynamic_target], project)
# 5. Install the target.
install_library(dynamic_sandbox, dynamic_target, project)
# 6. Write the actual .xcodeproj to the dynamic sandbox.
write_pod_project(project, dynamic_sandbox)
內(nèi)部分了 6 個(gè)方法,還附帶了詳細(xì)的注釋。需要注意的是,整個(gè) install_dynamic_pod 是基于 install_pod 所生成的 static_installer
來(lái)修改的。這里比較復(fù)雜,一步步來(lái)。
build_dynamic_target
# 1
spec_targets = static_installer.pod_targets.select do |target|
target.name == @spec.name
end
static_target = spec_targets[0]
# 2
file_accessors = create_file_accessors(static_target, dynamic_sandbox)
# 3
archs = []
dynamic_target = Pod::PodTarget.new(dynamic_sandbox, true, static_target.user_build_configurations, archs, platform, static_target.specs, static_target.target_definitions, file_accessors)
dynamic_target
通過(guò) select
static_installer.pod_targets
篩選出 static_target,所過(guò)濾掉的是 target 是 spec 的第三方依賴。這里是過(guò)濾了QueryKit
;-
針對(duì)每個(gè) subspec 生成相應(yīng)的 FileAccessory :
<Pod::Sandbox::FileAccessor spec=KFData platform=osx root=...> <Pod::Sandbox::FileAccessor spec=KFData/Attribute platform=osx root=...> <Pod::Sandbox::FileAccessor spec=KFData/Core platform=osx root=...> <Pod::Sandbox::FileAccessor spec=KFData/Essentials platform=osx root=...> <Pod::Sandbox::FileAccessor spec=KFData/Manager platform=osx root=...> <Pod::Sandbox::FileAccessor spec=KFData/Store platform=osx root=...>
創(chuàng)建 PodTarget。
prepare_pods_project
# Create a new pods project
pods_project = Pod::Project.new(dynamic_sandbox.project_path)
# Update build configurations
installer.analysis_result.all_user_build_configurations.each do |name, type|
pods_project.add_build_configuration(name, type)
end
# Add the pod group for only the dynamic framework
local = dynamic_sandbox.local?(spec_name)
path = dynamic_sandbox.pod_dir(spec_name)
was_absolute = dynamic_sandbox.local_path_was_absolute?(spec_name)
pods_project.add_pod_group(spec_name, path, local, was_absolute)
pods_project
創(chuàng)建 Pod::Project,然后將 static project 中的 user configuration 復(fù)制過(guò)來(lái),最后為 target 新建 Pods Group,這里的 group 是 PBXGroup。什么是 PBXGroup 呢?看下面的分組就明白的了:
我們平時(shí)說(shuō)看到的 Dependencies、Frameworks、Pods、Products、Target Support Files 他們?cè)?CocoaPods 中都是對(duì)應(yīng)了 PBXGroup。
copy_dynamic_target
從 static sandbox 中 cp 到 dynamic sandbox 目錄下。
install_file_references
installer = Pod::Installer::Xcode::PodsProjectGenerator::FileReferencesInstaller.new(dynamic_sandbox, pod_targets, pods_project)
installer.install!
通過(guò) FileReferencesInstaller 為 dynamic target 生成文件引用,為最后的 project 寫(xiě)入做準(zhǔn)備。FileReferencesInstaller 說(shuō)明:
Controller class responsible of installing the file references of the specifications in the Pods project.
install_library
將 dynamic_target 寫(xiě)入新建的 project,同時(shí)會(huì) install 依賴的 system framework。
write_pod_project
dynamic_project.pods.remove_from_project if dynamic_project.pods.empty?
dynamic_project.development_pods.remove_from_project if dynamic_project.development_pods.empty?
dynamic_project.sort(:groups_position => :below)
dynamic_project.recreate_user_schemes(false)
# Edit search paths so that we can find our dependency headers
dynamic_project.targets.first.build_configuration_list.build_configurations.each do |config|
config.build_settings['HEADER_SEARCH_PATHS'] = "$(inherited) #{Dir.pwd}/Pods/Static/Headers/**"
config.build_settings['USER_HEADER_SEARCH_PATHS'] = "$(inherited) #{Dir.pwd}/Pods/Static/Headers/**"
config.build_settings['OTHER_LDFLAGS'] = '$(inherited) -ObjC'
end
dynamic_project.save
最后,將 project 寫(xiě)入 dynamic sandbox,修改 Search Path 保證能查詢到依賴的 header 引用。
Builder
繼 project 創(chuàng)建和 pod install 后,Package 中的 perform_build
先將所需參數(shù)傳入以創(chuàng)建 Builder,開(kāi)始執(zhí)行 builder.build(@package_type)
。build 會(huì)依據(jù) package_type 分為三種:
- build_static_library
- build_static_framework
- build_dynamic_framework
這三個(gè)方法區(qū)別其實(shí)不大,就是 framework 的方法多了資源 copy 的過(guò)程。這里以 build_dynamic_framework 為例。
build_dynamic_framework
defines = compile
build_sim_libraries(defines)
if @bundle_identifier
defines = "#{defines} PRODUCT_BUNDLE_IDENTIFIER='#{@bundle_identifier}'"
end
output = "#{@dynamic_sandbox_root}/build/#{@spec.name}.framework/#{@spec.name}"
clean_directory_for_dynamic_build
if @platform.name == :ios
build_dynamic_framework_for_ios(defines, output)
else
build_dynamic_framework_for_mac(defines, output)
end
copy_resources
首先是先執(zhí)行 compile
構(gòu)建 Pods-packager
,成功后返回 defines。compile 實(shí)現(xiàn)如下:
defines = "GCC_PREPROCESSOR_DEFINITIONS='$(inherited) PodsDummy_Pods_#{@spec.name}=PodsDummy_PodPackage_#{@spec.name}'"
defines << ' ' << @spec.consumer(@platform).compiler_flags.join(' ')
if @platform.name == :ios
options = ios_build_options
end
xcodebuild(defines, options)
if @mangle
return build_with_mangling(options)
end
defines
如果沒(méi)有指定 --no-mangle
則正常執(zhí)行 xcodebuild,否則轉(zhuǎn)入 build_with_mangling
。
build_with_mangling 內(nèi)部也是調(diào)用 xcodebuild,區(qū)別在于它生成的 defines 不同。mangling 會(huì)遍歷 sandbox build 目錄下的 libxxx.a 靜態(tài)庫(kù)來(lái)查找,核心方法如下:
defines = Symbols.mangle_for_pod_dependencies(@spec.name, @static_sandbox_root)
mangle.rb 的描述如下:
performs symbol aliasing,for each dependency:
? - determine symbols for classes and global constants
? - alias each symbol to Pod#{pod_name}_#{symbol}
? - put defines into
GCC_PREPROCESSOR_DEFINITIONS
for passing to Xcode
mangling 也稱為 namespacing,會(huì)把類(lèi)名和全局常量改成 Pod#{pod_name}_#{symbol}
的形式。以我們調(diào)試的 KFData 的 PodsDummy_KFData 類(lèi)為例:
no-mangle: PodsDummy_KFData
mangling: PodKFData_PodsDummy_KFData
就是統(tǒng)一在類(lèi)前面添加了前綴,目的是為了避免靜態(tài)庫(kù)中的符號(hào)表沖突。比如,我們打包的 KFData 依賴了 QueryKit 那生成的 libKFData.a 靜態(tài)庫(kù)會(huì)有一份 QueryKit 的 copy,而此時(shí)如果在主工程里也直接引用了 QueryKit 那就會(huì)產(chǎn)生類(lèi)似 duplicate symbols for architecture x86_64
的錯(cuò)誤。
你可以 nm
工具來(lái)查看 class 的 symbol:
nm -gU KFData.framework/KFData | grep "_OBJC_CLASS_\$.*KF.*"
在查這個(gè)問(wèn)題時(shí),還填補(bǔ)了一個(gè)疑惑很久的問(wèn)題:Why do cocoapod create a dummy class for every pod? CocoPods 上 issue_3410 也有詳細(xì)討論
build_sim_libraries
由于 simulator 自由 iOS 有,實(shí)現(xiàn)比較簡(jiǎn)單,參數(shù)是前面 compile 返回的 defines。
if @platform.name == :ios
xcodebuild(defines, '-sdk iphonesimulator', 'build-sim')
end
build_dynamic_framework_for_ios
# Specify frameworks to link and search paths
linker_flags = static_linker_flags_in_sandbox
defines = "#{defines} OTHER_LDFLAGS='$(inherited) #{linker_flags.join(' ')}'"
# Build Target Dynamic Framework for both device and Simulator
device_defines = "#{defines} LIBRARY_SEARCH_PATHS=\"#{Dir.pwd}/#{@static_sandbox_root}/build\""
device_options = ios_build_options << ' -sdk iphoneos'
xcodebuild(device_defines, device_options, 'build', @spec.name.to_s, @dynamic_sandbox_root.to_s)
sim_defines = "#{defines} LIBRARY_SEARCH_PATHS=\"#{Dir.pwd}/#{@static_sandbox_root}/build-sim\" ONLY_ACTIVE_ARCH=NO"
xcodebuild(sim_defines, '-sdk iphonesimulator', 'build-sim', @spec.name.to_s, @dynamic_sandbox_root.to_s)
# Combine architectures
`lipo #{@dynamic_sandbox_root}/build/#{@spec.name}.framework/#{@spec.name} #{@dynamic_sandbox_root}/build-sim/#{@spec.name}.framework/#{@spec.name} -create -output #{output}`
FileUtils.mkdir(@platform.name.to_s)
`mv #{@dynamic_sandbox_root}/build/#{@spec.name}.framework #{@platform.name}`
`mv #{@dynamic_sandbox_root}/build/#{@spec.name}.framework.dSYM #{@platform.name}`
上面貼的代碼看去很多,更多的是參數(shù)拼接的工作,主要做了三件事:
- xcodebuild 模擬器支持的 framework;
- xcodebuild 真機(jī)支持的 framework;
- lipo 合并??生成的 framework 輸出到
#{working_directory}/ios
;
build_dynamic_framework_for_mac 省去 simulator build 這一步,也就沒(méi)有 lipo 操作,更簡(jiǎn)單。這里就不列出了。
最后就是 resource、header、license 等相關(guān)資源的 copy。至此大致流程就結(jié)束了,用簡(jiǎn)單流程來(lái)回顧一下:
總結(jié)
cocoapods-packager 的邏輯還是比較簡(jiǎn)單的,整個(gè)熟悉過(guò)程困難還是在于對(duì) CocoaPods 的 API 和作用并不是和了解,導(dǎo)致很多調(diào)用需要查資料或者看 CocoaPods 源碼才能了大致理解。內(nèi)部實(shí)現(xiàn)還是圍繞 xcodebuild 來(lái)展開(kāi)的,通過(guò)生成 podfile 及 pod install 來(lái)構(gòu)建 project 環(huán)境,算是對(duì) CocoaPods 的深度集成了。