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

本文主要講解消息轉(zhuǎn)發(fā),需要對 Class 的結(jié)構(gòu) selector,IMP,元類等概念有一點的了解,如果之前沒了解,最好先暫停去了解一下

我們在 iOS 開發(fā)過程中應(yīng)該都有碰到過這樣的錯:unrecognized selector sent to instance **,是因為我們調(diào)用了一個不存在的方法,用 OC 消息機制來說,消息的接收者的 methods 列表中找不到 selector 對應(yīng)的方法實現(xiàn),這樣就啟動消息轉(zhuǎn)發(fā)機制。但是 Objective-C 提供了3次機會讓我們?nèi)パa救 動態(tài)添加方法實現(xiàn)轉(zhuǎn)發(fā)方法完整的消息轉(zhuǎn)發(fā)

首先寫一個 unrecognized selector sent to instance ** 例子,然后通過這三種方法分別一一解決:

// Person.h
@interface Person : NSObject
- (void)run;
@end
// Person.m
@implementation Person
@end
    
// main.m
int main(int argc, const char * argv[]) {
    Person *p = [[Person alloc] init];
    [p run];
    return 0;
}

動態(tài)添加方法實現(xiàn)

對象在收到 unrecognized selector sent to instance ** 錯誤的時候,首先會調(diào)用 + (BOOL)resolveInstanceMethod:(SEL)sel 或則 + (BOOL)resolveClassMethod:(SEL)sel 詢問是否有動態(tài)添加的方法來處理異常

void dynamicRun(id self, SEL _cmd)
{
    NSLog(@"這是c語言的run函數(shù) -- %@", self);
}

void dynamicBattle(id self, SEL _cmd)
{
    NSLog(@"這是c語言的battle函數(shù) -- %@", self);
}

- (void)wd_run {
    NSLog(@"running");
}

+ (void)wd_battle {
    NSLog(@"Battle");
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel_isEqual(sel, NSSelectorFromString(@"run"))) {
        // 添加OC方法
        Method method = class_getInstanceMethod(self, @selector(wd_run));
        IMP imp = method_getImplementation(method);
        class_addMethod([self class], sel, imp, method_getTypeEncoding(method));
        // 添加c函數(shù)
        class_addMethod([self class], sel, (IMP)dynamicRun, "v@:");
        
        return YES;
    }
    return NO;
}

+ (BOOL)resolveClassMethod:(SEL)sel {
    if (sel_isEqual(sel, NSSelectorFromString(@"battle"))) {
        // 獲取元類
        Class metaClass = object_getClass(self);
        // 添加OC方法
        Method method = class_getClassMethod(self, @selector(wd_battle));
        IMP imp = method_getImplementation(method);
        class_addMethod(metaClass, sel, imp, method_getTypeEncoding(method));
        // 添加c函數(shù)
        class_addMethod(metaClass, sel, (IMP)dynamicBattle, "v@:");
        
        return YES;
    }
    return NO;
}

當(dāng) Person 收到 未知選擇子 run 的時候,如果是實例方法,則會調(diào)用上文的 resolveInstanceMethod: 方法;如果是類方法,則會調(diào)用 resolveClassMethod: 方法。在方法內(nèi)部,我們可以通過 class_addMethod 方法動態(tài)添加一個 run 的實現(xiàn)方法來解決,并返回 YES,如果這一步不解決,則返回 NO,然后會進入第二步的 轉(zhuǎn)發(fā)

這里最值得注意的是,resolveClassMethod 內(nèi)部通過 class_addMethod 的時候是添加到 Person 的元類上的

轉(zhuǎn)發(fā)

如果在第一步的動態(tài)添加方法也沒解決的選擇子,會進入到這一步中,嘗試轉(zhuǎn)發(fā)給其他對象或類處理:

// Dog.h
@interface Dog : NSObject
- (void)run;
@end
// Dog.m
@implementation Dog
- (void)run {
    NSLog(@"Dog running");
}
@end
    
// Person.m
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (sel_isEqual(aSelector, @selector(run))) {
        return [[Dog alloc] init];
    }
}

可能有些人有疑問了,為什么只有 forwardingTargetForSelector: 只有實例方法,沒有對應(yīng)的類方法呢?Objective-C 中的方法調(diào)用都是消息發(fā)送,并不沒有明確表示調(diào)用的是實例方法還是類方法,只和接收者有關(guān)。也就是說消息消息接收者是實例對象,那么調(diào)用的就是實例方法;如果消息接收者是類對象,那么調(diào)用的就是類方法。下面可以試試轉(zhuǎn)發(fā)類方法:

// Dog.h
+ (void)battle;

// Dog.m
+ (void)battle {
    NSLog(@"Dog Battle");
}

// Person.m
+ (id)forwardingTargetForSelector:(SEL)aSelector {
    if (sel_isEqual(aSelector, @selector(battle))) {
        return [Dog class];
    }
    return [super forwardingTargetForSelector:aSelector];
}

沒報錯,控制臺也成功輸出了

Dog Battle

完整的消息轉(zhuǎn)發(fā)

如果第二步的 forwardingTargetForSelector 方法返回 nil,那么就會進入消息轉(zhuǎn)發(fā)的最后階段——完整的消息轉(zhuǎn)發(fā),這一步驟比較麻煩,需要實現(xiàn)2個方法: methodSignatureForSelector: 返回未知選擇子 run 的簽名; forwardInvocation: 拿到對應(yīng)的信息進行處理

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (sel_isEqual(aSelector, @selector(run))) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    if (sel_isEqual(anInvocation.selector, @selector(run))) {
        Dog *d = [[Dog alloc] init];
        [anInvocation invokeWithTarget:d];
    }
}

這時控制臺會打印

Dog running

對應(yīng)的類方法的完整消息轉(zhuǎn)發(fā),和第二步一樣的,只需要將實例方法 - 改成類方法 + 就行了

+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (sel_isEqual(aSelector, @selector(battle))) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

+ (void)forwardInvocation:(NSInvocation *)anInvocation {
    if (sel_isEqual(anInvocation.selector, @selector(battle))) {
        [anInvocation invokeWithTarget:[Dog class]];
    }
}

如果這三個步驟都沒實現(xiàn)的話,那么還是會調(diào)用 - (void)doesNotRecognizeSelector:(SEL)aSelector 這個方法報錯。當(dāng)然,線上環(huán)境肯定是不能出現(xiàn)此類問題的

最后附上用 Sketch 畫的流程圖:


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

其實之前有寫過消息轉(zhuǎn)發(fā)的文章,但是表達有點問題,就拖到了現(xiàn)在??

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

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

  • 消息轉(zhuǎn)發(fā)(Message Forwarding)是在查找 IMP 失敗后執(zhí)行一系列轉(zhuǎn)發(fā)流程的慢速通道,如果不作轉(zhuǎn)發(fā)...
    二豬哥閱讀 4,010評論 0 7
  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 2,041評論 0 9
  • 當(dāng)我們像一個對象發(fā)送消息[Receiver message],Receiver沒有實現(xiàn)該消息,即[Receiver...
    AlvinCrash閱讀 921評論 1 5
  • 消息轉(zhuǎn)發(fā)機制 假設(shè)說我們聲明一個類, 初始化對象, 并且在此類聲明一個方法, 調(diào)用方法的時候底層是怎么處理的呢? ...
    軟件iOS開發(fā)閱讀 338評論 0 0
  • 級別: ★★☆☆☆標(biāo)簽:「iOS」「消息轉(zhuǎn)發(fā)」「null([NSNull null])」作者: WYW[http...
    QiShare閱讀 4,143評論 1 27

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