iOS - 消息轉(zhuǎn)發(fā)機(jī)制

iOS - 方法查找流程一文中,提到過當(dāng)查找不到方法時(shí)會(huì)進(jìn)行動(dòng)態(tài)方法決議,如果動(dòng)態(tài)方法決議也找不到該怎么辦呢?那么我們就具體分析一下動(dòng)態(tài)方法決議找不到之后,系統(tǒng)會(huì)做些什么.

1、動(dòng)態(tài)方法決議
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]

        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}

通過判斷時(shí)實(shí)例方法還是類方法我們分別調(diào)用_class_resolveInstanceMethod和_class_resolveClassMethod;

1.1、實(shí)例方法: _class_resolveInstanceMethod
static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return NO;
}
  • 判斷是否實(shí)現(xiàn) SEL_resolveInstanceMethod 方法, 在NSObject 已經(jīng)實(shí)現(xiàn)這個(gè)方法 , 默認(rèn)返回為 NO,因此繼承NSObject的類不會(huì)走該方法;
  • 向類發(fā)送SEL_resolveInstanceMethod消息,調(diào)用該方法;
  • 調(diào)用解析器方法 ( SEL_resolveInstanceMethod ) 完成后 , 重新檢查有沒有這個(gè) sel 的 imp;
  • imp如果找到,則輸出動(dòng)態(tài)解析對象方法成功的日志
  • 如果imp 沒有找到,則輸出雖然實(shí)現(xiàn)了+(BOOL)resolveInstanceMethod:(SEL)sel,并且返回了 YES,但并沒有查找到imp的日志
    因此為了避免crash我們可以重寫+ (BOOL)resolveInstanceMethod:(SEL)sel方法,使其找到相應(yīng)的方法:
實(shí)例:

在NSObject類中添加兩個(gè)方法

@interface NSObject (E)

- (void)speak;

+ (void)sing;

@end
- (void)speak{
    NSLog(@"%s",__func__);
}

+ (void)sing{
    NSLog(@"%s",__func__);
}

創(chuàng)建Person類繼承于NSObject

@interface Person : NSObject

- (void)eat;

+ (void)shopping;

@end
@implementation Person
//Person中沒有方法實(shí)現(xiàn)
@end

創(chuàng)建Student類繼承于Person

@interface Student : Person

- (void)study;

+ (void)play;

@end

@implementation Student

- (void)study {
    NSLog(@"%s",__func__);
}

+ (void)play {
    NSLog(@"%s",__func__);
}

@end

//調(diào)用eat方法

int main(int argc, const char * argv[]) {
    @autoreleasepool {
#pragma clang diagnostic push
// 讓編譯器忽略錯(cuò)誤
#pragma clang diagnostic ignored "-Wundeclared-selector"
        Student *student = [[Student alloc] init];
        [student eat];
#pragma clang diagnostic pop
    }
    return 0;
}

由于Person類中并沒有eat方法的實(shí)現(xiàn),因此此處勢必找不到imp,程序會(huì)崩潰

2020-01-29 20:54:35.919681+0800 Test[3061:196810] -[Student eat]: unrecognized selector sent to instance 0x10060ed70
2020-01-29 20:54:35.921386+0800 Test[3061:196810] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Student eat]: unrecognized selector sent to instance 0x10060ed70'
*** First throw call stack:
(
    0   CoreFoundation                      0x00007fff39d398ab __exceptionPreprocess + 250
    1   libobjc.A.dylib                     0x00007fff6fff3805 objc_exception_throw + 48
    2   CoreFoundation                      0x00007fff39db8b61 -[NSObject(NSObject) __retain_OA] + 0
    3   CoreFoundation                      0x00007fff39c9dadf ___forwarding___ + 1427
    4   CoreFoundation                      0x00007fff39c9d4b8 _CF_forwarding_prep_0 + 120
    5   Test                                0x0000000100000c56 main + 86
    6   libdyld.dylib                       0x00007fff713617fd start + 1
    7   ???                                 0x0000000000000001 0x0 + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

那么如何能使程序不崩潰呢?由上面的分析可知,如果我們在Student類中重寫+ (BOOL)resolveInstanceMethod:(SEL)sel方法并在其中實(shí)現(xiàn)eat方法不就可以找到imp了嗎

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    NSLog(@"來了:%s - %@",__func__,NSStringFromSelector(sel));
    if (sel == @selector(eat)) {
//當(dāng)調(diào)用eat方法時(shí),調(diào)用study的方法實(shí)現(xiàn)
        NSLog(@"你媽喊你回家吃飯了");
        IMP studyIMP = class_getMethodImplementation(self, @selector(study));
        Method studyMethod = class_getInstanceMethod(self, @selector(study));
        const char *studyType = method_getTypeEncoding(studyMethod);
        return class_addMethod(self, sel, studyIMP, studyType);
    }

    return [super resolveInstanceMethod:sel];
}

