Ex2. Ruby 黑魔法 - eval 和 alias

本文做者:冬瓜
校對:Edmond

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 的編譯過程,他進行了精簡:

  1. 去除了 Preprocesser 的預編譯環節,保留了 Lexer 詞法分析和 Parser 語法分析,
  2. 利用 NSMethodSignature 封裝方法,結合遞歸降低,使用 Runtime 對方法進行消息轉發。

利用這種思路的還有另一個 OCRunner[3] 項目。

這些都是經過自制解釋器,實現 eval 特性,進而配合 libffi 來實現。

Ruby 中的 evalbinding

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_methodextend 、 refinement 等。若是後面 CocoaPods 有所涉及,咱們也會跟進介紹一些。

總結

本文經過 CocoaPods 中的兩個使用到的特性 Eval 和 Alias,講述了不少 Ruby 當中有意思的語法特性和元編程思想。Ruby 在衆多的語言中,由於注重思想和語法優雅脫穎而出,也讓我我的對語言有很大的思想提高。

若是你有經歷,我也強烈推薦你閱讀 SICP 和「Ruby 元編程」這兩本書,相信它們也會讓你在語言設計的理解上,有着更深的認識。從共性提煉到方法論,從語言昇華到經驗。

知識點問題梳理

這裏羅列了四個問題用來考察你是否已經掌握了這篇文章,你能夠在評論區及時回答問題與做者交流。若是沒有建議你加入收藏再次閱讀:

  1. REPL 的核心思想是什麼?與 Evaluation 特性有什麼關係?
  2. Ruby 中 eval 方法做用是什麼?Binding 對象用來幹什麼?
  3. Ruby 是否能夠實現 Method Swizzling 這種功能?
  4. Open Class 是什麼?環繞別名如何利用?

參考資料

[1]

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源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索