iOS 消息

Objc 中發(fā)送消息是用中括號(hào)([])把接收者和消息括起來(lái),而直到運(yùn)行時(shí)才會(huì)把消息與方法實(shí)現(xiàn)綁定。

有關(guān)消息發(fā)送和消息轉(zhuǎn)發(fā)機(jī)制的原理,可以查看這篇文章。

objc_msgSend 函數(shù)

在引言中已經(jīng)對(duì)objc_msgSend進(jìn)行了一點(diǎn)介紹,看起來(lái)像是objc_msgSend返回了數(shù)據(jù),其實(shí)objc_msgSend從不返回?cái)?shù)據(jù)而是你的方法被調(diào)用后返回了數(shù)據(jù)。下面詳細(xì)敘述下消息發(fā)送步驟:
1、檢測(cè)這個(gè)selector是不是要忽略的。比如 Mac OS X 開發(fā),有了垃圾回收就不理會(huì) retain, release 這些函數(shù)了。
2、檢測(cè)這個(gè) target是不是nil 對(duì)象。ObjC的特性是允許對(duì)一個(gè) nil 對(duì)象執(zhí)行任何一個(gè)方法不會(huì) Crash,因?yàn)闀?huì)被忽略掉。
3、如果上面兩個(gè)都過(guò)了,那就開始查找這個(gè)類的IMP,先從cache里面找,完了找得到就跳到對(duì)應(yīng)的函數(shù)去執(zhí)行。
4、如果cache找不到就找一下方法分發(fā)表。
5、如果分發(fā)表找不到就到超類的分發(fā)表去找,一直找,直到找到NSObject類為止。
6、如果還找不到就要開始進(jìn)入動(dòng)態(tài)方法解析了,后面會(huì)提到。

PS:這里說(shuō)的分發(fā)表其實(shí)就是Class中的方法列表,它將方法選擇器和方法實(shí)現(xiàn)地址聯(lián)系起來(lái)。

其實(shí)編譯器會(huì)根據(jù)情況在objc_msgSend, objc_msgSend_stret, objc_msgSendSuper, 或objc_msgSendSuper_stret四個(gè)方法中選擇一個(gè)來(lái)調(diào)用。如果消息是傳遞給超類,那么會(huì)調(diào)用名字帶有”Super”的函數(shù);如果消息返回值是數(shù)據(jù)結(jié)構(gòu)而不是簡(jiǎn)單值時(shí),那么會(huì)調(diào)用名字帶有”stret”的函數(shù)。排列組合正好四個(gè)方法。

值得一提的是在 i386 平臺(tái)處理返回類型為浮點(diǎn)數(shù)的消息時(shí),需要用到objc_msgSend_fpret函數(shù)來(lái)進(jìn)行處理,這是因?yàn)榉祷仡愋蜑楦↑c(diǎn)數(shù)的函數(shù)對(duì)應(yīng)的 ABI(Application Binary Interface) 與返回整型的函數(shù)的 ABI 不兼容。此時(shí)objc_msgSend不再適用,于是objc_msgSend_fpret被派上用場(chǎng),它會(huì)對(duì)浮點(diǎn)數(shù)寄存器做特殊處理。不過(guò)在 PPC 或 PPC64 平臺(tái)是不需要麻煩它的。

PS:有木有發(fā)現(xiàn)這些函數(shù)的命名規(guī)律哦?帶“Super”的是消息傳遞給超類;“stret”可分為“st”+“ret”兩部分,分別代表“struct”和“return”;“fpret”就是“fp”+“ret”,分別代表“floating-point”和“return”。

方法中的隱藏參數(shù)

我們經(jīng)常在方法中使用self關(guān)鍵字來(lái)引用實(shí)例本身,但從沒(méi)有想過(guò)為什么self就能取到調(diào)用當(dāng)前方法的對(duì)象吧。其實(shí)self的內(nèi)容是在方法運(yùn)行時(shí)被偷偷的動(dòng)態(tài)傳入的。

當(dāng)objc_msgSend找到方法對(duì)應(yīng)的實(shí)現(xiàn)時(shí),它將直接調(diào)用該方法實(shí)現(xiàn),并將消息中所有的參數(shù)都傳遞給方法實(shí)現(xiàn),同時(shí),它還將傳遞兩個(gè)隱藏的參數(shù):

  • 接收消息的對(duì)象(也就是self指向的內(nèi)容)
  • 方法選擇器(_cmd指向的內(nèi)容)

