runtime進(jìn)行曲,objc_msgSend的前世今生(二)

概要:傻瓜式講解動(dòng)態(tài)綁定和消息轉(zhuǎn)發(fā)。
學(xué)習(xí)進(jìn)度:

一、objc_msgSend偽代碼復(fù)習(xí)

偽代碼

// 首先看一下objc_msgSend的方法實(shí)現(xiàn)的偽代碼
id objc_msgSend(id self, SEL op, ...) {
   if (!self) return nil;
   // 關(guān)鍵代碼(a)
   IMP imp = class_getMethodImplementation(self->isa, SEL op);
   imp(self, op, ...); // 調(diào)用這個(gè)函數(shù),偽代碼...
}
// 查找IMP
IMP class_getMethodImplementation(Class cls, SEL sel) {
    if (!cls || !sel) return nil;
    IMP imp = lookUpImpOrNil(cls, sel);
    if (!imp) {
      ... // 執(zhí)行動(dòng)態(tài)綁定
    }
    IMP imp = lookUpImpOrNil(cls, sel);
    if (!imp) return _objc_msgForward; // 這個(gè)是用于消息轉(zhuǎn)發(fā)的
    return imp;
}
// 遍歷繼承鏈,查找IMP
IMP lookUpImpOrNil(Class cls, SEL sel) {
    if (!cls->initialize()) {
        _class_initialize(cls);
    }
    Class curClass = cls;
    IMP imp = nil;
    do { // 先查緩存,緩存沒有時(shí)重建,仍舊沒有則向父類查詢
        if (!curClass) break;
        if (!curClass->cache) fill_cache(cls, curClass);
        imp = cache_getImp(curClass, sel);
        if (imp) break;
    } while (curClass = curClass->superclass); // 關(guān)鍵代碼(b)
    return imp;
}

問題
偽代碼中大部分在runtime進(jìn)行曲,objc_msgSend的前世今生(一)中已經(jīng)說明的很詳細(xì),這里看下上篇中留下的兩個(gè)小疑問:

  • 動(dòng)態(tài)綁定。
  • 消息轉(zhuǎn)發(fā)。

二、動(dòng)態(tài)綁定

動(dòng)態(tài)綁定,從名稱來看就大致懂了。如果調(diào)用一個(gè)類的方法,而這個(gè)類及其父類均沒有實(shí)現(xiàn)這個(gè)方法。那么我們就在運(yùn)行時(shí)綁定此方法到該類。舉一例子如下:

// 使用@dynamic表明不自動(dòng)合成屬性a的set和get方法。
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface A : NSObject
@property (nonatomic, assign) NSInteger a;
@end
@implementation A
@dynamic a;
@end
int main(int argc, char * argv[]) {
    A *aObject = [[A alloc] init];
    NSLog(@"%ld", aObject.a);   // 崩于此行
}

// 執(zhí)行結(jié)果:crash,報(bào)錯(cuò)如下
2017-01-09 21:25:06.929 block[28341:228218] -[A a]: unrecognized selector sent to instance 0x60800000c580

參照一中objc_msgSend執(zhí)行步驟,可知A類并沒有動(dòng)態(tài)綁定和消息轉(zhuǎn)發(fā),所以返回的imp為空,執(zhí)行crash。下面我們?yōu)槠浼尤雱?dòng)態(tài)綁定的方法。

// 代碼
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface A : NSObject
@property (nonatomic, assign) NSInteger a;
@end
@implementation A
@dynamic a;
int a(id self, SEL _cmd) {
    return 1;
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    class_addMethod([self class], @selector(a), (IMP)a, "i@:");
    return YES;
}
@end
int main(int argc, char * argv[]) {
    A *aObject = [[A alloc] init];
    NSLog(@"%ld", aObject.a);
}

// 沒有crash,并輸出1
2017-01-09 21:50:11.314 block[30605:247164] 1

OC中的方法實(shí)質(zhì)上就是一個(gè)有id self和 SEL _cmd兩個(gè)參數(shù)的C方法。

這里的aObject.a中a為實(shí)例方法,那么類方法怎么進(jìn)行動(dòng)態(tài)綁定?即通過resolveClassMethod方法。

