OC的動(dòng)態(tài)特性(runtime)

Objective-C的動(dòng)態(tài)特性

http://www.reibang.com/writer#/notebooks/2467236/notes/7270911/preview
這篇文章主要是來領(lǐng)略下Objective-C的運(yùn)行時(shí)(runtime)吃挑,同時(shí)解釋是什么讓Objective-C如此動(dòng)態(tài)号胚,然后感受下這些動(dòng)態(tài)化的技術(shù)細(xì)節(jié)。希望這回讓你對(duì)Objective-C和Cocoa是如何運(yùn)行的有更好的了解犯助。

The Runtime

Objective-C是一門簡單的語言,95%是C维咸。只是在語言層面上加了些關(guān)鍵字和語法剂买。真正讓Objective-C如此強(qiáng)大的是它的運(yùn)行時(shí)。它很小但卻很強(qiáng)大癌蓖。它的核心是消息分發(fā)瞬哼。

Messages

如果你是從動(dòng)態(tài)語言如Ruby或Python轉(zhuǎn)過來的,可能知道什么是消息租副,可以直接跳過進(jìn)入下一節(jié)坐慰。那些從其他語言轉(zhuǎn)過來的,繼續(xù)看用僧。

執(zhí)行一個(gè)方法结胀,有些語言,編譯器會(huì)執(zhí)行一些額外的優(yōu)化和錯(cuò)誤檢查责循,因?yàn)檎{(diào)用關(guān)系很直接也很明顯糟港。但對(duì)于消息分發(fā)來說,就不那么明顯了院仿。在發(fā)消息前不必知道某個(gè)對(duì)象是否能夠處理消息秸抚。你把消息發(fā)給它,它可能會(huì)處理歹垫,也可能轉(zhuǎn)給其他的Object來處理剥汤。一個(gè)消息不必對(duì)應(yīng)一個(gè)方法,一個(gè)對(duì)象可能實(shí)現(xiàn)一個(gè)方法來處理多條消息排惨。

在Objective-C中秀姐,消息是通過objc_msgSend()這個(gè)runtime方法及相近的方法來實(shí)現(xiàn)的。這個(gè)方法需要一個(gè)target若贮,selector省有,還有一些參數(shù)。理論上來說谴麦,編譯器只是把消息分發(fā)變成objc_msgSend來執(zhí)行蠢沿。比如下面這兩行代碼是等價(jià)的。

[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很像),也可以處理消息野宜,這也是為什么會(huì)有類方法和實(shí)例方法扫步。具體來說,Objective-C中的Object是一個(gè)結(jié)構(gòu)體(struct)匈子,第一個(gè)成員是isa河胎,指向自己的class。這是在objc/objc.h中定義的虎敦。

typedef struct objc_object {
Class isa;
} *id;

object的class保存了方法列表游岳,還有指向父類的指針。但classes也是objects其徙,也會(huì)有isa變量胚迫,那么它又指向哪兒呢?這里就引出了第三個(gè)類型: metaclasses唾那。一個(gè) metaclass被指向class访锻,class被指向object。它保存了所有實(shí)現(xiàn)的方法列表闹获,以及父類的metaclass朗若。如果想更清楚地了解objects,classes以及metaclasses是如何一起工作地,可以閱讀這篇文章昌罩。

Methods, Selectors and IMPs

我們知道了運(yùn)行時(shí)會(huì)發(fā)消息給對(duì)象。我們也知道一個(gè)對(duì)象的class保存了方法列表灾馒。那么這些消息是如何映射到方法的茎用,這些方法又是如何被執(zhí)行的呢?

第一個(gè)問題的答案很簡單睬罗。class的方法列表其實(shí)是一個(gè)字典轨功,key為selectors,IMPs為value容达。一個(gè)IMP是指向方法在內(nèi)存中的實(shí)現(xiàn)古涧。很重要的一點(diǎn)是,selector和IMP之間的關(guān)系是在運(yùn)行時(shí)才決定的花盐,而不是編譯時(shí)羡滑。這樣我們就能玩出些花樣。

IMP通常是指向方法的指針算芯,第一個(gè)參數(shù)是self柒昏,類型為id,第二個(gè)參數(shù)是cmd熙揍,類型為SEL职祷,余下的是方法的參數(shù)。這也是self和cmd被定義的地方。下面演示了Method和IMP

- (id)doSomethingWithInt:(int)aInt{}

id doSomethingWithInt(id self, SEL _cmd, int aInt){}

其他運(yùn)行時(shí)的方法

現(xiàn)在我們知道了objects,classes,selectors,IMPs以及消息分發(fā)有梆,那么運(yùn)行時(shí)到底能做什么呢是尖?主要有兩個(gè)作用:

創(chuàng)建、修改泥耀、自省classes和objects
消息分發(fā)
之前已經(jīng)提過消息分發(fā)饺汹,不過這只是一小部分功能。所有的運(yùn)行時(shí)方法都有特定的前綴爆袍。下面是一些有意思的方法:

class

class開頭的方法是用來修改和自省classes首繁。方法如class_addIvar, class_addMethod, class_addProperty和class_addProtocol允許重建classes。class_copyIvarList, class_copyMethodList, class_copyProtocolList和class_copyPropertyList能拿到一個(gè)class的所有內(nèi)容陨囊。而class_getClassMethod, class_getClassVariable, class_getInstanceMethod, class_getInstanceVariable, class_getMethodImplementation和class_getProperty返回單個(gè)內(nèi)容弦疮。

也有一些通用的自省方法,如class_conformsToProtocol, class_respondsToSelector, class_getSuperclass蜘醋。最后胁塞,你可以使用class_createInstance來創(chuàng)建一個(gè)object。

ivar

這些方法能讓你得到名字压语,內(nèi)存地址和Objective-C type encoding啸罢。

method

這些方法主要用來自省,比如method_getName, method_getImplementation, method_getReturnType等等胎食。也有一些修改的方法扰才,包括method_setImplementation和method_exchangeImplementations,這些我們后面會(huì)講到厕怜。

objc

一旦拿到了object衩匣,你就可以對(duì)它做一些自省和修改。你可以get/set ivar, 使用object_copy和object_dispose來copy和free object的內(nèi)存粥航。最NB的不僅是拿到一個(gè)class琅捏,而是可以使用object_setClass來改變一個(gè)object的class。待會(huì)就能看到使用場景递雀。

property

屬性保存了很大一部分信息柄延。除了拿到名字,你還可以使用property_getAttributes來發(fā)現(xiàn)property的更多信息缀程,如返回值搜吧、是否為atomic、getter/setter名字杨凑、是否為dynamic赎败、背后使用的ivar名字、是否為弱引用蠢甲。

protocol

Protocols有點(diǎn)像classes僵刮,但是精簡版的,運(yùn)行時(shí)的方法是一樣的。你可以獲取method, property, protocol列表, 檢查是否實(shí)現(xiàn)了其他的protocol搞糕。

sel

最后我們有一些方法可以處理 selectors勇吊,比如獲取名字,注冊(cè)一個(gè)selector等等窍仰。

現(xiàn)在我們對(duì)Objective-C的運(yùn)行時(shí)有了大概的了解汉规,來看看它們能做哪些有趣的事情。

Classes And Selectors From Strings

比較基礎(chǔ)的一個(gè)動(dòng)態(tài)特性是通過String來生成Classes和Selectors驹吮。Cocoa提供了NSClassFromString和NSSelectorFromString方法针史,使用起來很簡單:

Class stringclass = NSClassFromString(@"NSString");

于是我們就得到了一個(gè)string class。接下來:

NSString *myString = [stringclass stringWithString:@"Hello World"];

為什么要這么做呢碟狞?直接使用Class不是更方便啄枕?通常情況下是,但有些場景下這個(gè)方法會(huì)很有用族沃。首先频祝,可以得知是否存在某個(gè)class,NSClassFromString 會(huì)返回nil脆淹,如果運(yùn)行時(shí)不存在該class的話常空。比如可以檢查NSClassFromString(@"NSRegularExpression")是否為nil來判斷是否為iOS4.0+。

另一個(gè)使用場景是根據(jù)不同的輸入返回不同的class或method盖溺。比如你在解析一些數(shù)據(jù)漓糙,每個(gè)數(shù)據(jù)項(xiàng)都有要解析的字符串以及自身的類型(String,Number烘嘱,Array)昆禽。你可以在一個(gè)方法里搞定這些,也可以使用多個(gè)方法拙友。其中一個(gè)方法是獲取type,然后使用if來調(diào)用匹配的方法歼郭。另一種是根據(jù)type來生成一個(gè)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

之前我們講過,方法由兩個(gè)部分組成逼蒙。Selector相當(dāng)于一個(gè)方法的id从绘;IMP是方法的實(shí)現(xiàn)。這樣分開的一個(gè)便利之處是selector和IMP之間的對(duì)應(yīng)關(guān)系可以被改變。比如一個(gè) IMP 可以有多個(gè) selectors 指向它僵井。

而 Method Swizzling 可以交換兩個(gè)方法的實(shí)現(xiàn)陕截。或許你會(huì)問“什么情況下會(huì)需要這個(gè)呢批什?”农曲。我們先來看下Objective-C中驻债,兩種擴(kuò)展class的途徑。首先是 subclassing。你可以重寫某個(gè)方法,調(diào)用父類的實(shí)現(xiàn),這也意味著你必須使用這個(gè)subclass的實(shí)例,但如果繼承了某個(gè)Cocoa class,而Cocoa又返回了原先的class(比如 NSArray)炊昆。這種情況下洛搀,你會(huì)想添加一個(gè)方法到NSArray,也就是使用Category。99%的情況下這是OK的,但如果你重寫了某個(gè)方法宛蚓,就沒有機(jī)會(huì)再調(diào)用原先的實(shí)現(xiàn)了痕钢。

Method Swizzling 可以搞定這個(gè)問題任连。你可以重寫某個(gè)方法而不用繼承蚤吹,同時(shí)還可以調(diào)用原先的實(shí)現(xiàn)。通常的做法是在category中添加一個(gè)方法(當(dāng)然也可以是一個(gè)全新的class)随抠〔米牛可以通過method_exchangeImplementations這個(gè)運(yùn)行時(shí)方法來交換實(shí)現(xiàn)。來看一個(gè)demo拱她,這個(gè)demo演示了如何重寫addObject:方法來紀(jì)錄每一個(gè)新添加的對(duì)象二驰。

#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中,這個(gè)方法只會(huì)被調(diào)用一次秉沼,而且是運(yùn)行時(shí)載入桶雀。如果指向臨時(shí)用一下,可以放到別的地方氧猬。注意到一個(gè)很明顯的遞歸調(diào)用logAddObject:背犯。這也是Method Swizzling容易把我們搞混的地方坏瘩,因?yàn)槲覀円呀?jīng)交換了方法的實(shí)現(xiàn)盅抚,所以其實(shí)調(diào)用的是addObject:

Method Swizzling

動(dòng)態(tài)繼承、交換

我們可以在運(yùn)行時(shí)創(chuàng)建新的class倔矾,這個(gè)特性用得不多妄均,但其實(shí)它還是很強(qiáng)大的柱锹。你能通過它創(chuàng)建新的子類,并添加新的方法丰包。

但這樣的一個(gè)子類有什么用呢禁熏?別忘了Objective-C的一個(gè)關(guān)鍵點(diǎn):object內(nèi)部有一個(gè)叫做isa的變量指向它的class。這個(gè)變量可以被改變邑彪,而不需要重新創(chuàng)建瞧毙。然后就可以添加新的ivar和方法了〖闹ⅲ可以通過以下命令來修改一個(gè)object的class

object_setClass(myObject, [MySubclass class]);

這可以用在Key Value Observing宙彪。當(dāng)你開始o(jì)bserving an object時(shí),Cocoa會(huì)創(chuàng)建這個(gè)object的class的subclass有巧,然后將這個(gè)object的isa指向新創(chuàng)建的subclass释漆。點(diǎn)擊這里查看更詳細(xì)的解釋。

動(dòng)態(tài)方法處理

目前為止篮迎,我們討論了方法交換男图,以及已有方法的處理。那么當(dāng)你發(fā)送了一個(gè)object無法處理的消息時(shí)會(huì)發(fā)生什么呢甜橱?很明顯逊笆,"it breaks"。大多數(shù)情況下確實(shí)如此渗鬼,但Cocoa和runtime也提供了一些應(yīng)對(duì)方法览露。

首先是動(dòng)態(tài)方法處理。通常來說譬胎,處理一個(gè)方法差牛,運(yùn)行時(shí)尋找匹配的selector然后執(zhí)行之。有時(shí)堰乔,你只想在運(yùn)行時(shí)才創(chuàng)建某個(gè)方法偏化,比如有些信息只有在運(yùn)行時(shí)才能得到。要實(shí)現(xiàn)這個(gè)效果镐侯,你需要重寫+resolveInstanceMethod: 和/或 +resolveClassMethod:侦讨。如果確實(shí)增加了一個(gè)方法,記得返回YES苟翻。

+ (BOOL)resolveInstanceMethod:(SEL)aSelector {
if (aSelector == @selector(myDynamicMethod)) {
    class_addMethod(self, aSelector, (IMP)myDynamicIMP, "v@:");
    return YES;
}
return [super resolveInstanceMethod:aSelector];
}

那Cocoa在什么場景下會(huì)使用這些方法呢韵卤?Core Data用得很多。NSManagedObjects有許多在運(yùn)行時(shí)添加的屬性用來處理get/set屬性和關(guān)系崇猫。那如果Model在運(yùn)行時(shí)被改變了呢沈条?

消息轉(zhuǎn)發(fā)

如果 resolve method 返回NO,運(yùn)行時(shí)就進(jìn)入下一步驟:消息轉(zhuǎn)發(fā)诅炉。有兩種常見用例蜡歹。1) 將消息轉(zhuǎn)發(fā)到另一個(gè)可以處理該消息的object屋厘。2) 將多個(gè)消息轉(zhuǎn)發(fā)到同一個(gè)方法。

