過去的幾年中涌現(xiàn)了大量的Objective-C開發(fā)者。有些是從動態(tài)語言轉(zhuǎn)過來的,比如Ruby或Python,有些是從強(qiáng)類型語言轉(zhuǎn)過來的,如Java或C#,當(dāng)然也有直接以O(shè)bjective-C作為入門語言的。也就是說有很大一部分開發(fā)者都沒有使用Objective-C太長時間。當(dāng)你接觸一門新語言時,更多地會關(guān)注基礎(chǔ)知識,如語法和特性等。但通常有一些更高級的,更鮮為人知又有強(qiáng)大功能的特性等待你去開拓。
這篇文章主要是來領(lǐng)略下Objective-C的運(yùn)行時(runtime),同時解釋是什么讓Objective-C如此動態(tài),然后感受下這些動態(tài)化的技術(shù)細(xì)節(jié)。希望這回讓你對Objective-C和Cocoa是如何運(yùn)行的有更好的了解。
The Runtime
Objective-C是一門簡單的語言,95%是C。只是在語言層面上加了些關(guān)鍵字和語法。真正讓Objective-C如此強(qiáng)大的是它的運(yùn)行時。它很小但卻很強(qiáng)大。它的核心是消息分發(fā)。
Messages
如果你是從動態(tài)語言如Ruby或Python轉(zhuǎn)過來的,可能知道什么是消息,可以直接跳過進(jìn)入下一節(jié)。那些從其他語言轉(zhuǎn)過來的,繼續(xù)看。
執(zhí)行一個方法,有些語言,編譯器會執(zhí)行一些額外的優(yōu)化和錯誤檢查,因?yàn)檎{(diào)用關(guān)系很直接也很明顯。但對于消息分發(fā)來說,就不那么明顯了。在發(fā)消息前不必知道某個對象是否能夠處理消息。你把消息發(fā)給它,它可能會處理,也可能轉(zhuǎn)給其他的Object來處理。一個消息不必對應(yīng)一個方法,一個對象可能實(shí)現(xiàn)一個方法來處理多條消息。
在Objective-C中,消息是通過objc_msgSend()這個runtime方法及相近的方法來實(shí)現(xiàn)的。這個方法需要一個target,selector,還有一些參數(shù)。理論上來說,編譯器只是把消息分發(fā)變成objc_msgSend來執(zhí)行。比如下面這兩行代碼是等價的。
[array insertObject:foo atIndex:5];objc_msgSend(array, @selector(insertObject:atIndex:), foo, 5);
Objects, Classes, MetaClasses
大多數(shù)面向?qū)ο蟮恼Z言里有 classes 和 objects 的概念。Objects通過Classes生成。但是在Objective-C中,classes本身也是objects(譯者注:這點(diǎn)跟python很像),也可以處理消息,這也是為什么會有類方法和實(shí)例方法。具體來說,Objective-C中的Object是一個結(jié)構(gòu)體(struct),第一個成員是isa,指向自己的class。這是在objc/objc.h中定義的。
typedef struct objc_object {
Class isa;
} *id;
object的class保存了方法列表,還有指向父類的指針。但classes也是objects,也會有isa變量,那么它又指向哪兒呢?這里就引出了第三個類型: metaclasses。一個 metaclass被指向class,class被指向object。它保存了所有實(shí)現(xiàn)的方法列表,以及父類的metaclass。如果想更清楚地了解objects,classes以及metaclasses是如何一起工作地,可以閱讀這篇文章。
Methods, Selectors and IMPs
我們知道了運(yùn)行時會發(fā)消息給對象。我們也知道一個對象的class保存了方法列表。那么這些消息是如何映射到方法的,這些方法又是如何被執(zhí)行的呢?
第一個問題的答案很簡單。class的方法列表其實(shí)是一個字典,key為selectors,IMPs為value。一個IMP是指向方法在內(nèi)存中的實(shí)現(xiàn)。很重要的一點(diǎn)是,selector和IMP之間的關(guān)系是在運(yùn)行時才決定的,而不是編譯時。這樣我們就能玩出些花樣。
IMP通常是指向方法的指針,第一個參數(shù)是self,類型為id,第二個參數(shù)是_cmd,類型為SEL,余下的是方法的參數(shù)。這也是self和_cmd被定義的地方。下面演示了Method和IMP
- (id)doSomethingWithInt:(int)aInt{}
id doSomethingWithInt(id self, SEL _cmd, int aInt){}
其他運(yùn)行時的方法
現(xiàn)在我們知道了objects,classes,selectors,IMPs以及消息分發(fā),那么運(yùn)行時到底能做什么呢?主要有兩個作用:
- 創(chuàng)建、修改、自省classes和objects
- 消息分發(fā)
之前已經(jīng)提過消息分發(fā),不過這只是一小部分功能。所有的運(yùn)行時方法都有特定的前綴。下面是一些有意思的方法:
class
class開頭的方法是用來修改和自省classes。方法如class_addIvar, class_addMethod, class_addProperty和class_addProtocol允許重建classes。class_copyIvarList, class_copyMethodList, class_copyProtocolList和class_copyPropertyList能拿到一個class的所有內(nèi)容。而class_getClassMethod, class_getClassVariable, class_getInstanceMethod, class_getInstanceVariable, class_getMethodImplementation和class_getProperty返回單個內(nèi)容。
也有一些通用的自省方法,如class_conformsToProtocol, class_respondsToSelector, class_getSuperclass。最后,你可以使用class_createInstance來創(chuàng)建一個object。
ivar
這些方法能讓你得到名字,內(nèi)存地址和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的內(nèi)存。最NB的不僅是拿到一個class,而是可以使用object_setClass來改變一個object的class。待會就能看到使用場景。
property
屬性保存了很大一部分信息。除了拿到名字,你還可以使用property_getAttributes來發(fā)現(xiàn)property的更多信息,如返回值、是否為atomic、getter/setter名字、是否為dynamic、背后使用的ivar名字、是否為弱引用。
protocol
Protocols有點(diǎn)像classes,但是精簡版的,運(yùn)行時的方法是一樣的。你可以獲取method, property, protocol列表, 檢查是否實(shí)現(xiàn)了其他的protocol。
sel
最后我們有一些方法可以處理 selectors,比如獲取名字,注冊一個selector等等。
現(xiàn)在我們對Objective-C的運(yùn)行時有了大概的了解,來看看它們能做哪些有趣的事情。
Classes And Selectors From Strings
比較基礎(chǔ)的一個動態(tài)特性是通過String來生成Classes和Selectors。Cocoa提供了NSClassFromString和NSSelectorFromString方法,使用起來很簡單:
Class stringclass = NSClassFromString(@"NSString");
于是我們就得到了一個string class。接下來:
NSString *myString = [stringclass stringWithString:@"Hello World"];
為什么要這么做呢?直接使用Class不是更方便?通常情況下是,但有些場景下這個方法會很有用。首先,可以得知是否存在某個class,NSClassFromString 會返回nil,如果運(yùn)行時不存在該class的話。比如可以檢查NSClassFromString(@"NSRegularExpression")是否為nil來判斷是否為iOS4.0+。
另一個使用場景是根據(jù)不同的輸入返回不同的class或method。比如你在解析一些數(shù)據(jù),每個數(shù)據(jù)項(xiàng)都有要解析的字符串以及自身的類型(String,Number,Array)。你可以在一個方法里搞定這些,也可以使用多個方法。其中一個方法是獲取type,然后使用if來調(diào)用匹配的方法。另一種是根據(jù)type來生成一個selector,然后調(diào)用之。以下是兩種實(shí)現(xiàn)方式:
- (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行。將來如果有新的類型,只需增加實(shí)現(xiàn)方法即可,而不用再去添加新的 else if。
Method Swizzling
之前我們講過,方法由兩個部分組成。Selector相當(dāng)于一個方法的id;IMP是方法的實(shí)現(xiàn)。這樣分開的一個便利之處是selector和IMP之間的對應(yīng)關(guān)系可以被改變。比如一個 IMP 可以有多個 selectors 指向它。
而 Method Swizzling 可以交換兩個方法的實(shí)現(xiàn)。或許你會問“什么情況下會需要這個呢?”。我們先來看下Objective-C中,兩種擴(kuò)展class的途徑。首先是 subclassing。你可以重寫某個方法,調(diào)用父類的實(shí)現(xiàn),這也意味著你必須使用這個subclass的實(shí)例,但如果繼承了某個Cocoa class,而Cocoa又返回了原先的class(比如 NSArray)。這種情況下,你會想添加一個方法到NSArray,也就是使用Category。99%的情況下這是OK的,但如果你重寫了某個方法,就沒有機(jī)會再調(diào)用原先的實(shí)現(xiàn)了。
Method Swizzling 可以搞定這個問題。你可以重寫某個方法而不用繼承,同時還可以調(diào)用原先的實(shí)現(xiàn)。通常的做法是在category中添加一個方法(當(dāng)然也可以是一個全新的class)??梢酝ㄟ^method_exchangeImplementations這個運(yùn)行時方法來交換實(shí)現(xiàn)。來看一個demo,這個demo演示了如何重寫addObject:方法來紀(jì)錄每一個新添加的對象。
#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中,這個方法只會被調(diào)用一次,而且是運(yùn)行時載入。如果指向臨時用一下,可以放到別的地方。注意到一個很明顯的遞歸調(diào)用logAddObject:。這也是Method Swizzling容易把我們搞混的地方,因?yàn)槲覀円呀?jīng)交換了方法的實(shí)現(xiàn),所以其實(shí)調(diào)用的是addObject:

動態(tài)繼承、交換
我們可以在運(yùn)行時創(chuàng)建新的class,這個特性用得不多,但其實(shí)它還是很強(qiáng)大的。你能通過它創(chuàng)建新的子類,并添加新的方法。
但這樣的一個子類有什么用呢?別忘了Objective-C的一個關(guān)鍵點(diǎn):object內(nèi)部有一個叫做isa的變量指向它的class。這個變量可以被改變,而不需要重新創(chuàng)建。然后就可以添加新的ivar和方法了??梢酝ㄟ^以下命令來修改一個object的class
object_setClass(myObject, [MySubclass class]);
這可以用在Key Value Observing。當(dāng)你開始o(jì)bserving an object時,Cocoa會創(chuàng)建這個object的class的subclass,然后將這個object的isa指向新創(chuàng)建的subclass。點(diǎn)擊這里查看更詳細(xì)的解釋。
動態(tài)方法處理
目前為止,我們討論了方法交換,以及已有方法的處理。那么當(dāng)你發(fā)送了一個object無法處理的消息時會發(fā)生什么呢?很明顯,”it breaks”。大多數(shù)情況下確實(shí)如此,但Cocoa和runtime也提供了一些應(yīng)對方法。
首先是動態(tài)方法處理。通常來說,處理一個方法,運(yùn)行時尋找匹配的selector然后執(zhí)行之。有時,你只想在運(yùn)行時才創(chuàng)建某個方法,比如有些信息只有在運(yùn)行時才能得到。要實(shí)現(xiàn)這個效果,你需要重寫+resolveInstanceMethod: 和/或 +resolveClassMethod:。如果確實(shí)增加了一個方法,記得返回YES。
+ (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有許多在運(yùn)行時添加的屬性用來處理get/set屬性和關(guān)系。那如果Model在運(yùn)行時被改變了呢?
消息轉(zhuǎn)發(fā)
如果 resolve method 返回NO,運(yùn)行時就進(jìn)入下一步驟:消息轉(zhuǎn)發(fā)。有兩種常見用例。1) 將消息轉(zhuǎn)發(fā)到另一個可以處理該消息的object。2) 將多個消息轉(zhuǎn)發(fā)到同一個方法。
消息轉(zhuǎn)發(fā)分兩步。首先,運(yùn)行時調(diào)用-forwardingTargetForSelector:,如果只是想把消息發(fā)送到另一個object,那么就使用這個方法,因?yàn)楦咝?。如果想要修改消息,那么就要使?code>-forwardInvocation:,運(yùn)行時將消息打包成NSInvocation,然后返回給你處理。處理完之后,調(diào)用invokeWithTarget:。
Cocoa有幾處地方用到了消息轉(zhuǎn)發(fā),主要的兩個地方是代理(Proxies)和響應(yīng)鏈(Responder Chain)。NSProxy是一個輕量級的class,它的作用就是轉(zhuǎn)發(fā)消息到另一個object。如果想要惰性加載object的某個屬性會很有用。NSUndoManager也有用到,不過是截取消息,之后再執(zhí)行,而不是轉(zhuǎn)發(fā)到其他的地方。
響應(yīng)鏈?zhǔn)顷P(guān)于Cocoa如何處理與發(fā)送事件與行為到對應(yīng)的對象。比如說,使用Cmd+C執(zhí)行了copy命令,會發(fā)送-copy:到響應(yīng)鏈。首先是First Responder,通常是當(dāng)前的UI。如果沒有處理該消息,則轉(zhuǎn)發(fā)到下一個-nextResponder。這么一直下去直到找到能夠處理該消息的object,或者沒有找到,報錯。
使用Block作為Method IMP
iOS 4.3帶來了很多新的runtime方法。除了對properties和protocols的加強(qiáng),還帶來一組新的以 imp 開頭的方法。通常一個 IMP 是一個指向方法實(shí)現(xiàn)的指針,頭兩個參數(shù)為 object(self)和selector(_cmd)。iOS 4.0和Mac OS X 10.6 帶來了block,imp_implementationWithBlock() 能讓我們使用block作為 IMP,下面這個代碼片段展示了如何使用block來添加新的方法。
IMP myIMP = imp_implementationWithBlock(^(id _self, NSString *string) {
NSLog(@"Hello %@", string);
});
class_addMethod([MYclass class], @selector(sayHello:), myIMP, "v@:@");
如果想知道這是如何實(shí)現(xiàn)的,可以查看這篇文章
可以看到,Objective-C 表面看起來挺簡單,但還是很靈活的,可以帶來很多可能性。動態(tài)語言的優(yōu)勢在于在不擴(kuò)展語言本身的情況下做很多很靈巧的事情。比如Key Value Observing,提供了優(yōu)雅的API可以與已有的代碼無縫結(jié)合,而不需要新增語言級別的特性。
希望這篇文章能讓你更深入地了解Objective-C,在開發(fā)app時也能開闊思路,考慮更多的可能性。