之所以說(shuō)它們是隱藏的是因?yàn)樵谠创a方法的定義中并沒(méi)有聲明這兩個(gè)參數(shù)。它們是在代碼被編譯時(shí)被插入實(shí)現(xiàn)中的。盡管這些參數(shù)沒(méi)有被明確聲明,在源代碼中我們?nèi)匀豢梢砸盟鼈?。在下面的例子中,self引用了接收者對(duì)象,而_cmd引用了方法本身的選擇器:

- strange
{
    id  target = getTheReceiver();
    SEL method = getTheMethod();
 
    if ( target == self || method == _cmd )
        return nil;
    return [target performSelector:method];
}

在這兩個(gè)參數(shù)中,self 更有用。實(shí)際上,它是在方法實(shí)現(xiàn)中訪問(wèn)消息接收者對(duì)象的實(shí)例變量的途徑。
而當(dāng)方法中的super關(guān)鍵字接收到消息時(shí),編譯器會(huì)創(chuàng)建一個(gè)objc_super結(jié)構(gòu)體:

struct objc_super { id receiver; Class class; };

這個(gè)結(jié)構(gòu)體指明了消息應(yīng)該被傳遞給特定超類的定義。但receiver仍然是self本身,這點(diǎn)需要注意,因?yàn)楫?dāng)我們想通過(guò)[super class]獲取超類時(shí),編譯器只是將指向selfid指針和classSEL傳遞給了objc_msgSendSuper函數(shù),因?yàn)橹挥性?code>NSObject類才能找到class方法,然后class方法調(diào)用object_getClass(),接著調(diào)用objc_msgSend(objc_super->receiver, @selector(class)),傳入的第一個(gè)參數(shù)是指向self的id指針,與調(diào)用[self class]相同,所以我們得到的永遠(yuǎn)都是self的類型。

獲取方法地址

IMP那節(jié)提到過(guò)可以避開消息綁定而直接獲取方法的地址并調(diào)用方法。這種做法很少用,除非是需要持續(xù)大量重復(fù)調(diào)用某方法的極端情況,避開消息發(fā)送泛濫而直接調(diào)用該方法會(huì)更高效。

NSObject類中有個(gè)methodForSelector:實(shí)例方法,你可以用它來(lái)獲取某個(gè)方法選擇器對(duì)應(yīng)的IMP,舉個(gè)栗子:

void (*setter)(id, SEL, BOOL);
int I;
 
setter = (void (*)(id, SEL, BOOL))[target
    methodForSelector:@selector(setFilled:)];
for ( i = 0 ; i < 1000 ; i++ )
    setter(targetList[i], @selector(setFilled:), YES);

當(dāng)方法被當(dāng)做函數(shù)調(diào)用時(shí),上節(jié)提到的兩個(gè)隱藏參數(shù)就需要我們明確給出了。上面的例子調(diào)用了1000次函數(shù),你可以試試直接給target發(fā)送1000次setFilled:消息會(huì)花多久。

PS:methodForSelector:方法是由CocoaRuntime系統(tǒng)提供的,而不是 Objc 自身的特性。

動(dòng)態(tài)方法解析

你可以動(dòng)態(tài)地提供一個(gè)方法的實(shí)現(xiàn)。例如我們可以用@dynamic關(guān)鍵字在類的實(shí)現(xiàn)文件中修飾一個(gè)屬性:

@dynamic propertyName;

這表明我們會(huì)為這個(gè)屬性動(dòng)態(tài)提供存取方法,也就是說(shuō)編譯器不會(huì)再默認(rèn)為我們生成setPropertyName:propertyName方法,而需要我們動(dòng)態(tài)提供。我們可以通過(guò)分別重載resolveInstanceMethod:resolveClassMethod:方法分別添加實(shí)例方法實(shí)現(xiàn)和類方法實(shí)現(xiàn)。因?yàn)楫?dāng) Runtime 系統(tǒng)在Cache和方法分發(fā)表中(包括超類)找不到要執(zhí)行的方法時(shí),Runtime會(huì)調(diào)用resolveInstanceMethod:或resolveClassMethod:來(lái)給程序員一次動(dòng)態(tài)添加方法實(shí)現(xiàn)的機(jī)會(huì)。我們需要用class_addMethod函數(shù)完成向特定類添加特定方法實(shí)現(xiàn)的操作:

