Ruby Singleton Class 與 Objective-C KVO
Ruby 是解釋強型別動態語言,Objective-C 是編譯弱型別(動態 & 靜態)語言,兩者看似沒什麼關聯,但是實際上可以說是師出同門,它們很大程度上繼承了 Smalltalk 的關鍵特性,所以很多設計理念是共通的,比如 Ruby 和 Objective-C 擁有相似的訊息傳遞機制 (dynamic message dispatch)、物件模型 (object model —— object class metaclass),並且都提供及其強大的執行時特性以及支撐執行時特性所需的介面等。 Ruby 和 Objective-C 的異同其實有挺多可以說的,但是本文不會過多地去探討,這裡只是窺探下 singleton class 和 KVO 兩個技術點間的聯絡。
初始設定
假設當前有 Animal 類,Dog 類繼承自 Animal,分別用 Ruby 和 Objective-C 建立 Dog 物件。
Ruby :
class Animal attr_accessor :name end class Dog < Animal def bark puts 'wangwang!' end end myDog = Dog.new myDog.bark # wangwang!
Objective-C:
@interface Animal : NSObject @property (copy, nonatomic) NSString *name; @end @implementation Animal @end @interface Dog : Animal - (void)bark; @end @implementation Dog - (void)bark { NSLog(@"wangwang!"); } + (void)clsBark { NSLog(@"wangwang!"); } @end int main(int argc, const char * argv[]) { @autoreleasepool { Dog *myDog = [Dog new]; [myDog bark]; // wangwang! } }
其中變數 myDog 指向的物件(下文為了方便統一用 myDog 物件描述)無法像 Dog 類物件一樣,建立其他物件,所以這裡將 myDog 物件稱為終端物件 (terminal object)。
既然師承 Smalltalk ,根據其物件模型,Dog 類就會負責描述 myDog 物件的行為,即 bark
方法將會儲存在 Dog 類中,Dog 類也會是一個物件,並且 Dog 類物件也有對應的類用以描述自身行為,在 Ruby 中,這個建立 Dog 類例項的類叫 singleton class ,Objecitve-C 中則稱為 metaclass 。Ruby 中的終端物件也可以有 singleton class ,Objecitve-C 在語言層面上並沒有實現這一點。
Ruby 的 Singleton Class
首先要明確的是 singleton class 區別於 singleton pattern 中建立的 class,在 Ruby 的物件模型中,singleton class 又可以稱作 metaclass(元類)、eigenclasses(特徵類),這裡統一稱做單件類。
考慮以下程式碼:
myDog = Dog.new def myDog.bark puts 'zizizi!' end myDog.bark # zizizi! yourDog = Dog.new yourDog.bark # wangwang!
在 Ruby 中,我們可以給特定物件定義專屬的方法,可以知道的是,新定義的 bark
方法不在 Dog 類中,因為給 yourDog 傳送 bark
訊息後的輸出並沒有改變,這種針對單個物件定義的方法稱為單件方法 (singleton method)。當我們定義單件方法或者呼叫 singleton_class
方法時,Ruby 會自動建立一個 “匿名類” 來儲存單件方法(惰性求值),這個類就是單件類,我們可以通過 singleton_class
方法用來訪問單件類。
除了使用上述的 def
,Ruby 還可以使用 <<
語法開啟物件的單件類,並且可以在單件類中使用 super
訪問其父類:
class << myDog def bark super puts 'zizizi!' end end myDog.bark # wangwang! # zizizi!
根據最新的示例程式碼,可以得到 Ruby 的物件模型圖:
需要注意的是,和 myDog 物件不一樣,yourDog 物件還沒有 #yourDog 單件類,其 klass
(Objective-C 中的 isa) 還是直接指向 Dog 類 (測試方法在文末 《Ruby 呼叫 C 擴充套件一節》 給出)。
如模型圖所示,類物件天然擁有一個對應的單件類 (對比 Objective-C 建立類時,必須同時建立元類),也就是說我們定義的類方法都是單件方法,直接存放在類物件的單件類中。採用下面幾種方式定義類方法,其結果都是一致的:
class Dog < Animal def self.create end end #################### class Dog < Animal class << self def create end end end #################### def Dog.create end #################### class << Dog def create end end
簡單來說, Ruby 中的單件類是隻屬於一個物件的類,它負責描述此物件的行為 。
Objective-C 的 KVO
Objective-C 可以支援多個根類 (NSObject、NSProxy、或者使用 OBJC_ROOT_CLASS 巨集自行建立的根類),這裡使用了 NSObject 作為根類,針對初始設定的示例程式碼,可以得到以下物件模型圖:
可以看到,對於終端物件 myDog 來說,其 isa
指向的是 Dog 類,這和上述 Ruby 實現中 yourDog 物件的 kclass
指向一致。接下來,我們給 myDog 新增一個觀察者 :
@interface Observer : NSObject @end @implementation Observer - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { Class cls = object_getClass(object); Class superCls = class_getSuperclass(cls); // cls: NSKVONotifying_Dog, super cls Dog NSLog(@"cls: %@, super cls %@", cls, superCls); } @end int main(int argc, const char * argv[]) { @autoreleasepool { Dog *myDog = [Dog new]; Observer *observer = [Observer new]; [myDog addObserver:observer forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil]; myDog.name = @"Bob"; } }
KVO 對屬性監聽的實現,本質上是對監聽屬性 setter 方法的切片,Objective-C 中實現方法切片最直接的方式是 method swizzling ,不過由於 setter 例項方法儲存在監聽物件所屬類中,如果直接替換,勢必會影響到這個類的其他例項化物件,於是 Objective-C 在這裡採用了另一種方式————繼承 + 多型。在新增觀察者之後,Objective-C 會建立 Dog 類的子類 NSKVONotifying_Dog ,並將 myDog 物件的 isa
指向 NSKVONotifying_Dog 類,接著在 NSKVONotifying_Dog 類中重寫監聽屬性的 setter 方法,變更之後的物件模型圖如下 :
可以看到,NSKVONotifying_Dog 類的功能幾乎和 Ruby 實現中 #myDog 單件類一致了,我們可以在這個類中給 myDog 物件新增例項方法,而不會對 Dog 類例項化的其他物件產生影響,這個類只負責描述 myDog 物件。
一旦實現了屬性的監聽,剩下的就是處理監聽者和監聽屬性的關係了,最直接的實現就是在監聽物件中維護一個 Map ,根據 Map 的值去派發屬性變更訊息,這一塊還是比較直觀的。
Aspects 中 hook 物件方法
Aspects 是針對 Objective-C 的 AOP 庫,我們可以使用 Aspects 做三件事情 ( 這裡的例項方法和類方法所屬描述,建立在從邏輯上講例項方法屬於類例項化的物件,從物理實現上講屬於類的這一假設之上 ):
- hook 單個終端物件的例項方法
- hook 某個類所有例項化物件的例項方法
- hook 某個類的類方法
其中 hook 單個終端物件的例項方法,本質上也屬於 hook 某個類所有例項化物件的例項方法,只不過這個類只會有 hook 物件這一個例項。三種 hook 的示例程式碼如下 :
Dog *myDog = [Dog new]; [myDog aspect_hookSelector:@selector(bark) withOptions:AspectPositionAfter usingBlock:^(id <AspectInfo> info){ NSLog(@"zizizi!"); } error:nil]; [myDog bark]; // wangwang! // zizizi! #################### [Dog aspect_hookSelector:@selector(bark) withOptions:AspectPositionAfter usingBlock:^(id <AspectInfo> info){ NSLog(@"zizizi!"); } error:nil]; Dog *myDog = [Dog new]; [myDog bark]; // wangwang! // zizizi! #################### id meta = object_getClass([Dog class]); [meta aspect_hookSelector:@selector(clsBark) withOptions:AspectPositionAfter usingBlock:^(id <AspectInfo> info){ NSLog(@"zizizi!"); } error:nil]; [Dog clsBark]; // wangwang! // zizizi!
第一種 hook 和 KVO 面臨一樣的問題,所以需要做子類化處理,而第二種和第三種 hook 因為影響範圍都是全域性的,所以可以直接操作類 / 元類的方法列表,在知道 hook 具體方法的前提下,我們也可以直接用 method swizzling 來替換 Aspects 的後兩種 hook 方式。
Aspects 建立了 aspect_hookClass 函式來處理這幾種 hook ,這個函式大概做了這麼幾件準備工作:
_Aspects_ object_getClass _Aspects_
可以看到,hook 終端物件時,為了變更範圍能侷限在終端物件中,Aspects 也建立了屬於 Objective-C 的“單件類”。
Ruby 呼叫 C 擴充套件
Ruby 可以通過擴充套件呼叫 C 函式,從而打印出記憶體中物件所屬的類。
建立 real_klass.c 檔案 :
#include <ruby.h> VALUE real_klass(VALUE self) { return RBASIC(self)->klass; } void Init_real_klass() { rb_define_method(rb_cObject,"real_klass",real_klass,0); }
建立 extconf.rb 檔案 :
require 'mkmf' extension_name = 'real_klass' dir_config(extension_name) create_makefile(extension_name)
生成擴充套件模組 :
$ ruby extconf.rb && make $ ls Makefilereal_klass.bundle real_klass.o extconf.rbreal_klass.canimal.rb
編輯 animal.rb 引入 real_class 模組 :
require './real_klass.bundle' ... myDog = Dog.new class << myDog def bark puts 'zizizi!' end end myDog.bark # zizizi! yourDog = Dog.new yourDog.bark # wangwang! # myDog 物件單例類已建立,klass 指向單例類 p myDog.real_klass# #<Class:#<Dog:0x007fcae98f1d10>> p myDog.singleton_class# #<Class:#<Dog:0x007fcae98f1d10>> # myDog 物件單例類的 klass 指向 Dog 類物件的單例類 # (在 myDog.singleton_class 沒有建立屬於它的單例類的情況下) p Dog.singleton_class# #<Class:Dog> p myDog.singleton_class.real_klass # #<Class:Dog> # yourDog 物件單例類還未建立,klass 指向 Dog 類物件 p yourDog.real_klass# Dog # yourDog 物件單例類已建立,klass 指向單例類 p yourDog.singleton_class # #<Class:#<Dog:0x007fdb2f049518>> p yourDog.real_klass# #<Class:#<Dog:0x007fdb2f049518>>
更完善的 Ruby 物件模型可以檢視 wiki 上的示意圖 。
小結
如果要在不影響一個類例項化其他物件前提下,給這個了建立的某個物件新增專屬的例項方法,在 Ruby 中我們可以通過將例項方法新增到物件的單件類中解決這個需求,並且 Ruby 在語言層面上就可以提供終端物件的單件類,而 Objective-C 沒有提供,在瞭解了 Ruby 單件類的實現之後,藉助 Objective-C 強大的執行時能力,我們可以自己去實現這個“語言特性”,主要有兩個關鍵步驟:
- 建立終端物件所屬類的子類
- 設定終端物件的類為剛建立的子類