打印結(jié)果:

2020-01-29 21:00:35.862697+0800 Test[3102:199221] 來了:+[Student resolveInstanceMethod:] - eat
2020-01-29 21:00:35.863553+0800 Test[3102:199221] 你媽喊你回家吃飯了
2020-01-29 21:00:35.863698+0800 Test[3102:199221] -[Student study]

由結(jié)果可知:重寫resolveInstanceMethod的確可以避免程序崩潰

1.2、類方法: _class_resolveClassMethod
static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
    assert(cls->isMetaClass());

    if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(_class_getNonMetaClass(cls, inst), 
                        SEL_resolveClassMethod, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveClassMethod adds to self->ISA() a.k.a. cls
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}
  • 跟實(shí)例方法下的動(dòng)態(tài)決議一樣,區(qū)別在于此時(shí)調(diào)用的是SEL_resolveClassMethod方法;
+ (BOOL)resolveClassMethod:(SEL)sel {
    return NO;
}
實(shí)例:

調(diào)用shopping方法

int main(int argc, const char * argv[]) {
    @autoreleasepool {
#pragma clang diagnostic push
// 讓編譯器忽略錯(cuò)誤
#pragma clang diagnostic ignored "-Wundeclared-selector"
        Student *student = [[Student alloc] init];
//        [student eat];
        [Student shopping];
#pragma clang diagnostic pop
    }
    return 0;
}
+ (BOOL)resolveClassMethod:(SEL)sel{

    NSLog(@"來了類方法:%s - %@",__func__,NSStringFromSelector(sel));

     if (sel == @selector(shopping)) {
         NSLog(@"開心購物");
//類方法存在元類中 因此要調(diào)用元類中的方法objc_getMetaClass("Student")
         IMP playIMP = class_getMethodImplementation(objc_getMetaClass("Student"), @selector(play));
         Method playMethod = class_getClassMethod(objc_getMetaClass("Student"), @selector(play));
         const char *playType = method_getTypeEncoding(playMethod);
         return class_addMethod(objc_getMetaClass("Student"), sel, playIMP, playType);
     }
     return [super resolveClassMethod:sel];
}

打印結(jié)果:

2020-01-29 21:15:25.626454+0800 Test[3139:205404] 來了類方法:+[Student resolveClassMethod:] - shopping
2020-01-29 21:15:25.627208+0800 Test[3139:205404] 開心購物
2020-01-29 21:15:25.627312+0800 Test[3139:205404] +[Student play]

如果在動(dòng)態(tài)決議下依舊找不到該方法時(shí),系統(tǒng)又會(huì)怎么處理呢?接下來將會(huì)進(jìn)入到詳細(xì)轉(zhuǎn)發(fā)機(jī)制.

2、消息轉(zhuǎn)發(fā)
2.1準(zhǔn)備階段

iOS - 方法查找流程一文中,我們提到了,在方法的查找過程中會(huì)調(diào)用log_and_fill_cache方法

static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
    if (objcMsgLogEnabled) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
    cache_fill (cls, sel, imp, receiver);
}
bool objcMsgLogEnabled = false;
bool objcMsgLogEnabled = false;
static int objcMsgLogFD = -1;