void dynamicMethodIMP(id self, SEL _cmd) {
    // implementation ....
}
@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
    if (aSEL == @selector(resolveThisMethodDynamically)) {
          class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
          return YES;
    }
    return [super resolveInstanceMethod:aSEL];
}
@end

上面的例子為resolveThisMethodDynamically方法添加了實(shí)現(xiàn)內(nèi)容,也就是dynamicMethodIMP方法中的代碼。其中 “v@:” 表示返回值和參數(shù),這個(gè)符號(hào)涉及 Type Encoding

PS:動(dòng)態(tài)方法解析會(huì)在消息轉(zhuǎn)發(fā)機(jī)制浸入前執(zhí)行。如果 respondsToSelector:instancesRespondToSelector:方法被執(zhí)行,動(dòng)態(tài)方法解析器將會(huì)被首先給予一個(gè)提供該方法選擇器對(duì)應(yīng)的IMP的機(jī)會(huì)。如果你想讓該方法選擇器被傳送到轉(zhuǎn)發(fā)機(jī)制,那么就讓resolveInstanceMethod:返回NO。

評(píng)論區(qū)有人問(wèn)如何用 resolveClassMethod: 解析類方法,我將他貼出有問(wèn)題的代碼做了糾正和優(yōu)化后如下,可以順便將實(shí)例方法和類方法的動(dòng)態(tài)方法解析對(duì)比下:

#import <Foundation/Foundation.h>

@interface Student : NSObject
+ (void)learnClass:(NSString *) string;
- (void)goToSchool:(NSString *) name;
@end

#import "Student.h"
#import <objc/runtime.h>

@implementation Student
+ (BOOL)resolveClassMethod:(SEL)sel {
    if (sel == @selector(learnClass:)) {
        class_addMethod(object_getClass(self), sel, class_getMethodImplementation(object_getClass(self), @selector(myClassMethod:)), "v@:");
        return YES;
    }
    return [class_getSuperclass(self) resolveClassMethod:sel];
}

+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
    if (aSEL == @selector(goToSchool:)) {
        class_addMethod([self class], aSEL, class_getMethodImplementation([self class], @selector(myInstanceMethod:)), "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:aSEL];
}

+ (void)myClassMethod:(NSString *)string {
    NSLog(@"myClassMethod = %@", string);
}

- (void)myInstanceMethod:(NSString *)string {
    NSLog(@"myInstanceMethod = %@", string);
}
@end

需要深刻理解[self class]object_getClass(self)甚至 object_getClass([self class]) 的關(guān)系,其實(shí)并不難,重點(diǎn)在于 self 的類型:

  • 1、當(dāng) self為實(shí)例對(duì)象時(shí),[self class]object_getClass(self)等價(jià),因?yàn)榍罢邥?huì)調(diào)用后者。object_getClass([self class]) 得到元類。

  • 2、當(dāng) self為類對(duì)象時(shí),[self class] 返回值為自身,還是 self。object_getClass(self) 與 object_getClass([self class]) 等價(jià)。

凡是涉及到類方法時(shí),一定要弄清楚元類、selector、IMP 等概念,這樣才能做到舉一反三,隨機(jī)應(yīng)變。

消息轉(zhuǎn)發(fā)
重定向

在消息轉(zhuǎn)發(fā)機(jī)制執(zhí)行前,Runtime 系統(tǒng)會(huì)再給我們一次偷梁換柱的機(jī)會(huì),即通過(guò)重載- (id)forwardingTargetForSelector:(SEL)aSelector方法替換消息的接受者為其他對(duì)象:

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if(aSelector == @selector(mysteriousMethod:)){
        return alternateObject;
    }
    return [super forwardingTargetForSelector:aSelector];
}

畢竟消息轉(zhuǎn)發(fā)要耗費(fèi)更多時(shí)間,抓住這次機(jī)會(huì)將消息重定向給別人是個(gè)不錯(cuò)的選擇,不過(guò)千萬(wàn)別返回self,因?yàn)槟菢訒?huì)死循環(huán)。 如果此方法返回nilself,則會(huì)進(jìn)入消息轉(zhuǎn)發(fā)機(jī)制(forwardInvocation:);否則將向返回的對(duì)象重新發(fā)送消息。
如果想替換類方法的接受者,需要覆寫+ (id)forwardingTargetForSelector:(SEL)aSelector方法,并返回類對(duì)象:

