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源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索