稍有 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 的人很难相信上面的这些文本是代码的。数组
在介绍 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 中只有对象和方法,这两个概念,这也下降了咱们理解这门语言的复杂度:
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
了,早在几十年前的 Lisp 语言就有了 eval
这个方法,这个方法会将字符串当作代码来执行,也就是说 eval
模糊了代码与数据之间的边界。
> eval "1 + 2 * 3"
=> 7复制代码
有了 eval
方法,咱们就得到了更增强大的动态能力,在运行时,使用字符串来改变控制流程,执行代码;而不须要去手动解析输入、生成语法树。
在咱们对 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复制代码
由于这里的 source
、platform
、target
以及 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 的实现部分了。
在上面简单介绍了 Ruby 的一些语法以及如何解析 Podfile 以后,咱们开始深刻了解一下 CocoaPods 是如何管理 iOS 项目的依赖,也就是 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 install
和 update
之间最大的区别,其中后者会无视已有的 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 章节所介绍的差很少,整个过程主要都是由 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!
就会使用这些信息安装当前工程的依赖,而整个安装依赖的过程大约有四个部分:
Pods.xcodeproj
工程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
就是用于解决依赖关系的类。
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 的下载流程。
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
工程这几件事情都离不开 CocoaPods 的另一个组件 Xcodeproj,这是一个能够操做一个 Xcode 工程中的 Group 以及文件的组件,咱们都知道对 Xcode 工程的修改大多数状况下都是对一个名叫 project.pbxproj
的文件进行修改,而 Xcodeproj 这个组件就是 CocoaPods 团队开发的用于操做这个文件的第三方库。
最后的这一部分与生成 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