iOS - 消息转发机制

iOS - 方法查找流程一文中,提到过当查找不到方法时会进行动态方法决议,如果动态方法决议也找不到该怎么办呢?那么我们就具体分析一下动态方法决议找不到之后,系统会做些什么.

1、动态方法决议
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);
        }
    }
}

通过判断时实例方法还是类方法我们分别调用_class_resolveInstanceMethod和_class_resolveClassMethod;

1.1、实例方法: _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;
}
  • 判断是否实现 SEL_resolveInstanceMethod 方法, 在NSObject 已经实现这个方法 , 默认返回为 NO,因此继承NSObject的类不会走该方法;
  • 向类发送SEL_resolveInstanceMethod消息,调用该方法;
  • 调用解析器方法 ( SEL_resolveInstanceMethod ) 完成后 , 重新检查有没有这个 sel 的 imp;
  • imp如果找到,则输出动态解析对象方法成功的日志
  • 如果imp 没有找到,则输出虽然实现了+(BOOL)resolveInstanceMethod:(SEL)sel,并且返回了 YES,但并没有查找到imp的日志
    因此为了避免crash我们可以重写+ (BOOL)resolveInstanceMethod:(SEL)sel方法,使其找到相应的方法:
实例:

在NSObject类中添加两个方法

@interface NSObject (E)

- (void)speak;

+ (void)sing;

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

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

创建Person类继承于NSObject

@interface Person : NSObject

- (void)eat;

+ (void)shopping;

@end
@implementation Person
//Person中没有方法实现
@end

创建Student类继承于Person

@interface Student : Person

- (void)study;

+ (void)play;

@end

@implementation Student

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

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

@end

//调用eat方法

int main(int argc, const char * argv[]) {
    @autoreleasepool {
#pragma clang diagnostic push
// 让编译器忽略错误
#pragma clang diagnostic ignored "-Wundeclared-selector"
        Student *student = [[Student alloc] init];
        [student eat];
#pragma clang diagnostic pop
    }
    return 0;
}

由于Person类中并没有eat方法的实现,因此此处势必找不到imp,程序会崩溃

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方法并在其中实现eat方法不就可以找到imp了吗

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    NSLog(@"来了:%s - %@",__func__,NSStringFromSelector(sel));
    if (sel == @selector(eat)) {
//当调用eat方法时,调用study的方法实现
        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];
}

打印结果:

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]

由结果可知:重写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));
        }
    }
}
  • 跟实例方法下的动态决议一样,区别在于此时调用的是SEL_resolveClassMethod方法;
+ (BOOL)resolveClassMethod:(SEL)sel {
    return NO;
}
实例:

调用shopping方法

int main(int argc, const char * argv[]) {
    @autoreleasepool {
#pragma clang diagnostic push
// 让编译器忽略错误
#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(@"开心购物");
//类方法存在元类中 因此要调用元类中的方法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];
}

打印结果:

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]

如果在动态决议下依旧找不到该方法时,系统又会怎么处理呢?接下来将会进入到详细转发机制.

2、消息转发
2.1准备阶段

iOS - 方法查找流程一文中,我们提到了,在方法的查找过程中会调用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方法来设置是否输出日志,且该日志存储在/tmp目录下文件名为msgSends-"一串数字";

extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
#pragma clang diagnostic push
// 让编译器忽略错误
#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

由日志我们可以看出,系统不但执行了动态决议的方法还执了一系列的其他的方法;

2.2快速转发
forwardingTargetForSelector
+ (id)forwardingTargetForSelector:(SEL)sel {
    return nil;
}

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

发现在这里什么都看不出来,那么我们就去官方文档查看该方法的具体解释是什么.

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.
大致的意思是:这个方法给了未知消息在进入forwardInvocation:方法前一次重新定义的机会,当您只想将消息重定向到另一个对象时,这非常有用,并且可以比常规转发快一个数量级。当转发的目标是NSInvocation对象时,或者在转发带有参数或返回值时,它不起作用

例如我们创建一个Teacher类继承于NSObject

@interface Teacher : NSObject

@end

@implementation Teacher

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

在Student类中我们不在实现动态决议的方法,而是实现forwardingTargetForSelector;

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

打印结果:

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]

由此可知,我们可以利用这个方法将A类中的方法转发到B类中去实现;

2.3慢速转发
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.
大致意思是: 此方法用于协议的实现。 在必须创建NSInvocation对象的情况下(例如,在消息转发期间),也可以使用此方法。 如果您的对象维护一个委托或能够处理它不直接实现的消息,则应重写此方法以返回适当的方法签名。

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.

大致意思是: 要响应对象本身无法识别的方法,除了forwardInvocation:外,还必须重写methodSignatureForSelector:。 转发消息的机制使用从methodSignatureForSelector:获得的信息来创建要转发的NSInvocation对象。 您的重写方法必须为给定的选择器提供适当的方法签名,方法是预先制定一个公式,也可以要求另一个对象提供一个方法签名。

- (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];
}

关于v@:的含义,请查看官方文档:方法签名encode表
打印结果:

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中不是实现后续方法也不会崩溃,这次的转发作用和第二次的比较类似,都是将 A 类的某个方法,转发到 B 类的实现中去。不同的是,第三次的转发相对于第二次更加灵活,forwardingTargetForSelector: 只能固定的转发到一个对象;forwardInvocation: 可以让我们转发到多个对象中去.

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

报出异常错误;

3.总结
  • 在动态方法决议无法处理时,将会执行消息转发机制,首先执行的是快速转发,即调用forwardingTargetForSelector;
  • 当快速转发也无法对消息进行处理时,则执行慢速转发,调用methodSignatureForSelector和resolveInstanceMethod;
  • 当慢速转发也无法对消息进行处理时,则抛出异常,调用doesNotRecognizeSelector方法,打印错误信息;
附:消息转发流程图
消息转发流程.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,302评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,232评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,337评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,977评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,920评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,194评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,638评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,319评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,455评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,379评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,426评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,106评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,696评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,786评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,996评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,467评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,043评论 2 341

推荐阅读更多精彩内容