+ (id)forwardingTargetForSelector:(SEL)aSelector {
    if(aSelector == @selector(xxx)) {
        return NSClassFromString(@"Class name");
    }
    return [super forwardingTargetForSelector:aSelector];
}

轉(zhuǎn)發(fā)

當(dāng)動(dòng)態(tài)方法解析不作處理返回NO時(shí),消息轉(zhuǎn)發(fā)機(jī)制會(huì)被觸發(fā)。在這時(shí)forwardInvocation:方法會(huì)被執(zhí)行,我們可以重寫這個(gè)方法來(lái)定義我們的轉(zhuǎn)發(fā)邏輯:

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    if ([someOtherObject respondsToSelector:
            [anInvocation selector]])
        [anInvocation invokeWithTarget:someOtherObject];
    else
        [super forwardInvocation:anInvocation];
}

該消息的唯一參數(shù)是個(gè)NSInvocation類型的對(duì)象——該對(duì)象封裝了原始的消息和消息的參數(shù)。我們可以實(shí)現(xiàn)forwardInvocation:方法來(lái)對(duì)不能處理的消息做一些默認(rèn)的處理,也可以將消息轉(zhuǎn)發(fā)給其他對(duì)象來(lái)處理,而不拋出錯(cuò)誤。

這里需要注意的是參數(shù)anInvocation是從哪的來(lái)的呢?其實(shí)在forwardInvocation:消息發(fā)送前,Runtime系統(tǒng)會(huì)向?qū)ο蟀l(fā)送methodSignatureForSelector:消息,并取到返回的方法簽名用于生成NSInvocation對(duì)象。所以我們?cè)谥貙?code>forwardInvocation:的同時(shí)也要重寫methodSignatureForSelector:方法,否則會(huì)拋異常。

當(dāng)一個(gè)對(duì)象由于沒(méi)有相應(yīng)的方法實(shí)現(xiàn)而無(wú)法響應(yīng)某消息時(shí),運(yùn)行時(shí)系統(tǒng)將通過(guò)forwardInvocation:消息通知該對(duì)象。每個(gè)對(duì)象都從NSObject類中繼承了forwardInvocation:方法。然而,NSObject中的方法實(shí)現(xiàn)只是簡(jiǎn)單地調(diào)用了doesNotRecognizeSelector:。通過(guò)實(shí)現(xiàn)我們自己的forwardInvocation:方法,我們可以在該方法實(shí)現(xiàn)中將消息轉(zhuǎn)發(fā)給其它對(duì)象。

forwardInvocation:方法就像一個(gè)不能識(shí)別的消息的分發(fā)中心,將這些消息轉(zhuǎn)發(fā)給不同接收對(duì)象?;蛘咚部梢韵笠粋€(gè)運(yùn)輸站將所有的消息都發(fā)送給同一個(gè)接收對(duì)象。它可以將一個(gè)消息翻譯成另外一個(gè)消息,或者簡(jiǎn)單的”吃掉“某些消息,因此沒(méi)有響應(yīng)也沒(méi)有錯(cuò)誤。forwardInvocation:方法也可以對(duì)不同的消息提供同樣的響應(yīng),這一切都取決于方法的具體實(shí)現(xiàn)。該方法所提供是將不同的對(duì)象鏈接到消息鏈的能力。

注意:forwardInvocation:方法只有在消息接收對(duì)象中無(wú)法正常響應(yīng)消息時(shí)才會(huì)被調(diào)用。 所以,如果我們希望一個(gè)對(duì)象將negotiate消息轉(zhuǎn)發(fā)給其它對(duì)象,則這個(gè)對(duì)象不能有negotiate方法。否則,forwardInvocation:將不可能會(huì)被調(diào)用。

轉(zhuǎn)發(fā)和多繼承

轉(zhuǎn)發(fā)和繼承相似,可以用于為Objc編程添加一些多繼承的效果。就像下圖那樣,一個(gè)對(duì)象把消息轉(zhuǎn)發(fā)出去,就好似它把另一個(gè)對(duì)象中的方法借過(guò)來(lái)或是“繼承”過(guò)來(lái)一樣。