// 代碼
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface A : NSObject
@end
@implementation A
void b(id self, SEL _cmd) {
    NSLog(@"b");
}
+ (BOOL)resolveClassMethod:(SEL)sel {
    class_addMethod([self class], @selector(b), (IMP)b, "v@:");
    return YES;
}
@end
int main(int argc, char * argv[]) {
    [[A class] performSelector:@selector(b)]; // 因?yàn)閇A b];這種調(diào)用方式會(huì)編譯錯(cuò)誤,所以動(dòng)態(tài)調(diào)用
}

// 打斷點(diǎn)查看,雖然調(diào)用了resolveClassMethod,但還是crash
017-01-09 22:26:58.671 block[34171:285065] +[A b]: unrecognized selector sent to class 0x1037b5e30

參照上一篇文章中,A的class中只存有實(shí)例方法,A的metaClass中只存有類方法,而相應(yīng)的調(diào)用也是如此,即實(shí)例方法在A的class中找,而類方法在A的metaClass中找。所以上述給A class添加方法b并沒有作用,僅僅是添加了一個(gè)實(shí)例方法b。正確方法如下:

// 代碼
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface A : NSObject
@end
@implementation A
void b(id self, SEL _cmd) {
    NSLog(@"b");
}
+ (BOOL)resolveClassMethod:(SEL)sel {
    Class aMeta = objc_getMetaClass(class_getName([self class]));
    class_addMethod([aMeta class], @selector(b), (IMP)b, "v@:");
    return YES;
}
@end
int main(int argc, char * argv[]) {
    [[A class] performSelector:@selector(b)];
}

// 沒有crash,輸出b,無敵
2017-01-09 22:31:53.440 block[34634:289598] b

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

參見一中查找IMP代碼。

// 查找IMP
IMP class_getMethodImplementation(Class cls, SEL sel) {
    if (!cls || !sel) return nil;
    IMP imp = lookUpImpOrNil(cls, sel);
    if (!imp) {
      ... // 執(zhí)行動(dòng)態(tài)綁定
    }
    IMP imp = lookUpImpOrNil(cls, sel);
    if (!imp) return _objc_msgForward; // 這個(gè)是用于消息轉(zhuǎn)發(fā)的
    return imp;
}

可知,消息轉(zhuǎn)發(fā)是objc_msgSend的最后一道防線。如果找不到imp,則會(huì)調(diào)用下面方法拋出異常。

- (void)doesNotRecognizeSelector:(SEL)aSelector;

而在調(diào)用doesNotRecognizeSelector之前,會(huì)先調(diào)用方法(對(duì)于實(shí)例方法)。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;

若此方法能為另一個(gè)類的消息創(chuàng)建一個(gè)有效的方法簽名(當(dāng)另一個(gè)類中有aSelector則可以創(chuàng)建)。創(chuàng)建方式如下:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature* signature = [super methodSignatureForSelector:aSelector];
    if (!signature) {
        B *bObject = [[B alloc] init]; // 假設(shè)B中有實(shí)例方法aSelector
        signature = [bObject methodSignatureForSelector:aSelector];
    }
    return signature;
}

若返回值signature為nil,則執(zhí)行doesNotRecognizeSelector拋出異常,若signature簽名成功,則執(zhí)行轉(zhuǎn)發(fā)方法。

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    B *bObject = [[B alloc] init];
    [anInvocation invokeWithTarget:bObject];
}

當(dāng)然,進(jìn)入forwardInvocation中之后就不會(huì)在調(diào)用起本類的doesNotRecognizeSelector方法了。除非將這個(gè)消息又轉(zhuǎn)回自己(如果調(diào)用某個(gè)對(duì)象的方法沒找到,則調(diào)用相應(yīng)類的doesNotRecognizeSelector拋出異常)。又比如,若forwardInvocation什么都不寫,則不會(huì)有任何現(xiàn)象,也不會(huì)crash,也不會(huì)拋出異常。

下面看一下完整的實(shí)例方法轉(zhuǎn)發(fā)代碼:

