Podfile、DSL 和 Ruby 語法
iOS 工程常常使用 CocoaPods 管理第三方庫。只需要按規則寫好 Podfile 檔案,之後敲pod install
命令,第三方庫以及相關依賴都自動下載配置好,很方便。但此文不討論 CocoaPods 工具的使用,只討論 Podfile 檔案。
很多人都知道 CocoaPods 是 Ruby 編寫的,但可能有些人沒有留意到,這個 Podfile 檔案本身也符合 Ruby 語法。Podfile 有自身 source、target、pod 等等關鍵字,看起來似乎是專門為管理第三方庫設計的語言。
Podfile 其實是 DSL 的一個例子。
DSL
DSL 是 Domain Specific Language (領域特定語言) 的縮寫。
DSL 簡單地說,就是為解決某一個特定任務(領域),專門設計的計算機語言。比如排版是一個特定任務(有 TeX),字串匹配是一個特定任務(有正則表示式),控制編譯是一個特定任務(有 make、cmake),資料庫查詢是一個特定任務(有 SQL),都可以設計出針對這個任務的語言。DSL 選擇接近於特定任務的概念,概念甚至直接對應於某個關鍵字,因而解決某個領域內的問題十分高效。但也正因為 DSL 選擇特定的概念,超出了領域範圍,在通用問題上,反而表達能力有限。
外部和內部 DSL
DSL 通常需要跟某個宿主語言配合,不可單獨使用。宿主語言中整合一個直譯器,解釋呼叫 DSL,特定的問題就使用 DSL 來描述。DSL 就為這種問題而生,描述起來自然也更容易。通過這種方式,就可以高效地解決特定問題。
考慮 DSL 和宿主語言的關係,有兩種不同的 DSL。假如 DSL 跟宿主語言是不同的,這個 DSL 就需要專門寫一個直譯器,稱為外部 DSL。SQL、正則表示式都是外部 DSL。而假如 DSL 和宿主語言是相同的語言,有著相同的語法(或者 DSL 語法是宿主語言的子集),這種 DSL 稱為叫內部 DSL。內部 DSL 語法需要跟宿主語言一樣,語法上就會受到限制,但最大的好處是不用專門去寫直譯器。外部 DSL 需要自己寫直譯器,語法可以自由設計。
有些程式語言,本身語法很靈活(詭異),特別適合於寫內部 DSL,Ruby 是其中之一。Podfile 就是內部 DSL,本身就是 Ruby 語法,是 Ruby 去解釋呼叫 Ruby。其它適合做內部 DSL 的語言還有 Lisp、Swift、C++ 等。任何語言都可以寫內部 DSL,只是某些語言寫起來麻煩一些。比如 Objective-C,語法算很規矩(死板)了,也可以寫 Masonry 這種 AutoLayout 佈局庫。
有一本專門討論DSL 的書,可以讀讀。
Podfile 和 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 end
platform
第一行
platform :ios, '10.0'
在 Ruby 中,函式呼叫可以省略括號。上述語句其實是一個函式呼叫,相當於
platform(:ios, '10.0')
:ios
這種語法在 Ruby 中叫符號(Symbol)。Ruby 區分了符號和字串,符號內容不可變,字串內容可變。因為符號內容並不可變,假如它們內容相同,就是同一個物件,共用同一份記憶體;但是字串可變,就算它們內容相同,也是分離的兩個物件。在 Ruby 中經常使用符號,特別是作為字典的 key。
翻查原始碼,platform
這個函式定義在ofollow,noindex">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) end
Podfile 中的 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) end
Ruby 的函式引數可以有預設值。這裡不傳引數,flag 就預設為 trun。另外 Ruby 的名字可以帶感嘆號 !,通常帶 ! 的函式表示強調,需要特別注意或者有某種危險。比如
- str.capitalize 把字串轉換為大寫字母顯示。
- str.capitalize! 與 capitalize 相同,但是 str 會發生變化並返回。
Ruby 的名字也可以帶問號 ?,表示某種疑問,通常返回一個 bool 值,比如
- str.empty?,如果 str 為空(即長度為 0),則返回 true。
- str.eql?(other),如果兩個字串有相同的長度和內容,則這兩個字串相等。
target
target 'MyApp' do pod 'Masonry' pod 'ObjectiveSugar', '~> 0.5' xxxx end
Ruby 中,do ... end
構成了一個 Block, 實際上相當於
targe('MyApp') { pod 'Masonry' pod 'ObjectiveSugar', '~> 0.5' xxxx }
Ruby 的 Block 回撥有兩種實現方式,
- 一種隱式 block ,使用 yield 回撥
- 一種是引數前帶 &, 使用 call 回撥
比如
def test_block0(name, █) 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 中了。