這使得不同繼承體系分支下的兩個(gè)類可以“繼承”對(duì)方的方法,在上圖中WarriorDiplomat沒(méi)有繼承關(guān)系,但是Warriornegotiate消息轉(zhuǎn)發(fā)給了Diplomat后,就好似DiplomatWarrior的超類一樣。

消息轉(zhuǎn)發(fā)彌補(bǔ)了Objc不支持多繼承的性質(zhì),也避免了因?yàn)槎嗬^承導(dǎo)致單個(gè)類變得臃腫復(fù)雜。它將問(wèn)題分解得很細(xì),只針對(duì)想要借鑒的方法才轉(zhuǎn)發(fā),而且轉(zhuǎn)發(fā)機(jī)制是透明的。

替代者對(duì)象(Surrogate Objects)

轉(zhuǎn)發(fā)不僅能模擬多繼承,也能使輕量級(jí)對(duì)象代表重量級(jí)對(duì)象。弱小的女人背后是強(qiáng)大的男人,畢竟女人遇到難題都把它們轉(zhuǎn)發(fā)給男人來(lái)做了。這里有一些適用案例,可以參看官方文檔。

轉(zhuǎn)發(fā)與繼承

盡管轉(zhuǎn)發(fā)很像繼承,但是NSObject類不會(huì)將兩者混淆。像respondsToSelector:isKindOfClass:這類方法只會(huì)考慮繼承體系,不會(huì)考慮轉(zhuǎn)發(fā)鏈。比如上圖中一個(gè)Warrior對(duì)象如果被問(wèn)到是否能響應(yīng)negotiate消息:

if ( [aWarrior respondsToSelector:@selector(negotiate)] )
    ...

結(jié)果是NO,盡管它能夠接受negotiate消息而不報(bào)錯(cuò),因?yàn)樗哭D(zhuǎn)發(fā)消息給Diplomat類來(lái)響應(yīng)消息。

如果你為了某些意圖偏要“弄虛作假”讓別人以為Warrior繼承到了Diplomatnegotiate方法,你得重新實(shí)現(xiàn) respondsToSelector:isKindOfClass:來(lái)加入你的轉(zhuǎn)發(fā)算法:

- (BOOL)respondsToSelector:(SEL)aSelector
{
    if ( [super respondsToSelector:aSelector] )
        return YES;
    else {
        /* Here, test whether the aSelector message can     *
         * be forwarded to another object and whether that  *
         * object can respond to it. Return YES if it can.  */
    }
    return NO;
}

除了respondsToSelector:isKindOfClass:之外,instancesRespondToSelector:中也應(yīng)該寫一份轉(zhuǎn)發(fā)算法。如果使用了協(xié)議,conformsToProtocol:同樣也要加入到這一行列中。類似地,如果一個(gè)對(duì)象轉(zhuǎn)發(fā)它接受的任何遠(yuǎn)程消息,它得給出一個(gè)methodSignatureForSelector:來(lái)返回準(zhǔn)確的方法描述,這個(gè)方法會(huì)最終響應(yīng)被轉(zhuǎn)發(fā)的消息。比如一個(gè)對(duì)象能給它的替代者對(duì)象轉(zhuǎn)發(fā)消息,它需要像下面這樣實(shí)現(xiàn)methodSignatureForSelector::

- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
    NSMethodSignature* signature = [super methodSignatureForSelector:selector];
    if (!signature) {
       signature = [surrogate methodSignatureForSelector:selector];
    }
    return signature;
}

健壯的實(shí)例變量 (Non Fragile ivars)

Runtime 的現(xiàn)行版本中,最大的特點(diǎn)就是健壯的實(shí)例變量。當(dāng)一個(gè)類被編譯時(shí),實(shí)例變量的布局也就形成了,它表明訪問(wèn)類的實(shí)例變量的位置。從對(duì)象頭部開始,實(shí)例變量依次根據(jù)自己所占空間而產(chǎn)生位移:

上圖左邊是NSObject類的實(shí)例變量布局,右邊是我們寫的類的布局,也就是在超類后面加上我們自己類的實(shí)例變量,看起來(lái)不錯(cuò)。但試想如果哪天蘋果更新了NSObject類,發(fā)布新版本的系統(tǒng)的話,那就悲劇了:

