iOS 底层原理 - 消息转发

在上一篇 iOS 底层原理 - 消息查找流程中,我们知道OC消息机制分为三个阶段,消息发送,动态解析和消息转发,如果消息发送阶段没有找到方法,则会进入动态解析阶段,负责动态的添加方法实现。我们先分析下动态解析阶段。

动态方法决议阶段

我们先来到_class_resolveMethod

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);
        }
    }
}

首先会判断cls是不是元类,如果cls不是元类的话,说明调用的是实例方法,那就会调用_class_resolveInstanceMethod函数,如果是元类的话,说明调用的是类方法,那么就会调用_class_resolveClassMethod函数,并且调用完后会再次查找一下sel的指针,找到了就会返回,如果还是找不到的话会调用_class_resolveInstanceMethod函数,这里可以用上面的isa走位图解释。然后进入_class_resolveInstanceMethod方法

static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
//这里是一种容错处理,判断有没有resolveInstanceMethod这个方法,没有就return,有就进行下一步,如果一个类没有继承NSObject,是自己写的resolveInstanceMethod这个方法,这行就不会通过
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }
// 如果找到,则通过objc_msgSend调用一下+(BOOL)resolveInstanceMethod:(SEL)sel方法
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
// 再次寻找方法的IMP
    // 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));
        }
    }
}

通过上面的源码分析,我们知道,如果要进行动态解析的话,需要在方法resolveInstanceMethod里处理,给一个没有实现的方法一个已经实现的方法的IMP,就可以实现动态解析。
我们先调用一个对象方法,在这里我们调用一个不存在的saySomething方法,给它sayHello方法的imp

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    
    if (sel == @selector(saySomething)) {
        NSLog(@"说话了");
        
        IMP sayHIMP = class_getMethodImplementation(self, @selector(sayHello));
    
        Method sayHMethod = class_getInstanceMethod(self, @selector(sayHello));
        
        const char *sayHType = method_getTypeEncoding(sayHMethod);
        
        return class_addMethod(self, sel, sayHIMP, sayHType);
    }
    
    NSLog(@"来了  老弟 - %p",sel);
    return [super resolveInstanceMethod:sel];
}

打印结果

2020-03-08 15:24:21.590396+0800 LGTest[7487:301370] 说话了
2020-03-08 15:24:21.590947+0800 LGTest[7487:301370] -[LGStudent sayHello]
Program ended with exit code: 0

然后看下类方法,需要实现_class_resolveClassMethod,这里要注意类方法在元类里面,需要把上面的self改为objc_getMetaClass("LGStudent"),这里划重点。。。

+ (BOOL)resolveClassMethod:(SEL)sel{
    
    NSLog(@"来了类方法:%s - %@",__func__,NSStringFromSelector(sel));

     if (sel == @selector(sayLove)) {
         NSLog(@"说- 说你你爱我");
         IMP sayHIMP = class_getMethodImplementation(objc_getMetaClass("LGStudent"), @selector(sayObjc));
         Method sayHMethod = class_getClassMethod(objc_getMetaClass("LGStudent"), @selector(sayObjc));
         const char *sayHType = method_getTypeEncoding(sayHMethod);
         // 类方法在元类 objc_getMetaClass("LGStudent")
         return class_addMethod(objc_getMetaClass("LGStudent"), sel, sayHIMP, sayHType);
     }
     return [super resolveClassMethod:sel];
}

打印结果为

2020-03-08 17:14:18.911042+0800 LGTest[8481:346767] 来了类方法:+[LGStudent resolveClassMethod:] - sayLove
2020-03-08 17:14:18.911631+0800 LGTest[8481:346767] 说- 说你你爱我
2020-03-08 17:14:18.911793+0800 LGTest[8481:346767] +[LGStudent sayObjc]
Program ended with exit code: 0