bool logMessageSend(bool isClassMethod,
                    const char *objectsClass,
                    const char *implementingClass,
                    SEL selector)
{
    char    buf[ 1024 ];

    // Create/open the log file
    if (objcMsgLogFD == (-1))
    {
        snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());
        objcMsgLogFD = secure_open (buf, O_WRONLY | O_CREAT, geteuid());
        if (objcMsgLogFD < 0) {
            // no log file - disable logging
            objcMsgLogEnabled = false;
            objcMsgLogFD = -1;
            return true;
        }
    }

    // Make the log entry
    snprintf(buf, sizeof(buf), "%c %s %s %s\n",
            isClassMethod ? '+' : '-',
            objectsClass,
            implementingClass,
            sel_getName(selector));

    objcMsgLogLock.lock();
    write (objcMsgLogFD, buf, strlen(buf));
    objcMsgLogLock.unlock();

    // Tell caller to not cache the method
    return false;
}
void instrumentObjcMessageSends(BOOL flag)
{
    bool enable = flag;

    // Shortcut NOP
    if (objcMsgLogEnabled == enable)
        return;

    // If enabling, flush all method caches so we get some traces
    if (enable)
        _objc_flush_caches(Nil);

    // Sync our log file
    if (objcMsgLogFD != -1)
        fsync (objcMsgLogFD);

    objcMsgLogEnabled = enable;
}

由源碼可知,是否打印日志取決于objcMsgLogEnabled的值,而objcMsgLogEnabled的值則取決于instrumentObjcMessageSends方法,因此我們可以通過instrumentObjcMessageSends方法來設(shè)置是否輸出日志,且該日志存儲(chǔ)在/tmp目錄下文件名為msgSends-"一串?dāng)?shù)字";

extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
#pragma clang diagnostic push
// 讓編譯器忽略錯(cuò)誤
#pragma clang diagnostic ignored "-Wundeclared-selector"
        Student *student = [[Student alloc] init];
        instrumentObjcMessageSends(true);
        [student eat];
        instrumentObjcMessageSends(false);
#pragma clang diagnostic pop
    }
    return 0;
}

查看日志

+ Student NSObject resolveInstanceMethod:
+ Student NSObject resolveInstanceMethod:
- Student NSObject forwardingTargetForSelector:
- Student NSObject forwardingTargetForSelector:
- Student NSObject methodSignatureForSelector:
- Student NSObject methodSignatureForSelector:
- Student NSObject class
+ Student NSObject resolveInstanceMethod:
+ Student NSObject resolveInstanceMethod:
- Student NSObject doesNotRecognizeSelector:
- Student NSObject doesNotRecognizeSelector:
- Student NSObject class

由日志我們可以看出,系統(tǒng)不但執(zhí)行了動(dòng)態(tài)決議的方法還執(zhí)了一系列的其他的方法;

2.2快速轉(zhuǎn)發(fā)
forwardingTargetForSelector
+ (id)forwardingTargetForSelector:(SEL)sel {
    return nil;
}

- (id)forwardingTargetForSelector:(SEL)sel {
    return nil;
}

發(fā)現(xiàn)在這里什么都看不出來,那么我們就去官方文檔查看該方法的具體解釋是什么.

This method gives an object a chance to redirect an unknown message sent to it before the much more expensive forwardInvocation: machinery takes over. This is useful when you simply want to redirect messages to another object and can be an order of magnitude faster than regular forwarding. It is not useful where the goal of the forwarding is to capture the NSInvocation, or manipulate the arguments or return value during the forwarding.
大致的意思是:這個(gè)方法給了未知消息在進(jìn)入forwardInvocation:方法前一次重新定義的機(jī)會(huì),當(dāng)您只想將消息重定向到另一個(gè)對象時(shí),這非常有用,并且可以比常規(guī)轉(zhuǎn)發(fā)快一個(gè)數(shù)量級(jí)。當(dāng)轉(zhuǎn)發(fā)的目標(biāo)是NSInvocation對象時(shí),或者在轉(zhuǎn)發(fā)帶有參數(shù)或返回值時(shí),它不起作用

例如我們創(chuàng)建一個(gè)Teacher類繼承于NSObject

@interface Teacher : NSObject

@end

@implementation Teacher

- (void)eat{
    NSLog(@"%s",__func__);
}
@end

在Student類中我們不在實(shí)現(xiàn)動(dòng)態(tài)決議的方法,而是實(shí)現(xiàn)forwardingTargetForSelector;

- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
    if (aSelector == @selector(eat)) {
        return [Teacher alloc];
    }
    return [super forwardingTargetForSelector:aSelector];
}

打印結(jié)果:

2020-01-29 22:16:17.270894+0800 Test[3315:238995] -[Student forwardingTargetForSelector:] -- eat
2020-01-29 22:16:17.271522+0800 Test[3315:238995] -[Teacher eat]

由此可知,我們可以利用這個(gè)方法將A類中的方法轉(zhuǎn)發(fā)到B類中去實(shí)現(xiàn);