我們自定義的類被劃了兩道線,那是因?yàn)槟菈K區(qū)域跟超類重疊了。唯有蘋果將超類改為以前的布局才能拯救我們,但這樣也導(dǎo)致它們不能再拓展它們的框架了,因?yàn)槌蓡T變量布局被死死地固定了。在脆弱的實(shí)例變量(Fragile ivars) 環(huán)境下我們需要重新編譯繼承自 Apple 的類來(lái)恢復(fù)兼容性。那么在健壯的實(shí)例變量下會(huì)發(fā)生什么呢?


在健壯的實(shí)例變量下編譯器生成的實(shí)例變量布局跟以前一樣,但是當(dāng) runtime 系統(tǒng)檢測(cè)到與超類有部分重疊時(shí)它會(huì)調(diào)整你新添加的實(shí)例變量的位移,那樣你在子類中新添加的成員就被保護(hù)起來(lái)了。

需要注意的是在健壯的實(shí)例變量下,不要使用sizeof(SomeClass),而是用class_getInstanceSize([SomeClass class])代替;也不要使用offsetof(SomeClass, SomeIvar),而要用ivar_getOffset(class_getInstanceVariable([SomeClass class], "SomeIvar"))來(lái)代替。

優(yōu)化 App 的啟動(dòng)時(shí)間 講過(guò)加載 Mach-O 文件時(shí)有個(gè)步驟是通過(guò) fix-up 修改偏移量來(lái)解決 fragile base class。

Objective-C Associated Objects

在 OS X 10.6 之后,Runtime系統(tǒng)讓Objc支持向?qū)ο髣?dòng)態(tài)添加變量。涉及到的函數(shù)有以下三個(gè):

void objc_setAssociatedObject ( id object, const void *key, id value, objc_AssociationPolicy policy );
id objc_getAssociatedObject ( id object, const void *key );
void objc_removeAssociatedObjects ( id object );

這些方法以鍵值對(duì)的形式動(dòng)態(tài)地向?qū)ο筇砑?、獲取或刪除關(guān)聯(lián)值。其中關(guān)聯(lián)政策是一組枚舉常量:

enum {
   OBJC_ASSOCIATION_ASSIGN  = 0,
   OBJC_ASSOCIATION_RETAIN_NONATOMIC  = 1,
   OBJC_ASSOCIATION_COPY_NONATOMIC  = 3,
   OBJC_ASSOCIATION_RETAIN  = 01401,
   OBJC_ASSOCIATION_COPY  = 01403
};

這些常量對(duì)應(yīng)著引用關(guān)聯(lián)值的政策,也就是 Objc 內(nèi)存管理的引用計(jì)數(shù)機(jī)制。有關(guān) Objective-C 引用計(jì)數(shù)機(jī)制的原理,可以查看這篇文章。

Method Swizzling

之前所說(shuō)的消息轉(zhuǎn)發(fā)雖然功能強(qiáng)大,但需要我們了解并且能更改對(duì)應(yīng)類的源代碼,因?yàn)槲覀冃枰獙?shí)現(xiàn)自己的轉(zhuǎn)發(fā)邏輯。當(dāng)我們無(wú)法觸碰到某個(gè)類的源代碼,卻想更改這個(gè)類某個(gè)方法的實(shí)現(xiàn)時(shí),該怎么辦呢?可能繼承類并重寫方法是一種想法,但是有時(shí)無(wú)法達(dá)到目的。這里介紹的是 Method Swizzling ,它通過(guò)重新映射方法對(duì)應(yīng)的實(shí)現(xiàn)來(lái)達(dá)到“偷天換日”的目的。跟消息轉(zhuǎn)發(fā)相比,Method Swizzling 的做法更為隱蔽,甚至有些冒險(xiǎn),也增大了debug的難度。

PS: 對(duì)于熟練使用 Method Swizzling 的開發(fā)者,可以跳過(guò)此章節(jié),看看我另一篇『稍微深入』一點(diǎn)的文章 Objective-C Method Swizzling

這里摘抄一個(gè) NSHipster 的例子:

#import <objc/runtime.h> 
 
@implementation UIViewController (Tracking) 
 