消息轉(zhuǎn)發(fā)分兩步月而。首先汗洒,運(yùn)行時(shí)調(diào)用-forwardingTargetForSelector:,如果只是想把消息發(fā)送到另一個(gè)object父款,那么就使用這個(gè)方法溢谤,因?yàn)楦咝АH绻胍薷南⒑┰埽敲淳鸵褂?forwardInvocation:溯香,運(yùn)行時(shí)將消息打包成NSInvocation,然后返回給你處理浓恶。處理完之后玫坛,調(diào)用invokeWithTarget:。

Cocoa有幾處地方用到了消息轉(zhuǎn)發(fā)包晰,主要的兩個(gè)地方是代理(Proxies)和響應(yīng)鏈(Responder Chain)湿镀。NSProxy是一個(gè)輕量級(jí)的class,它的作用就是轉(zhuǎn)發(fā)消息到另一個(gè)object伐憾。如果想要惰性加載object的某個(gè)屬性會(huì)很有用勉痴。NSUndoManager也有用到,不過是截取消息树肃,之后再執(zhí)行蒸矛,而不是轉(zhuǎn)發(fā)到其他的地方。

響應(yīng)鏈?zhǔn)顷P(guān)于Cocoa如何處理與發(fā)送事件與行為到對(duì)應(yīng)的對(duì)象胸嘴。比如說雏掠,使用Cmd+C執(zhí)行了copy命令,會(huì)發(fā)送-copy:到響應(yīng)鏈劣像。首先是First Responder乡话,通常是當(dāng)前的UI。如果沒有處理該消息耳奕,則轉(zhuǎn)發(fā)到下一個(gè)-nextResponder绑青。這么一直下去直到找到能夠處理該消息的object,或者沒有找到屋群,報(bào)錯(cuò)闸婴。