2.3慢速轉(zhuǎn)發(fā)
methodSignatureForSelector

This method is used in the implementation of protocols. This method is also used in situations where an NSInvocation object must be created, such as during message forwarding. If your object maintains a delegate or is capable of handling messages that it does not directly implement, you should override this method to return an appropriate method signature.
大致意思是: 此方法用于協(xié)議的實(shí)現(xiàn)。 在必須創(chuàng)建NSInvocation對象的情況下(例如,在消息轉(zhuǎn)發(fā)期間),也可以使用此方法。 如果您的對象維護(hù)一個(gè)委托或能夠處理它不直接實(shí)現(xiàn)的消息,則應(yīng)重寫此方法以返回適當(dāng)?shù)姆椒ê灻?/p>

resolveInstanceMethod

To respond to methods that your object does not itself recognize, you must override methodSignatureForSelector: in addition to forwardInvocation:. The mechanism for forwarding messages uses information obtained from methodSignatureForSelector: to create the NSInvocation object to be forwarded. Your overriding method must provide an appropriate method signature for the given selector, either by pre formulating one or by asking another object for one.

大致意思是: 要響應(yīng)對象本身無法識(shí)別的方法,除了forwardInvocation:外,還必須重寫methodSignatureForSelector:。 轉(zhuǎn)發(fā)消息的機(jī)制使用從methodSignatureForSelector:獲得的信息來創(chuàng)建要轉(zhuǎn)發(fā)的NSInvocation對象。 您的重寫方法必須為給定的選擇器提供適當(dāng)?shù)姆椒ê灻?,方法是預(yù)先制定一個(gè)公式,也可以要求另一個(gè)對象提供一個(gè)方法簽名。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
    if (aSelector == @selector(eat)) { // v @ :
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
     NSLog(@"%s",__func__);
     SEL aSelector = [anInvocation selector];

    if ([[Teacher alloc] respondsToSelector:aSelector])
        [anInvocation invokeWithTarget:[Teacher alloc]];
    else
        [super forwardInvocation:anInvocation];
}

關(guān)于v@:的含義,請查看官方文檔:方法簽名encode表
打印結(jié)果:

2020-01-29 22:29:59.982181+0800 Test[3360:247313] -[Student methodSignatureForSelector:] -- eat
2020-01-29 22:29:59.983108+0800 Test[3360:247313] -[Student forwardInvocation:]
2020-01-29 22:29:59.983341+0800 Test[3360:247313] -[Teacher eat]

即使forwardInvocation中不是實(shí)現(xiàn)后續(xù)方法也不會(huì)崩潰,這次的轉(zhuǎn)發(fā)作用和第二次的比較類似,都是將 A 類的某個(gè)方法,轉(zhuǎn)發(fā)到 B 類的實(shí)現(xiàn)中去。不同的是,第三次的轉(zhuǎn)發(fā)相對于第二次更加靈活,forwardingTargetForSelector: 只能固定的轉(zhuǎn)發(fā)到一個(gè)對象;forwardInvocation: 可以讓我們轉(zhuǎn)發(fā)到多個(gè)對象中去.

2.4消息無法處理
doesNotRecognizeSelector
- (void)doesNotRecognizeSelector:(SEL)sel {
    _objc_fatal("-[%s %s]: unrecognized selector sent to instance %p", 
                object_getClassName(self), sel_getName(sel), self);
}

報(bào)出異常錯(cuò)誤;

3.總結(jié)
  • 在動(dòng)態(tài)方法決議無法處理時(shí),將會(huì)執(zhí)行消息轉(zhuǎn)發(fā)機(jī)制,首先執(zhí)行的是快速轉(zhuǎn)發(fā),即調(diào)用forwardingTargetForSelector;
  • 當(dāng)快速轉(zhuǎn)發(fā)也無法對消息進(jìn)行處理時(shí),則執(zhí)行慢速轉(zhuǎn)發(fā),調(diào)用methodSignatureForSelector和resolveInstanceMethod;
  • 當(dāng)慢速轉(zhuǎn)發(fā)也無法對消息進(jìn)行處理時(shí),則拋出異常,調(diào)用doesNotRecognizeSelector方法,打印錯(cuò)誤信息;
附:消息轉(zhuǎn)發(fā)流程圖
消息轉(zhuǎn)發(fā)流程.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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