+ (void)load { 
    static dispatch_once_t onceToken; 
    dispatch_once(&onceToken, ^{ 
        Class aClass = [self class]; 
            // When swizzling a class method, use the following:
        // Class aClass = object_getClass((id)self);
        
        SEL originalSelector = @selector(viewWillAppear:); 
        SEL swizzledSelector = @selector(xxx_viewWillAppear:); 
 
        Method originalMethod = class_getInstanceMethod(aClass, originalSelector); 
        Method swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector); 
 
        BOOL didAddMethod = 
            class_addMethod(aClass, 
                originalSelector, 
                method_getImplementation(swizzledMethod), 
                method_getTypeEncoding(swizzledMethod)); 
 
        if (didAddMethod) { 
            class_replaceMethod(aClass, 
                swizzledSelector, 
                method_getImplementation(originalMethod), 
                method_getTypeEncoding(originalMethod)); 
        } else { 
            method_exchangeImplementations(originalMethod, swizzledMethod); 
        } 
    }); 
} 
 
#pragma mark - Method Swizzling 
 
- (void)xxx_viewWillAppear:(BOOL)animated { 
    [self xxx_viewWillAppear:animated]; 
    NSLog(@"viewWillAppear: %@", self); 
} 
 
@end

上面的代碼通過(guò)添加一個(gè)Tracking類別到UIViewController類中,將UIViewController類的viewWillAppear:方法和Tracking類別中xxx_viewWillAppear:方法的實(shí)現(xiàn)相互調(diào)換。Swizzling應(yīng)該在+load方法中實(shí)現(xiàn),因?yàn)?code>+load是在一個(gè)類最開始加載時(shí)調(diào)用。dispatch_onceGCD中的一個(gè)方法,它保證了代碼塊只執(zhí)行一次,并讓其為一個(gè)原子操作,線程安全是很重要的。

如果類中不存在要替換的方法,那就先用class_addMethodclass_replaceMethod函數(shù)添加和替換兩個(gè)方法的實(shí)現(xiàn);如果類中已經(jīng)有了想要替換的方法,那么就調(diào)用method_exchangeImplementations函數(shù)交換了兩個(gè)方法的 IMP,這是蘋果提供給我們用于實(shí)現(xiàn) Method Swizzling的便捷方法。
可能有人注意到了這行:

// When swizzling a class method, use the following:
// Class aClass = object_getClass((id)self);
// ...
// Method originalMethod = class_getClassMethod(aClass, originalSelector);
// Method swizzledMethod = class_getClassMethod(aClass, swizzledSelector);

object_getClass((id)self)[self class] 返回的結(jié)果類型都是 Class,但前者為元類,后者為其本身,因?yàn)榇藭r(shí) self 為 Class 而不是實(shí)例.注意 [NSObject class] 與 [object class] 的區(qū)別:

+ (Class)class {
    return self;
}

- (Class)class {
    return object_getClass(self);
}

PS:如果類中沒(méi)有想被替換實(shí)現(xiàn)的原方法時(shí),class_replaceMethod相當(dāng)于直接調(diào)用class_addMethod向類中添加該方法的實(shí)現(xiàn);否則調(diào)用method_setImplementation方法,types參數(shù)會(huì)被忽略。method_exchangeImplementations方法做的事情與如下的原子操作等價(jià):

IMP imp1 = method_getImplementation(m1);
IMP imp2 = method_getImplementation(m2);
method_setImplementation(m1, imp2);
method_setImplementation(m2, imp1);

最后xxx_viewWillAppear:方法的定義看似是遞歸調(diào)用引發(fā)死循環(huán),其實(shí)不會(huì)的。因?yàn)?code>[self xxx_viewWillAppear:animated]消息會(huì)動(dòng)態(tài)找到xxx_viewWillAppear:方法的實(shí)現(xiàn),而它的實(shí)現(xiàn)已經(jīng)被我們與viewWillAppear:方法實(shí)現(xiàn)進(jìn)行了互換,所以這段代碼不僅不會(huì)死循環(huán),如果你把[self xxx_viewWillAppear:animated]換成[self viewWillAppear:animated]反而會(huì)引發(fā)死循環(huán)。

看到有人說(shuō)+load方法本身就是線程安全的,因?yàn)樗诔绦騽傞_始就被調(diào)用,很少會(huì)碰到并發(fā)問(wèn)題,于是 stackoverflow 上也有大神給出了另一個(gè) Method Swizzling 的實(shí)現(xiàn):