使用Block作為Method IMP

iOS 4.3帶來了很多新的runtime方法。除了對(duì)properties和protocols的加強(qiáng)芍躏,還帶來一組新的以 imp 開頭的方法邪乍。通常一個(gè) IMP 是一個(gè)指向方法實(shí)現(xiàn)的指針,頭兩個(gè)參數(shù)為 object(self)和selector(cmd)。iOS 4.0和Mac OS X 10.6 帶來了block溺欧,impimplementationWithBlock() 能讓我們使用block作為 IMP,下面這個(gè)代碼片段展示了如何使用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 表面看起來挺簡單烦味,但還是很靈活的聂使,可以帶來很多可能性。動(dòng)態(tài)語言的優(yōu)勢(shì)在于在不擴(kuò)展語言本身的情況下做很多很靈巧的事情谬俄。比如Key Value Observing柏靶,提供了優(yōu)雅的API可以與已有的代碼無縫結(jié)合,而不需要新增語言級(jí)別的特性溃论。

希望這篇文章能讓你更深入地了解Objective-C屎蜓,在開發(fā)app時(shí)也能開闊思路,考慮更多的可能性钥勋。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末炬转,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子算灸,更是在濱河造成了極大的恐慌扼劈,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,036評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件菲驴,死亡現(xiàn)場離奇詭異荐吵,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)赊瞬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,046評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門先煎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人巧涧,你說我怎么就攤上這事榨婆。” “怎么了褒侧?”我有些...
    開封第一講書人閱讀 164,411評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵良风,是天一觀的道長。 經(jīng)常有香客問我闷供,道長烟央,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,622評(píng)論 1 293
  • 正文 為了忘掉前任歪脏,我火速辦了婚禮疑俭,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘婿失。我一直安慰自己钞艇,他們只是感情好啄寡,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,661評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著哩照,像睡著了一般挺物。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上飘弧,一...
    開封第一講書人閱讀 51,521評(píng)論 1 304
  • 那天识藤,我揣著相機(jī)與錄音,去河邊找鬼次伶。 笑死痴昧,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的冠王。 我是一名探鬼主播赶撰,決...
    沈念sama閱讀 40,288評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼柱彻!你這毒婦竟也來了扣囊?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,200評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤绒疗,失蹤者是張志新(化名)和其女友劉穎侵歇,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體吓蘑,經(jīng)...
    沈念sama閱讀 45,644評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡惕虑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,837評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了磨镶。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片溃蔫。...
    茶點(diǎn)故事閱讀 39,953評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖琳猫,靈堂內(nèi)的尸體忽然破棺而出伟叛,到底是詐尸還是另有隱情,我是刑警寧澤脐嫂,帶...
    沈念sama閱讀 35,673評(píng)論 5 346
  • 正文 年R本政府宣布统刮,位于F島的核電站,受9級(jí)特大地震影響账千,放射性物質(zhì)發(fā)生泄漏侥蒙。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,281評(píng)論 3 329
  • 文/蒙蒙 一匀奏、第九天 我趴在偏房一處隱蔽的房頂上張望鞭衩。 院中可真熱鬧,春花似錦、人聲如沸论衍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,889評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽坯台。三九已至炬丸,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間捂人,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,011評(píng)論 1 269
  • 我被黑心中介騙來泰國打工矢沿, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留滥搭,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,119評(píng)論 3 370
  • 正文 我出身青樓捣鲸,卻偏偏與公主長得像瑟匆,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子栽惶,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,901評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容