CocoaPods 是使用 Ruby 這門腳本語言實現的工具。Ruby 有不少優質的特性被 CocoaPods 所利用,爲了在後續的源碼閱讀中不會被這些用法阻塞,因此在這個系列中,會給出一些 CocoaPods 的番外篇,來介紹 Ruby 及其當中的一些語言思想。git
今天這一篇咱們來聊聊 Ruby 中的一些十分「動態」的特性:eval 特性和 alias 特性。github
說說 Eval 特性
源自 Lisp 的 Evaluation
在一些語言中,eval
方法是將一個字符串看成表達式執行而返回一個結果的方法;在另一些中,eval
它所傳入的不必定是字符串,還有多是抽象句法形式,Lisp 就是這種語言,而且 Lisp 也是首先提出使用 eval
方法的語言,並提出了 Evaluation 這個特性。這也使得 Lisp 這門語言能夠實現脫離編譯這套體系而動態執行的結果。web
Lisp 中的 eval
方法預期是:將表達式做爲參數傳入到 eval
方法,並聲明給定形式的返回值,運行時動態計算。
編程
下面是一個 Lisp Evaluation 代碼的例子( Scheme[1] 方言 RRS 及之後版本):ruby
; 將 f1 設置爲表達式 (+ 1 2 3)
(define f1 '(+ 1 2 3))
; 執行 f1 (+ 1 2 3) 這個表達式,並返回 6
(eval f1 user-initial-environment)
可能你會以爲:這只是一個簡單的特性,爲何會稱做黑魔法特性?微信
由於 Evaluation 這種可 eval 特性是不少思想、落地工具的基礎。爲何這麼說,下面來講幾個很常見的場景。數據結構
REPL 的核心思想
若是你是 iOSer,你必定還會記得當年 Swift 剛剛誕生的時候,有一個主打的功能就是 REPL 交互式開發環境。編輯器
固然,做爲動態性十分強大的 Lisp 和 Ruby 也有對應的 REPL 工具。例如 Ruby 的 irb 和 pry 都是十分強大的 REPL。爲何這裏要說起 REPL 呢?由於在這個名字中,E 就是 eval 的意思。
函數
REPL 對應的英文是 Read-Eval-Print Loop。工具
-
Read 讀入一個來自於用戶的表達式,將其放入內存; -
Eval 求值函數,負責處理內部的數據結構並對上下文邏輯求值; -
Print 輸出方法,將結果呈現給用戶,完成交互。
REPL 的模型讓你們對於語言的學習和調試也有着增速做用,由於「Read - Eval - Print」 這種循環要比 「Code - Compile - Run - Debug」 這種循環更加敏捷。
在 Lisp 的思想中,爲了實現一個 Lisp REPL ,只須要實現這三個函數和一個輪循的函數便可。固然這裏咱們忽略掉複雜的求值函數,由於它就是一個解釋器。
有了這個思想,一個最簡單的 REPL 就可使用以下的形式表達:
# Lisp 中
(loop (print (eval (read))))
# Ruby 中
while [case]
print(eval(read))
end
簡單聊聊 HotPatch
大約在 2 年前,iOS 比較流行使用 JSPatch/RN 基於 JavaScriptCore 提供的 iOS 熱修復和動態化方案。其核心的思路基本都是下發 JavaScript 腳原本調用 Objective-C,從而實現邏輯注入。
JSPatch 尤爲被你們所知,須要編寫大量的 JavaScript 代碼來調用 Objective-C 方法,固然官方也看到了這一效率的窪地,並製做了 JSPatch 的語法轉化器來間接優化這一過程。
可是不管如何優化,其實最大的根本問題是 Objective-C 這門語言不具有 Evaluation 的可 eval 特性,假若擁有該特性,那其實就能夠跨越使用 JavaScript 作橋接的諸多問題。
咱們都知道 Objective-C 的 Runtime 利用消息轉發能夠動態執行任何 Objective-C 方法,這也就給了咱們一個啓示。假如咱們自制一個輕量級解釋器,動態解釋 Objective-C 代碼,利用 Runtime 消息轉發來動態執行 Objective-C 方法,就能夠實現一個「準 eval 方法」。
這種思路在 GitHub 上也已經有朋友開源出了 Demo - OCEval[2]。不一樣於 Clang 的編譯過程,他進行了精簡:
-
去除了 Preprocesser 的預編譯環節,保留了 Lexer 詞法分析和 Parser 語法分析, -
利用 NSMethodSignature
封裝方法,結合遞歸降低,使用 Runtime 對方法進行消息轉發。
利用這種思路的還有另一個 OCRunner[3] 項目。
這些都是經過自制解釋器,實現 eval 特性,進而配合 libffi 來實現。
Ruby 中的 eval
和 binding
Ruby 中的 eval 方法其實很好理解,就是將 Ruby 代碼以字符串的形式做爲參數傳入,而後進行執行。
str = 'Hello'
puts eval("str + ' CocoaPods'") # Hello CocoaPods
上面就是一個例子,咱們發現傳入的代碼 str + ' CocoaPods'
在 eval
方法中已經變成 Ruby 代碼執行,並返回結果 'Hello CocoaPods'
字符串。
在「Podfile 的解析邏輯」中講到, CocoaPods 中也使用了 eval
方法,從而以 Ruby 腳本的形式,執行了 Podfile
文件中的邏輯。
def self.from_ruby(path, contents = nil)
# ...
podfile = Podfile.new(path) do
begin
# 執行 Podfile 中的邏輯
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
固然,在 CocoaPods 中僅僅是用了 eval
方法的第一層,對於咱們學習者來講確定不能知足於此。
在 Ruby 中, Kernel
有一個方法 binding
,它會返回一個 Binding 類型的對象。這個 Binding 對象就是咱們俗稱的綁定,它封裝了當前執行上下文的全部綁定,包括變量、方法、Block 和 self
的名稱綁定等,這些綁定直接決定了面嚮對象語言中的執行環境。
那麼這個 Binding 對象在 eval
方法中怎麼使用呢?其實就是 eval
方法的第二個參數。這個在 CocoaPods 中運行 Podfile 代碼中並無使用到。咱們下面來作一個例子:
def foo
name = 'Gua'
binding
end
eval('p name', foo) # Gua
在這個例子中,咱們的 foo
方法就是咱們上面說的執行環境,在這個環境裏定義了 name
這個變量,並在方法體最後返回 binding
方法調用結果。在下面使用 eval
方法的時候,當作 Kernel#binding
入參傳入,即可以成功輸出 name
變量。
TOPLEVEL_BINDING
全局常量
在 Ruby 中 main
對象是最頂級範圍,Ruby 中的任何對象都至少須要在次做用域範圍內被實例化。爲了隨時隨地地訪問 main
對象的上下文,Ruby 提供了一個名爲 TOPLEVEL_BINDING
的全局常量,它指向一個封裝了頂級綁定的對象。便於理解,舉個例子:
@a = "Hello"
class Addition
def add
TOPLEVEL_BINDING.eval("@a += ' Gua'")
end
end
Addition.new.add
p TOPLEVEL_BINDING.receiver # main
p @a # Hello Gua
這段代碼中,Binding#receiver
方法返回 Kernel#binding
消息的接收者。爲此,則保存了調用執行上下文 - 在咱們的示例中,是 main
對象。
而後咱們在 Addition 類的實例中使用 TOPLEVEL_BINDING
全局常量訪問全局的 @a
變量。
總說 Ruby Eval 特性
以上的簡單介紹若是你曾經閱讀過 SICP(Structture and Interpretation of Computer Programs)這一神書的第四章後,必定會有更加深入的理解。
咱們將全部的語句看成求值,用語言去描述過程,用與被求值的語言相同的語言寫出的求值器被稱做元循環;eval 在元循環中,參數是一個表達式和一個環境,這也與 Ruby 的 eval
方法徹底吻合。
不得不說,Ruby 的不少思想,站在 SICP 的肩膀上。
相似於 Method Swizzling 的 alias
對於廣大 iOSer 必定都十分了解被稱做 Runtime 黑魔法的 Method Swizzling。這實際上是動態語言大都具備的特性。
在 iOS 中,使用 Selector 和 Implementation(即 IMP)的指向交換,從而實現了方法的替換。這種替換是發生在運行時的。
在 Ruby 中,也有相似的方法。爲了全面的瞭解 Ruby 中的 「Method Swizzling」,咱們須要瞭解這幾個關於元編程思想的概念:Open Class 特性與環繞別名。這兩個特性也是實現 CocoaPods 插件化的核心依賴。
Open Class 與特異方法
Open Class 特性就是在一個類已經完成定義以後,再次向其中添加方法。在 Ruby 中的實現方法就是定義同名類。
在 Ruby 中不會像 Objective-C 和 Swift 同樣被認爲是編譯錯誤,後者須要使用 Category 和 Extension 特殊的關鍵字語法來約定是擴展。而是把同名類中的定義方法所有附加到已定義的舊類中,不重名的增長,重名的覆蓋。如下爲示例代碼:
class Foo
def m1
puts "m1"
end
end
class Foo
def m2
puts "m2"
end
end
Foo.new.m1 # m1
Foo.new.m2 # m2
class Foo
def m1
puts "m1 new"
end
end
Foo.new.m1 # m1 new
Foo.new.m2 # m2
特異方法和 Open Class 有點相似,不過附加的方法不是附加到類中,而是附加到特定到實例中。被附加到方法僅僅在目標實例中存在,不會影響該類到其餘實例。示例代碼:
class Foo
def m1
puts "m1"
end
end
foo1 = Foo.new
def foo1.m2()
puts "m2"
end
foo1.m1 # m1
foo1.m2 # m2
foo2 = Foo.new
foo2.m1 # m1
# foo2.m2 undefined method `m2' for #<Foo:0x00007f88bb08e238> (NoMethodError)
環繞別名(Around Aliases)
其實環繞別名只是一種特殊的寫法,這裏使用了 Ruby 的 alias
關鍵字以及上文提到的 Open Class 的特性。
首先先介紹一下 Ruby 的 alias
關鍵字,其實很簡單,就是給一個方法起一個別名。可是 alias
配合上以前的 Open Class 特性,就能夠達到咱們所說的 Method Swizzling 效果。
class Foo
def m1
puts "m1"
end
end
foo = Foo.new
foo.m1 # m1
class Foo
alias :origin_m1 :m1
def m1
origin_m1
puts "Hook it!"
end
end
foo.m1
# m1
# Hook it!
雖然在第一個位置已經定義了 Foo#m1
方法,可是因爲 Open Class 的重寫機制以及 alias
的別名設置,咱們將 m1
已經修改爲了新的方法,舊的 m1
方法使用 origin_m1
也能夠調用到。如此也就完成了相似於 Objective-C 中的 Method Swizzling 機制。
總結一下環繞別名,其實就是給方法定義一個別名,而後從新定義這個方法,在新的方法中使用別名調用老方法。
猴子補丁(Monkey Patch)
既然說到了 alias
別名,那麼就順便說一下猴子補丁這個特性。猴子補丁區別於環繞別名的方式,它主要目的是在運行時動態替換並能夠暫時性避免程序崩潰。
先聊聊背景,因爲 Open Class 和環繞別名這兩個特性,Ruby 在運行時改變屬性已經十分容易了。可是若是咱們如今有一個需求,就是 **須要動態的進行 Patch ** ,而不是隻要 alias
就全局替換,這要怎麼作呢?
這裏咱們引入 Ruby 中的另外兩個關鍵字 refine
和 using
,經過它們咱們能夠動態實現 Patch。舉個例子:
class Foo
def m1
puts "m1"
end
end
foo = Foo.new
foo.m1 # m1
"""
定義一個 Patch
"""
module TemproaryPatch
refine Foo do
def m1
puts "m1 bugfix"
end
end
end
using TemproaryPatch
foo2 = Foo.new
foo2.m1 # m1 bugfix
上面代碼中,咱們先使用了 refine
方法從新定義了 m1
方法,定義完以後它並不會當即生效,而是在咱們使用 using TemporaryPatch
時,纔會生效。這樣也就實現了動態 Patch 的需求。
總說 alias 特性
Ruby 的 alias
使用實在是太靈活了,這也致使了 Ruby 很容易地實現插件化能力。由於全部的方法均可以經過環繞別名的方式進行 Hook ,從而實現本身的 Gem 插件。
除了以上介紹的一些擴展方式,其實 Ruby 還有更多修改方案。例如 alias_method
、 extend
、 refinement
等。若是後面 CocoaPods 有所涉及,咱們也會跟進介紹一些。
總結
本文經過 CocoaPods 中的兩個使用到的特性 Eval 和 Alias,講述了不少 Ruby 當中有意思的語法特性和元編程思想。Ruby 在衆多的語言中,由於注重思想和語法優雅脫穎而出,也讓我我的對語言有很大的思想提高。
若是你有經歷,我也強烈推薦你閱讀 SICP 和「Ruby 元編程」這兩本書,相信它們也會讓你在語言設計的理解上,有着更深的認識。從共性提煉到方法論,從語言昇華到經驗。
知識點問題梳理
這裏羅列了四個問題用來考察你是否已經掌握了這篇文章,你能夠在評論區及時回答問題與做者交流。若是沒有建議你加入收藏再次閱讀:
-
REPL 的核心思想是什麼?與 Evaluation 特性有什麼關係? -
Ruby 中 eval
方法做用是什麼?Binding 對象用來幹什麼? -
Ruby 是否能夠實現 Method Swizzling 這種功能? -
Open Class 是什麼?環繞別名如何利用?
參考資料
Scheme: https://zh.wikipedia.org/wiki/Scheme
[2]OCEval: https://github.com/lilidan/OCEval
[3]OCRunner: https://github.com/SilverFruity/OCRunner
本文分享自微信公衆號 - 一瓜技術(tech_gua)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。