過去的幾年中湧現了大量的Objective-C開發者。有些是從動態語言轉過來的,比如Ruby或Python,有些是從強型別語言轉過來的,如Java或C#,當然也有直接以Objective-C作為入門語言的。也就是說有很大一部分開發者都沒有使用Objective-C太長時間。當你接觸一門新語言時,更多地會關注基礎知識,如語法和特性等。但通常有一些更高階的,更鮮為人知又有強大功能的特性等待你去開拓。
這篇文章主要是來領略下Objective-C的執行時(runtime),同時解釋是什麼讓Objective-C如此動態,然後感受下這些動態化的技術細節。希望這回讓你對Objective-C和Cocoa是如何執行的有更好的瞭解。
The Runtime
Objective-C是一門簡單的語言,95%是C。只是在語言層面上加了些關鍵字和語法。真正讓Objective-C如此強大的是它的執行時。它很小但卻很強大。它的核心是訊息分發。
Messages
如果你是從動態語言如Ruby或Python轉過來的,可能知道什麼是訊息,可以直接跳過進入下一節。那些從其他語言轉過來的,繼續看。
執行一個方法,有些語言,編譯器會執行一些額外的優化和錯誤檢查,因為呼叫關係很直接也很明顯。但對於訊息分發來說,就不那麼明顯了。在發訊息前不必知道某個物件是否能夠處理訊息。你把訊息發給它,它可能會處理,也可能轉給其他的Object來處理。一個訊息不必對應一個方法,一個物件可能實現一個方法來處理多條訊息。
在Objective-C中,訊息是通過objc_msgSend()
這個runtime方法及相近的方法來實現的。這個方法需要一個target,selector,還有一些引數。理論上來說,編譯器只是把訊息分發變成objc_msgSend
來執行。比如下面這兩行程式碼是等價的。
1 2 |
[array insertObject:foo atIndex:5]; objc_msgSend(array, @selector(insertObject:atIndex:), foo, 5); |
Objects, Classes, MetaClasses
大多數物件導向的語言裡有 classes 和 objects 的概念。Objects通過Classes生成。但是在Objective-C中,classes本身也是objects(譯者注:這點跟python很像),也可以處理訊息,這也是為什麼會有類方法和例項方法。具體來說,Objective-C中的Object是一個結構體(struct),第一個成員是isa
,指向自己的class。這是在objc/objc.h中定義的。
1 2 3 |
typedef struct objc_object { Class isa; } *id; |
object的class儲存了方法列表,還有指向父類的指標。但classes也是objects,也會有isa
變數,那麼它又指向哪兒呢?這裡就引出了第三個型別: metaclasses
。一個 metaclass被指向class,class被指向object。它儲存了所有實現的方法列表,以及父類的metaclass。如果想更清楚地瞭解objects,classes以及metaclasses是如何一起工作地,可以閱讀這篇文章。
Methods, Selectors and IMPs
我們知道了執行時會發訊息給物件。我們也知道一個物件的class儲存了方法列表。那麼這些訊息是如何對映到方法的,這些方法又是如何被執行的呢?
第一個問題的答案很簡單。class的方法列表其實是一個字典,key為selectors,IMPs為value。一個IMP是指向方法在記憶體中的實現。很重要的一點是,selector和IMP之間的關係是在執行時才決定的,而不是編譯時。這樣我們就能玩出些花樣。
IMP通常是指向方法的指標,第一個引數是self,型別為id,第二個引數是_cmd,型別為SEL,餘下的是方法的引數。這也是self
和_cmd
被定義的地方。下面演示了Method和IMP
1 2 3 |
- (id)doSomethingWithInt:(int)aInt{} id doSomethingWithInt(id self, SEL _cmd, int aInt){} |
其他執行時的方法
現在我們知道了objects,classes,selectors,IMPs以及訊息分發,那麼執行時到底能做什麼呢?主要有兩個作用:
- 建立、修改、自省classes和objects
- 訊息分發
之前已經提過訊息分發,不過這只是一小部分功能。所有的執行時方法都有特定的字首。下面是一些有意思的方法:
class
class開頭的方法是用來修改和自省classes。方法如 class_addIvar、class_addMethod、 class_addProperty和class_addProtocol 允許重建classes。
class_copyIvarList、class_copyMethodList、class_copyProtocolList和class_copyPropertyList能拿到一個class的所有內容。而class_getClassMethod、class_getClassVariable、 class_getInstanceMethod, class_getInstanceVariable、class_getMethodImplementation 和 class_getProperty 返回單個內容。
也有一些通用的自省方法,如class_conformsToProtocol、class_respondsToSelector、 class_getSuperclass。最後,你可以使用class_createInstance來建立一個object。
ivar
這些方法能讓你得到名字,記憶體地址和Objective-C type encoding。
method
這些方法主要用來自省,比如method_getName
, method_getImplementation
, method_getReturnType
等等。也有一些修改的方法,包括method_setImplementation
和method_exchangeImplementations
,這些我們後面會講到。
objc
一旦拿到了object,你就可以對它做一些自省和修改。你可以get/set ivar, 使用object_copy
和object_dispose
來copy和free object的記憶體。最NB的不僅是拿到一個class,而是可以使用object_setClass
來改變一個object的class。待會就能看到使用場景。
property
屬性儲存了很大一部分資訊。除了拿到名字,你還可以使用property_getAttributes
來發現property的更多資訊,如返回值、是否為atomic、getter/setter名字、是否為dynamic、背後使用的ivar名字、是否為弱引用。
protocol
Protocols有點像classes,但是精簡版的,執行時的方法是一樣的。你可以獲取method, property, protocol列表, 檢查是否實現了其他的protocol。
sel
最後我們有一些方法可以處理 selectors,比如獲取名字,註冊一個selector等等。
現在我們對Objective-C的執行時有了大概的瞭解,來看看它們能做哪些有趣的事情。
Classes And Selectors From Strings
比較基礎的一個動態特性是通過String來生成Classes和Selectors。Cocoa提供了NSClassFromString
和NSSelectorFromString
方法,使用起來很簡單:
1 |
Class stringclass = NSClassFromString(@"NSString"); |
於是我們就得到了一個string class。接下來:
1 |
NSString *myString = [stringclass stringWithString:@"Hello World"]; |
為什麼要這麼做呢?直接使用Class不是更方便?通常情況下是,但有些場景下這個方法會很有用。首先,可以得知是否存在某個class,NSClassFromString
會返回nil,如果執行時不存在該class的話。比如可以檢查NSClassFromString(@"NSRegularExpression")
是否為nil來判斷是否為iOS4.0+。
另一個使用場景是根據不同的輸入返回不同的class或method。比如你在解析一些資料,每個資料項都有要解析的字串以及自身的型別(String,Number,Array)。你可以在一個方法裡搞定這些,也可以使用多個方法。其中一個方法是獲取type,然後使用if來呼叫匹配的方法。另一種是根據type來生成一個selector,然後呼叫之。以下是兩種實現方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
- (void)parseObject:(id)object { for (id data in object) { if ([[data type] isEqualToString:@"String"]) { [self parseString:[data value]]; } else if ([[data type] isEqualToString:@"Number"]) { [self parseNumber:[data value]]; } else if ([[data type] isEqualToString:@"Array"]) { [self parseArray:[data value]]; } } } - (void)parseObjectDynamic:(id)object { for (id data in object) { [self performSelector:NSSelectorFromString([NSString stringWithFormat:@"parse%@:", [data type]]) withObject:[data value]]; } } - (void)parseString:(NSString *)aString {} - (void)parseNumber:(NSString *)aNumber {} - (void)parseArray:(NSString *)aArray {} |
可一看到,你可以把7行帶if的程式碼變成1行。將來如果有新的型別,只需增加實現方法即可,而不用再去新增新的 else if。
Method Swizzling
之前我們講過,方法由兩個部分組成。Selector相當於一個方法的id;IMP是方法的實現。這樣分開的一個便利之處是selector和IMP之間的對應關係可以被改變。比如一個 IMP 可以有多個 selectors 指向它。
而 Method Swizzling 可以交換兩個方法的實現。或許你會問“什麼情況下會需要這個呢?”。我們先來看下Objective-C中,兩種擴充套件class的途徑。首先是 subclassing。你可以重寫某個方法,呼叫父類的實現,這也意味著你必須使用這個subclass的例項,但如果繼承了某個Cocoa class,而Cocoa又返回了原先的class(比如 NSArray)。這種情況下,你會想新增一個方法到NSArray,也就是使用Category。99%的情況下這是OK的,但如果你重寫了某個方法,就沒有機會再呼叫原先的實現了。
Method Swizzling 可以搞定這個問題。你可以重寫某個方法而不用繼承,同時還可以呼叫原先的實現。通常的做法是在category中新增一個方法(當然也可以是一個全新的class)。可以通過method_exchangeImplementations
這個執行時方法來交換實現。來看一個demo,這個demo演示瞭如何重寫addObject:
方法來紀錄每一個新新增的物件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#import <objc/runtime.h> @interface NSMutableArray (LoggingAddObject) - (void)logAddObject:(id)aObject; @end @implementation NSMutableArray (LoggingAddObject) + (void)load { Method addobject = class_getInstanceMethod(self, @selector(addObject:)); Method logAddobject = class_getInstanceMethod(self, @selector(logAddObject:)); method_exchangeImplementations(addObject, logAddObject); } - (void)logAddObject:(id)aobject { [self logAddObject:aObject]; NSLog(@"Added object %@ to array %@", aObject, self); } @end |
我們把方法交換放到了load
中,這個方法只會被呼叫一次,而且是執行時載入。如果指向臨時用一下,可以放到別的地方。注意到一個很明顯的遞迴呼叫logAddObject:
。這也是Method Swizzling容易把我們搞混的地方,因為我們已經交換了方法的實現,所以其實呼叫的是addObject:
動態繼承、交換
我們可以在執行時建立新的class,這個特性用得不多,但其實它還是很強大的。你能通過它建立新的子類,並新增新的方法。
但這樣的一個子類有什麼用呢?別忘了Objective-C的一個關鍵點:object內部有一個叫做isa
的變數指向它的class。這個變數可以被改變,而不需要重新建立。然後就可以新增新的ivar和方法了。可以通過以下命令來修改一個object的class
1 |
object_setClass(myObject, [MySubclass class]); |
這可以用在Key Value Observing。當你開始observing an object時,Cocoa會建立這個object的class的subclass,然後將這個object的isa指向新建立的subclass。點選這裡檢視更詳細的解釋。
動態方法處理
目前為止,我們討論了方法交換,以及已有方法的處理。那麼當你傳送了一個object無法處理的訊息時會發生什麼呢?很明顯,”it breaks”。大多數情況下確實如此,但Cocoa和runtime也提供了一些應對方法。
首先是動態方法處理。通常來說,處理一個方法,執行時尋找匹配的selector然後執行之。有時,你只想在執行時才建立某個方法,比如有些資訊只有在執行時才能得到。要實現這個效果,你需要重寫+resolveInstanceMethod:
和/或 +resolveClassMethod:
。如果確實增加了一個方法,記得返回YES。
1 2 3 4 5 6 7 |
+ (BOOL)resolveInstanceMethod:(SEL)aSelector { if (aSelector == @selector(myDynamicMethod)) { class_addMethod(self, aSelector, (IMP)myDynamicIMP, "v@:"); return YES; } return [super resolveInstanceMethod:aSelector]; } |
那Cocoa在什麼場景下會使用這些方法呢?Core Data用得很多。NSManagedObjects有許多在執行時新增的屬性用來處理get/set屬性和關係。那如果Model在執行時被改變了呢?
訊息轉發
如果 resolve method 返回NO,執行時就進入下一步驟:訊息轉發。有兩種常見用例。1) 將訊息轉發到另一個可以處理該訊息的object。2) 將多個訊息轉發到同一個方法。
訊息轉發分兩步。首先,執行時呼叫-forwardingTargetForSelector:
,如果只是想把訊息傳送到另一個object,那麼就使用這個方法,因為更高效。如果想要修改訊息,那麼就要使用-forwardInvocation:
,執行時將訊息打包成NSInvocation,然後返回給你處理。處理完之後,呼叫invokeWithTarget:
。
Cocoa有幾處地方用到了訊息轉發,主要的兩個地方是代理(Proxies)和響應鏈(Responder Chain)。NSProxy是一個輕量級的class,它的作用就是轉發訊息到另一個object。如果想要惰性載入object的某個屬性會很有用。NSUndoManager也有用到,不過是擷取訊息,之後再執行,而不是轉發到其他的地方。
響應鏈是關於Cocoa如何處理與傳送事件與行為到對應的物件。比如說,使用Cmd+C執行了copy命令,會傳送-copy:
到響應鏈。首先是First Responder,通常是當前的UI。如果沒有處理該訊息,則轉發到下一個-nextResponder
。這麼一直下去直到找到能夠處理該訊息的object,或者沒有找到,報錯。
使用Block作為Method IMP
iOS 4.3帶來了很多新的runtime方法。除了對properties和protocols的加強,還帶來一組新的以 imp 開頭的方法。通常一個 IMP 是一個指向方法實現的指標,頭兩個引數為 object(self)和selector(_cmd)。iOS 4.0和Mac OS X 10.6 帶來了block,imp_implementationWithBlock()
能讓我們使用block作為 IMP,下面這個程式碼片段展示瞭如何使用block來新增新的方法。
1 2 3 4 |
IMP myIMP = imp_implementationWithBlock(^(id _self, NSString *string) { NSLog(@"Hello %@", string); }); class_addMethod([MYclass class], @selector(sayHello:), myIMP, "v@:@"); |
如果想知道這是如何實現的,可以檢視這篇文章
可以看到,Objective-C 表面看起來挺簡單,但還是很靈活的,可以帶來很多可能性。動態語言的優勢在於在不擴充套件語言本身的情況下做很多很靈巧的事情。比如Key Value Observing,提供了優雅的API可以與已有的程式碼無縫結合,而不需要新增語言級別的特性。
希望這篇文章能讓你更深入地瞭解Objective-C,在開發app時也能開闊思路,考慮更多的可能性。