#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface B : NSObject
- (void)b;
@end
@implementation B
- (void)b {
    NSLog(@"b");
}
- (void)doesNotRecognizeSelector:(SEL)aSelector {
    [super doesNotRecognizeSelector:aSelector];
}
@end
@interface A : NSObject
@end
@implementation A

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature* signature = [super methodSignatureForSelector:aSelector];
    if (!signature) {
        B *bObject = [[B alloc] init];
        signature = [bObject methodSignatureForSelector:aSelector];
    }
    return signature;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    B *bObject = [[B alloc] init];
    [anInvocation invokeWithTarget:bObject];
}

- (void)doesNotRecognizeSelector:(SEL)aSelector {
    [super doesNotRecognizeSelector:aSelector];
}
@end
int main(int argc, char * argv[]) {
    A *aObject = [[A alloc] init];
    [aObject performSelector:@selector(b)];
}

實(shí)例方法的轉(zhuǎn)發(fā)大概講完了,接下來看下類方法的轉(zhuǎn)發(fā)。和實(shí)例方法類似,有兩個(gè)需要額外注意的地方。

  • 實(shí)例方法是在B的class中查找b方法,若查找類方法,需要在B的metaClass中查找。
  • 上述代碼中的methodSignatureForSelector、forwardInvocation、doesNotRecognizeSelector在類方法的轉(zhuǎn)發(fā)過程不會(huì)被觸發(fā),需要將前面的“-”換成“+”才會(huì)被觸發(fā)(畢竟是查找類方法,有點(diǎn)區(qū)別)。

類方法轉(zhuǎn)發(fā)代碼如下:

#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface B : NSObject
+ (void)b;
@end
@implementation B
+ (void)b {
    NSLog(@"b");
}
- (void)doesNotRecognizeSelector:(SEL)aSelector {
    [super doesNotRecognizeSelector:aSelector];
}
@end
@interface A : NSObject
@end
@implementation A

+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature* signature = [super methodSignatureForSelector:aSelector];
    if (!signature) {
        Class bMeta = objc_getMetaClass(class_getName([B class]));
        signature = [[bMeta class] instanceMethodSignatureForSelector:aSelector];
    }
    return signature;
}

+ (void)forwardInvocation:(NSInvocation *)anInvocation {
    [anInvocation invokeWithTarget:[B class]];
}

+ (void)doesNotRecognizeSelector:(SEL)aSelector {
    [super doesNotRecognizeSelector:aSelector];
}
@end
int main(int argc, char * argv[]) {
    [[A class] performSelector:@selector(b)];
}

四、消息轉(zhuǎn)發(fā)補(bǔ)充

1、forwardingTargetForSelector

看到現(xiàn)在,不少曾經(jīng)看過消息轉(zhuǎn)發(fā)的一些文章的讀友可能發(fā)現(xiàn),有一個(gè)函數(shù)沒有被提到:

- (id)forwardingTargetForSelector:(SEL)aSelector;

那么,它是做什么的呢?經(jīng)過測(cè)試,該函數(shù)會(huì)在methodSignatureForSelector調(diào)用之前進(jìn)行調(diào)用,來看一下是否可以進(jìn)行轉(zhuǎn)發(fā)。下面寫一個(gè)forwardingTargetForSelector實(shí)現(xiàn)轉(zhuǎn)發(fā)的樣例:

