iOS 工程常常使用 CocoaPods 管理第三方库。只需要按规则写好 Podfile 文件,之后敲 pod install 命令,第三方库以及相关依赖都自动下载配置好,很方便。但此文不讨论 CocoaPods 工具如何使用,只讨论 Podfile 文件。
Podfile 有 source、target、pod 等等关键字,似乎是专门为管理第三方库设计的配置语言,但它其实采用 Ruby 语法。Podfile 是 DSL 的一个例子。
DSL
DSL 是 Domain Specific Language (领域特定语言) 的缩写。
DSL 是为解决某一个特定任务(领域),专门设计的计算机语言。比如排版是一个特定任务(有 TeX),字符串匹配是一个特定任务(有正则表达式),控制编译是一个特定任务(有 make、cmake),数据库查找是一个特定任务(有 SQL),都可以设计专门的语言。DSL 选择接近于特定任务的概念,概念甚至直接对应于某个关键字,因而解决特定领域的问题十分高效。但也正因为 DSL 选择特定的概念,当超出了领域范围时,在通用问题上反而表达能力有限。
外部和内部 DSL
DSL 通常需要跟某个宿主语言配合,不可单独使用。宿主语言集成一个解释器,解释调用 DSL,特定的问题就使用 DSL 来描述。
考虑 DSL 和宿主语言的关系,有两种不同的 DSL。假如 DSL 跟宿主语言是不同的,这个 DSL 就需要专门写一个解释器,称为外部 DSL。SQL、正则表达式都是外部 DSL。而假如 DSL 和宿主语言是相同的语言,有着相同的语法(或者 DSL 语法是宿主语言的子集),这种 DSL 称为叫内部 DSL。内部 DSL 语法需要跟宿主语言一样,语法上受到限制,但最大的好处是不用专门去写解释器。外部 DSL 需要自己写解释器,语法可以自由设计。
有些编程语言,本身语法很灵活(诡异),特别适合于写内部 DSL,Ruby 是其中之一。其它适合做内部 DSL 的语言还有 Lisp、Swift、C++ 等。任何语言都可以写 DSL,只是某些语言写起来麻烦一些。比如 Objective-C,语法算很规矩(死板)了,也可以写 Masonry 这种布局库。
有一本专门讨论 DSL 的书,可以读读。
Podfile 和 Ruby 语法
Podfile 就是内部 DSL。CocoaPods 是 Ruby 编写的,Podfile 也使用 Ruby 语法,是 Ruby 去解释 Ruby。
接下来,描述跟 Podfile 相关的一些 Ruby 语法。一个 Podfile 可能是这样写的:
platform :ios, '10.0'
use_frameworks!
source 'https://github.com/CocoaPods/Specs.git'
target 'MyApp' do
pod 'Masonry'
pod 'ObjectiveSugar', '~> 0.5'
pod 'AFNetworking', :git => 'https://github.com/gowalla/AFNetworking.git', :branch => 'dev'
target "MyAppTests" do
inherit! :search_paths
pod 'OCMock', '~> 2.0.1'
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
puts "#{target.name}"
end
endplatform
第一行
platform :ios, '10.0'在 Ruby 中,函数调用可以省略括号。上述语句其实是一个函数调用,相当于
platform(:ios, '10.0'):ios 这种语法在 Ruby 中叫符号(Symbol)。Ruby 区分了符号和字符串,符号内容不可变,字符串内容可变。因为符号内容并不可变,假如它们内容相同,就是同一个对象,共用同一份内存;但是字符串可变,就算它们内容相同,也是分离的两个对象。在 Ruby 中经常使用符号,特别是作为字典的 key。
翻查源码,platform 这个函数定义在 dsl.rb 中。
def platform(name, target = nil)
# Support for deprecated options parameter
target = target[:deployment_target] if target.is_a?(Hash)
current_target_definition.set_platform!(name, target)
endPodfile 中的 platform、target、source、pod 等看起来似乎是关键字的,都是函数。都定义在 dsl.rb 文件中,再转调 target_definition.rb 的实现。
use_frameworks!
同理 use_frameworks! 也是函数调用,只是省略了括号,相当于
use_frameworks!()对应的 Ruby 定义为
def use_frameworks!(flag = true)
current_target_definition.use_frameworks!(flag)
endRuby 的函数参数可以有默认值。这里不传参数,flag 就默认为 true。另外 Ruby 的名字可以带感叹号 !,表示强调,需要特别注意或者有某种危险。比如
- str.capitalize 把字符串转换为大写字母显示。
- str.capitalize! 与 capitalize 相同,但是 str 会发生变化并返回。
Ruby 的名字也可以带问号 ?,表示某种疑问,比如
- str.empty?,如果 str 为空(即长度为 0),则返回 true。
- str.eql?(other),如果两个字符串有相同的长度和内容,则这两个字符串相等。
target
target 'MyApp' do
pod 'Masonry'
pod 'ObjectiveSugar', '~> 0.5'
xxxx
endRuby 中,do ... end 构成了一个 Block, 实际上相当于
targe('MyApp') {
pod 'Masonry'
pod 'ObjectiveSugar', '~> 0.5'
xxxx
}Ruby 的 Block 回调有两种实现方式,
- 一种隐式 block ,使用 yield 回调
- 一种是参数前带 &, 使用 call 回调
比如
def test_block0(name, &block)
puts name
block.call() if block
end
def test_block1(name)
puts name
yield if block_given?
end
test_block0 'hello' do
puts "test_block0"
end
test_block1 'hello' do
puts "test_block1"
end两种实现方法,真正调用起来差不多。yield 通常用于一次性调用的 block, block 调用完就扔掉,不需要存储起来。而显式传递 &block,通常表示这个 block 需要存储起来,以后再使用。对于这种传递到函数中,直接调用,不需要存储起来的 block,yield 这种写法,效率会高一些。
Podfile 中的 target 就是函数,使用隐式 yield 实现 block 回调。而 post_install 需要将 block 存储起来,显式传递 &block。
pod
pod 有多种不同写法,比如上面例子中的
pod 'Masonry'
pod 'ObjectiveSugar', '~> 0.5'
pod 'AFNetworking', :git => 'https://github.com/gowalla/AFNetworking.git', :branch => 'dev'这里的 pod 同样也是函数,用了不固定参数。Ruby 中,假如最后一个参数,名字前面带星号 *,就表示可以传递不定参数。
def pod(name = nil, *requirements)
unless name
raise StandardError, 'A dependency requires a name.'
end
current_target_definition.store_pod(name, *requirements)
end调用时,requirements 可以当成一个数组处理,装着传进来的参数。
pod 'Masonry'调用时,name 为 'Masonry',requirements 数组为空。
pod 'ObjectiveSugar', '~> 0.5'调用时,name 为 'ObjectiveSugar',requirements 有一个数据,为 '~> 0.5'。
pod 'AFNetworking', :git => 'https://github.com/gowalla/AFNetworking.git', :branch => 'dev'{ :key1 => value1, :key2 => value2 } 这种语法是 Ruby 字典的写法。字典在函数最后时,可以省略大括号。而函数调用又省略了圆括号,就变成了上面的形式。上述调用相当于。
pod('AFNetworking', { :git => 'https://github.com/gowalla/AFNetworking.git', :branch => 'dev' })调用之后,requirements 装着一个字典。:git、:branch 这两个符号,作为字典的 key, 符号这种写法上面已经说过了。
可以看到 Ruby 的语法十分灵活,因而很适合写 DSL。Podfile 也是 Ruby, 在其中可以写任意的逻辑,定义自己的函数。
Podfile 的 target
Podfile 中的 target 分两种,一种是抽象的,不直接对应 Xcode 工程中的 target。一种是具体的 target, 名字跟 Xcode 工程的 target 相同。Podfile 中的 target 对应于 CocoaPods 代码中的 TargetDefinition。
最开始,生成名字为 'Pods' 的 TargetDefinition,作为 current_target_definition。之后 Podfile 中每次出现 target 关键字,相当于调用 target 函数,就会生成一个新的 TargetDefinition,作为新的 current_target_definition。 pod 这些关键字对应成函数,就是在操作这个 TargetDefinition。
而 target 和 target 之间也很自然形成父子树状关系。修改了父 target, 其中的 pod 可以应用到子 target 中。假如 Xcode 工程中 targetA、targetB 两个目标,用到某些第三方库是相同的。就可以嵌套在一个抽象 target 当中,只在其中配置一次。这样抽象 target 作为父 target, 参数也就应用到 targetA、targetB 中了。