我们在验证一下类方法找不到就会走_class_resolveInstanceMethod这个方法。
这里我们把上面的方法实现注释掉,然后在NSObject中添加一个对象方法sayLove,运行下面代码

+ (BOOL)resolveClassMethod:(SEL)sel{
    
    NSLog(@"来了类方法:%s - %@",__func__,NSStringFromSelector(sel));

//     if (sel == @selector(sayLove)) {
//         NSLog(@"说- 说你你爱我");
//         IMP sayHIMP = class_getMethodImplementation(objc_getMetaClass("LGStudent"), @selector(sayObjc));
//         Method sayHMethod = class_getClassMethod(objc_getMetaClass("LGStudent"), @selector(sayObjc));
//         const char *sayHType = method_getTypeEncoding(sayHMethod);
//         // 类方法在元类 objc_getMetaClass("LGStudent")
//         return class_addMethod(objc_getMetaClass("LGStudent"), sel, sayHIMP, sayHType);
//     }
     return [super resolveClassMethod:sel];
}

打印结果为

2020-03-08 17:13:15.772444+0800 LGTest[8446:345707] 对象方法-[NSObject(LG) sayLove]
Program ended with exit code: 0

消息转发

如果没有做动态解析,就会来到消息转发阶段。
我们先看一下找不到方法的崩溃信息,发现中间还经过了forwarding和_CF_forwarding_prep_0,那我们可以猜想系统在消息转发时可能做了其他的处理。

*** First throw call stack:
(
    0   CoreFoundation                      0x00007fff447412fd __exceptionPreprocess + 256
    1   libobjc.A.dylib                     0x000000010035306a objc_exception_throw + 42
    2   CoreFoundation                      0x00007fff447bb056 __CFExceptionProem + 0
    3   CoreFoundation                      0x00007fff446e318f ___forwarding___ + 1485
    4   CoreFoundation                      0x00007fff446e2b38 _CF_forwarding_prep_0 + 120
    5   LGTest                              0x0000000100000d29 main + 89
    6   libdyld.dylib                       0x00007fff706043d5 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

下面开始验证,我们在之前的方法消息查找流程中注意到填充缓存的时候我们会走log_and_fill_cache这个方法,是打印消息的这种形式。这里我们看到只有objcMsgLogEnabled为true的时候才会打印,那么继续找它什么时候为ture,

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

通过instrumentObjcMessageSends这个方法来对objcMsgLogEnabled进行赋值,也就是flag为true才会打印日志

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

那么我们通过在代码中暴露instrumentObjcMessageSends方法,并定位在要崩溃的方法中,可以打上日志,来查看调用

 instrumentObjcMessageSends(true);
 [student saySomething];
instrumentObjcMessageSends(false);

之后前往/tmp/msgSends 然后生成了文件 msgSends-,查看打印日志


屏幕快照 2020-03-14 下午3.32.09.png

发现经过了resolveInstanceMethod,forwardingTargetForSelector,methodSignatureForSelector,doesNotRecognizeSelector,这就是我们要寻找的处理方法。forwardingTargetForSelector,methodSignatureForSelector也就是我们下面要说的快速转发流程和慢速转发流程。

快速转发流程 forwardingTargetForSelector

我们先找一下官方文档,我们可以得到它的返回参数是一个对象,如果这个对象非nil,非self的话,系统会将运行的消息转发给这个对象执行。否则,继续查找其他流程。意思就是一个无法识别的消息可以让其他的对象来处理这个消息,系统给了个将这个sel转给其他对象的机会。


16f64591d863f64f.png

那么我们可以做以下处理,在LGTeacher中实现方法saySomething,然后交给LGTeacher去处理。

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

打印结果如下:

2020-03-14 15:37:30.290247+0800 008-方法查找-消息转发[999:27953] -[LGStudent forwardingTargetForSelector:] -- saySomething
2020-03-14 15:37:30.291713+0800 008-方法查找-消息转发[999:27953] -[LGTeacher saySomething]
Program ended with exit code: 0

慢速转发流程

如果快速转发阶段没有实现,就会进入到慢速转发阶段,也就是methodSignatureForSelector。我们先找一下methodSignatureForSelector方法的文档。


16f6471630fe64f2.png

这个方法会返回SEL方法的签名,返回的签名是根据方法的参数来封装的,这个函数让重载方有机会抛出一个函数的签名,再由后面的forwardInvocation去执行。也就是forwardInvocation和methodSignatureForSelector必须是同时存在的。forwardInvocation这个函数可以将NSInvocation多次转发到多个对象中,这也是这个方式灵活的地方。forwardingTargetForSelector只能通过Selector 的形式转向一个对象。

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

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

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

打印结果如下

2020-03-14 15:52:28.392549+0800 008-方法查找-消息转发[1117:34015] -[LGStudent methodSignatureForSelector:] -- saySomething
2020-03-14 15:52:28.394561+0800 008-方法查找-消息转发[1117:34015] -[LGStudent forwardInvocation:]
2020-03-14 15:52:28.395346+0800 008-方法查找-消息转发[1117:34015] -[LGTeacher saySomething]
Program ended with exit code: 0

总结

1.objc_msgSend 从缓存中查找imp
2.慢速递归查找方法列表
3.没有找到imp,那么看你有没有进行特殊处理,也就是消息动态解析,如果没有进行特殊处理,就来到消息转发阶段。
4.快速消息转发 forwardingTargetForSelector,有没有交给别人处理,如果没有,就进入慢速消息转发
5.慢速消息转发,意味着你不想处理,谁想要处理就去处理。methodSignatureForSelector 实现方法签名,forwardInvocation 来对消息处理
6.doesNotRecognizeSelector 系统不会分发这个事务,报错。

最后附上流程图


屏幕快照 2020-03-14 下午6.06.09.png

补充

探究的过程中,我们发现在动态方法决议阶段,如果我们没有处理动态方法决议,会进来两次。

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    
    NSLog(@"来了老弟:%s - %@",__func__,NSStringFromSelector(sel));
    
    return [super resolveInstanceMethod:sel];
}