- (void)replacementReceiveMessage:(const struct BInstantMessage *)arg1 {
    NSLog(@"arg1 is %@", arg1);
    [self replacementReceiveMessage:arg1];
}
+ (void)load {
    SEL originalSelector = @selector(ReceiveMessage:);
    SEL overrideSelector = @selector(replacementReceiveMessage:);
    Method originalMethod = class_getInstanceMethod(self, originalSelector);
    Method overrideMethod = class_getInstanceMethod(self, overrideSelector);
    if (class_addMethod(self, originalSelector, method_getImplementation(overrideMethod), method_getTypeEncoding(overrideMethod))) {
            class_replaceMethod(self, overrideSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    } else {
            method_exchangeImplementations(originalMethod, overrideMethod);
    }
}

上面的代碼同樣要添加在某個(gè)類的類別中,相比第一個(gè)種實(shí)現(xiàn),只是去掉了dispatch_once 部分。

Method Swizzling 的確是一個(gè)值得深入研究的話題,找了幾篇不錯(cuò)的資源推薦給大家:

在用 SpriteKit 寫游戲的時(shí)候,因?yàn)?API 本身有一些缺陷(增刪節(jié)點(diǎn)時(shí)不考慮父節(jié)點(diǎn)是否存在啊,很容易崩潰啊有木有!),我在 Swift 上使用 Method Swizzling彌補(bǔ)這個(gè)缺陷:

extension SKNode {
    
    class func yxy_swizzleAddChild() {
        let cls = SKNode.self
        let originalSelector = #selector(SKNode.addChild(_:))
        let swizzledSelector = #selector(SKNode.yxy_addChild(_:))
        let originalMethod = class_getInstanceMethod(cls, originalSelector)
        let swizzledMethod = class_getInstanceMethod(cls, swizzledSelector)
        method_exchangeImplementations(originalMethod!, swizzledMethod!)
    }
    
    class func yxy_swizzleRemoveFromParent() {
        let cls = SKNode.self
        let originalSelector = #selector(SKNode.removeFromParent)
        let swizzledSelector = #selector(SKNode.yxy_removeFromParent)
        let originalMethod = class_getInstanceMethod(cls, originalSelector)
        let swizzledMethod = class_getInstanceMethod(cls, swizzledSelector)
        method_exchangeImplementations(originalMethod!, swizzledMethod!)
    }
    
    @objc func yxy_addChild(_ node: SKNode) {
        if node.parent == nil {
            self.yxy_addChild(node)
        }
        else {
            print("This node has already a parent!\(String(describing: node.name))")
        }
    }
    
    @objc func yxy_removeFromParent() {
        if parent != nil {
            DispatchQueue.main.async(execute: { () -> Void in
                self.yxy_removeFromParent()
            })
        }
        else {
            print("This node has no parent!\(String(describing: name))")
        }
    }
    
}

然后其他地方調(diào)用那兩個(gè)類方法:

SKNode.yxy_swizzleAddChild()
SKNode.yxy_swizzleRemoveFromParent()

因?yàn)?Swift 中的 extension 的特殊性,最好在某個(gè)類的load() 方法中調(diào)用上面的兩個(gè)方法.我是在AppDelegate 中調(diào)用的,于是保證了應(yīng)用啟動(dòng)時(shí)能夠執(zhí)行上面兩個(gè)方法.

總結(jié)

我們之所以讓自己的類繼承 NSObject 不僅僅因?yàn)樘O果幫我們完成了復(fù)雜的內(nèi)存分配問(wèn)題,更是因?yàn)檫@使得我們能夠用上 Runtime 系統(tǒng)帶來(lái)的便利??赡芪覀兤綍r(shí)寫代碼時(shí)可能很少會(huì)考慮一句簡(jiǎn)單的 [receiver message] 背后發(fā)生了什么,而只是當(dāng)做方法或函數(shù)調(diào)用。深入理解 Runtime 系統(tǒng)的細(xì)節(jié)更有利于我們利用消息機(jī)制寫出功能更強(qiáng)大的代碼,比如 Method Swizzling 等。

Update 20170820: 使用 objc4-709 源碼重寫部分章節(jié),更新至 Swift 4 代碼示例。

參考鏈接:

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容