// 下述為類方法的轉(zhuǎn)發(fā)樣例,如果是實(shí)例方法,需要將forwardingTargetForSelector改為實(shí)例方法
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface B : NSObject
+ (void)b;
@end
@implementation B
+ (void)b {
    NSLog(@"b");
}
@end
@interface A : NSObject
@end
@implementation A
+ (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(b)) {
        return [B class];
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end
int main(int argc, char * argv[]) {
    [[A class] performSelector:@selector(b)];
}

// 輸出
2017-01-10 08:54:39.007 block[52966:479279] b

那么,既然forwardingTargetForSelector可以實(shí)現(xiàn)消息轉(zhuǎn)發(fā),為什么還要使用forwardInvocation作為消息管理中心呢?

  • 雖然,forwardingTargetForSelector使用簡(jiǎn)單,不需要重寫methodSignatureForSelector,產(chǎn)生的消耗也比forwardInvocation低得多。
  • 但是,forwardingTargetForSelector無法獲取當(dāng)前的NSInvocation,或者說少了一些可以操作的值。

2、respondsToSelector:和isKindOfClass:

若不進(jìn)行重寫,respondsToSelector:和isKindOfClass:均只會(huì)作用于繼承鏈,而不會(huì)觸及轉(zhuǎn)發(fā)。假設(shè)我在四.1中:

// 代碼
NSLog(@"%i", [[A class] respondsToSelector:@selector(b)]);

// 雖然A中實(shí)現(xiàn)了b方法的轉(zhuǎn)發(fā),但是respondsToSelector:并不會(huì)查看
2017-01-10 09:10:47.921 block[54479:495382] 0

當(dāng)然,我們可以重寫respondsToSelector:來保證消息轉(zhuǎn)發(fā)鏈也可以響應(yīng)。

// 代碼,前面為+是因?yàn)檫@里看的是類方法b
+ (BOOL)respondsToSelector:(SEL)aSelector
{
    if ( [super respondsToSelector:aSelector] )
        return YES;
    else {
        if ([[B class] respondsToSelector:@selector(b)])
            return YES;
    }
    return NO;
}

// 調(diào)用NSLog(@"%i", [[A class] respondsToSelector:@selector(b)]);輸出
2017-01-10 09:14:59.227 block[54899:500487] 1

同樣的道理,isKindOfClass:、instancesRespondToSelector:和 conformsToProtocol:都會(huì)有相同的機(jī)制。

五、消息轉(zhuǎn)發(fā)應(yīng)用

1、多重繼承

根據(jù)上述任意消息轉(zhuǎn)發(fā)樣例,可知實(shí)現(xiàn)了在A中調(diào)用B中的方法b,大概可以猜到,這是不是類似繼承機(jī)制?答案是肯定的,因?yàn)镺C不支持多繼承,此處就給了一個(gè)實(shí)現(xiàn)多繼承的方式,因?yàn)槲覀兛梢詫?shí)現(xiàn)任意個(gè)類消息的轉(zhuǎn)發(fā),這里就不舉例了。

這是一個(gè)先進(jìn)的技術(shù),只適用于沒有其他解決方案的情況下。它能作為繼承的替代品。如果必須使用這種技術(shù),請(qǐng)確保您充分了解轉(zhuǎn)發(fā)的類和被轉(zhuǎn)發(fā)的類的行為。

2、NSProxy使用和面向切面編程

暫時(shí)沒空寫這部分,參見神經(jīng)病院Objective-C Runtime住院第二天——消息發(fā)送與轉(zhuǎn)發(fā)第四節(jié)。

3、JSPatch

JSPatch中也用到消息轉(zhuǎn)發(fā)相關(guān)內(nèi)容,暫不介紹,后續(xù)后有專門的文章。

六、消息傳遞流程圖

objc_msgSend全過程(假設(shè)存在動(dòng)態(tài)綁定)

七、文獻(xiàn)

1、https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtForwarding.html#//apple_ref/doc/uid/TP40008048-CH105-SW1
2、http://www.itdecent.cn/p/4d619b097e20

最后編輯于
?著作權(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ù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 2,083評(píng)論 0 9
  • 轉(zhuǎn)載:http://yulingtianxia.com/blog/2014/11/05/objective-c-r...
    F麥子閱讀 841評(píng)論 0 2
  • 參考鏈接: http://www.cnblogs.com/ioshe/p/5489086.html 簡(jiǎn)介 Runt...
    樂樂的簡(jiǎn)書閱讀 2,251評(píng)論 0 9
  • Runtime是什么 Runtime 又叫運(yùn)行時(shí),是一套底層的 C 語(yǔ)言 API,其為 iOS 內(nèi)部的核心之一,我...
    SuAdrenine閱讀 987評(píng)論 0 3
  • 我所自學(xué)的內(nèi)容都是Python3的,所以后面的內(nèi)容也是Python3的,操作系統(tǒng)為MacOS,后面也會(huì)出現(xiàn)Linu...
    ZYiDa閱讀 597評(píng)論 0 0

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