打印结果为

2020-03-14 18:11:50.658224+0800 LGTest[2037:82739] 来了老弟:+[LGStudent resolveInstanceMethod:] - saySomething
2020-03-14 18:11:50.659050+0800 LGTest[2037:82739] 来了老弟:+[LGStudent resolveInstanceMethod:] - saySomething
2020-03-14 18:11:50.659287+0800 LGTest[2037:82739] -[LGStudent saySomething]: unrecognized selector sent to instance 0x100f50650

这里为什么回来两次呢,我们观察一下控制台打印:

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    NSLog(@"%s",__func__);
    return [super resolveInstanceMethod:sel];
}


- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
    
    return [super forwardingTargetForSelector:aSelector];
}

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

       
}

结果如下:

2020-03-14 18:20:45.339901+0800 008-方法查找-消息转发[2117:86023] +[LGStudent resolveInstanceMethod:]
2020-03-14 18:20:45.340428+0800 008-方法查找-消息转发[2117:86023] -[LGStudent forwardingTargetForSelector:] -- saySomething
2020-03-14 18:20:45.340584+0800 008-方法查找-消息转发[2117:86023] -[LGStudent methodSignatureForSelector:] -- saySomething
2020-03-14 18:20:45.340665+0800 008-方法查找-消息转发[2117:86023] +[LGStudent resolveInstanceMethod:]
2020-03-14 18:20:45.340736+0800 008-方法查找-消息转发[2117:86023] -[LGStudent forwardInvocation:]
Program ended with exit code: 0

从打印结果看出在methodSignatureForSelector和forwardInvocation之间还做了其他的操作,methodSignatureForSelector会返回一个方法签名,之后会有一步去匹配签名的过程,会调用class_getInstanceMethod方法。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268