OC消息转发(三)— 动态方法解析和消息转发机制

前言

前边两篇文章(objc_msgSend探索消息的查找流程探索)我们对调用方法到消息的查找流程做了详细探索,如果说我们没有找到方法(消息)系统是怎么处理的,我们又该做些什么去防止崩溃呢。这篇文章我们就对动态方法解析和消息的转发机制进行详细探索研究。

一、动态方法解析

消息的查找流程探索文章我们探索到了方法lookUpImpOrForward寻找方法imp,当遍历完superclass没有找到的时候,会进入_class_resolveMethod动态方法解析过程,如下:

if (resolver  &&  !triedResolver) {
        runtimeLock.unlock();
        //动态方法解析入口
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.lock();
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;
        goto retry;
    }

_class_resolveMethod方法如下:

void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        //对象方法存在于类的方法列表里,类属于非元类class
        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        //类方法存在于元类的方法列表里,元类的class
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}
  • 对象方法
    可以看出如果是对象方法会调用_class_resolveInstanceMethod方法,cls是类,详情如下:
static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    //寻找一下+resolveInstanceMethod的imp,如果找不到直接return
    //找不到的话下方调用+resolveInstanceMethod会崩溃
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }
    //给objc_msgSend多添加一个参数,通过汇编快速寻找方法imp
    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));
        }
    }
}

1、SEL_resolveInstanceMethod就是NSObject+resolveInstanceMethod,首先会判断一下+resolveInstanceMethod方法是否实现,如果没实现直接return,要不然下边调用会崩溃。
2、然后通过objc_msgSend汇编快速查找对象方法sel,动态方法解析完成后,会goto retry;重新查找一遍imp,动态解析标识triedResolver变为YEScls是类。
3、如果还是没有查找到会进入_objc_msgForward_impcache消息转发,转发不成功就会崩溃。

  • 类方法
    如果是类方法的时候调用_class_resolveClassMethod方法,cls是元类,详情如下:
static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
    assert(cls->isMetaClass());
    //寻找一下+resolveClassMethod的imp,如果找不到直接return
    //找不到的话下方调用+resolveClassMethod会崩溃
    if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }
    //给objc_msgSend多添加一个参数,通过汇编快速寻找方法imp
    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));
        }
    }
}

1、SEL_resolveClassMethod就是NSObject+ resolveClassMethod,首先会判断一下+ resolveClassMethod方法是否实现,如果没实现直接return,要不然下边调用会崩溃。
2、然后通过objc_msgSend汇编快速查找类方法sel,获取到cls元类的nonMetaClass非元类,动态方法解析完成后,会goto retry;重新查找一遍imp,动态解析标识triedResolver变为YEScls是元类。
3、如果还是没有查找到会进入_objc_msgForward_impcache消息转发,转发不成功就会崩溃。

为什么要获取cls元类的nonMetaClass非元类呢?
是因为开发者是不能在元类中重写和自定义方法+ resolveClassMethod,所以objc_msgSend要向类发送消息。

  • 防崩溃处理
    +resolveInstanceMethod+resolveClassMethod的实现如下:
+ (BOOL)resolveClassMethod:(SEL)sel {
    return NO;
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    return NO;
}

这两个方法是NSObject的类方法,正常不处理的话return NO,开发者不实现方法就会找不到,然后崩溃。那么我们可以重写该方法然后把未实现的方法动态添加到类上,return YES,那么是不是就能够防止崩溃了。接下来我们验证一下。
我们创建两个类LGPersonLGStudentLGStudent继承于LGPersonLGPerson继承于NSObjectLGPerson声明两个方法-(void)saySomething+(void)sayLove,但是不实现。LGStudent声明并实现-(void)sayHello+(void)sayObjc。如下图:

LGPerson.h

LGStudent.h
LGStudent.m

main.m中用LGStudent类分别调用-(void)saySomething或者+(void)sayLove,都会崩溃unrecognized selector sent to instance xxxxxx。接下来我们来做一些处理。

首先我们来验证一下对象方法resolveInstanceMethod,在LGStudent.m重写resolveInstanceMethod方法,判断saySomething方法,如下:

+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    if (sel == @selector(saySomething))
    {
        IMP helloImp = class_getMethodImplementation(self, @selector(sayHello));
        Method helloMethod = class_getClassMethod(self, @selector(sayHello));
        const char *helloTypes = method_getTypeEncoding(helloMethod);
        return class_addMethod(self, sel, helloImp, helloTypes);
    }
    return [super resolveInstanceMethod:sel];
}

判断是saySomething方法的时候,动态添加对象方法saySomething,把saySomethingsel的实现指向了sayHelloimp,当调用的时候会执行sayHelloimp,打印-[LGStudent sayHello]

然后我们来以相同的方式验证一下resolveClassMethod,在LGStudent.m重写resolveClassMethod方法,判断sayLove方法,如下:

+ (BOOL)resolveClassMethod:(SEL)sel
{
    if (sel == @selector(sayLove))
    {
        IMP objcImp = class_getMethodImplementation(objc_getMetaClass("LGStudent"), @selector(sayObjc));
        Method objcMethod = class_getClassMethod(objc_getMetaClass("LGStudent"), @selector(sayObjc));
        const char *objcTypes = method_getTypeEncoding(objcMethod);
        return class_addMethod(objc_getMetaClass("LGStudent"), sel, objcImp, objcTypes);
    }
    return [super resolveClassMethod:sel];
}

这里要注意一点类方法是存储在元类里的,所以动态添加方法要添加到元类里,方法的imptypes都要从元类里获取。动态添加类方法sayLove,把sayLove的实现指向sayObjcimp,当调用的时候会执行sayObjcimp,打印+[LGStudent sayObjc]

  • 问题:当类方法调用_class_resolveMethod的时候,cls是元类,那为什么先调用了_class_resolveClassMethod,然后!lookUpImpOrNil判断之后又调用了_class_resolveInstanceMethod呢?
    首先看一下lookUpImpOrNil的实现:
IMP lookUpImpOrNil(Class cls, SEL sel, id inst, 
                   bool initialize, bool cache, bool resolver)
{
    IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
    if (imp == _objc_msgForward_impcache) return nil;
    else return imp;
}

lookUpImpOrForward是寻找方法的imp,上篇文章中有详细介绍。

解答:
_class_resolveClassMethod会通过objc_msgSendcls的非元类发送一个消息,如果+ resolveClassMethodsel做了处理,那么就能查找出imp

如果未查找到imp,调用_class_resolveInstanceMethod,会通过objc_msgSendcls元类发送一个消息,如果+ resolveInstanceMethodsel做了处理,那么就能查找出imp。否则崩溃。

注:这其中主要还是isa的走位,根元类的父类还是NSObject类,元类的superclass链会查找到NSObject类中。如果NSObject类中重写并实现了sel

二、消息转发机制

当动态方法解析失败的时候就进入到了消息转发过程。OC的消息转发机制就是当一个对象的方法调用(消息发送)它本身无法处理的时候,为了防止崩溃报错可以把消息转发个另一个对象处理,这就叫做消息转发机制。消息的查找流程探索中我们提到了当未找到selimp的时候会进入_objc_msgForward_impcache进入汇编消息转发。那么我们应该怎么跟踪并尝试消息转发过程呢?

消息查找流程的时候我们提到了一个方法log_and_fill_cache,是用来缓存方法的,他还有一个日志缓存的方法logMessageSend,详情如下:

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

可以看出缓存的文件位置在/tmp/msgSendsobjcMsgLogEnabled是个布尔值,控制objcMsgLogEnabled值的方法是void instrumentObjcMessageSends(BOOL flag),这样我们可以尝试着把消息转发过程中的方法缓存一下,看看都调用了哪些方法。

首先我们在main.m文件中添加方法extern void instrumentObjcMessageSends(BOOL);,然后在调用未实现方法前后调用此方法,如下:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGStudent *student = [[LGStudent alloc] init];
        instrumentObjcMessageSends(YES);
        [student saySomething];
        instrumentObjcMessageSends(NO);
    }
    return 0;
}

然后运行项目command+shift+G寻找到到日志文件,下边是复制了部分日志方法,如下:

+ LGStudent NSObject resolveInstanceMethod:
+ LGStudent NSObject resolveInstanceMethod:
- LGStudent NSObject forwardingTargetForSelector:
- LGStudent NSObject forwardingTargetForSelector:
- LGStudent NSObject methodSignatureForSelector:
- LGStudent NSObject methodSignatureForSelector:
- LGStudent NSObject class
+ LGStudent NSObject resolveInstanceMethod:
+ LGStudent NSObject resolveInstanceMethod:
- LGStudent NSObject doesNotRecognizeSelector:
- LGStudent NSObject doesNotRecognizeSelector:
- LGStudent NSObject class
//下边的省略

全局找不到这些方法,那我们就看一下文档这些方法是干什么的。翻译如下:

  • forwardingTargetForSelector:

1、如果对象实现(或继承)这个方法,并且返回了非nil和非self结果,那么这个被返回的对象将作为新的接收消息的对象并且消息会发送到这个新对象。(如果在这个方法里返回了self,那么代码就会进入到无限循环里。)。
2、如果在非根类里实现了这个方法,并且对于selector没有什么可以返回,需要返回super的实现。
3、在更复杂的forwardInvocation:执行之前,这个方法给了对象一个重新向它发送未知消息的机会。当你只是简单地把消息重新定向到另外一个对象的时候这个方法很有用,比常规的转发快了一个量级。但是当你转发的目标是捕获NSInvocation,或者是在转发过程中有参数或返回值操作,这个方法就没什么用了。

  • methodSignatureForSelector :

在必要的时候必须要避免无限循环,方法是检查aSelector是不是该方法的selector和不发送任何可能调起该方法的消息。

  • doesNotRecognizeSelector :

1、每当对象接收到它无法响应或转发的aSelector消息时,运行时系统就会调用此方法。此方法反过来引发NSInvalidArgumentException,并生成错误消息。
2、任何DoesNotRecogniteSelector:消息通常仅由运行时系统发送。但是,可以在程序代码中使用它们来防止方法被继承。例如,NSObject子类可以通过重新实现copy或init方法来不使用父类copy或init方法,重写的方法里要包含DoesNotRecogniteSelector:,如下:

- (id)copy
{
  [self doesNotRecognizeSelector:_cmd];
}

3、如果重写此方法,则必须在实现结束时调用super或引发NSInvalidArgumentException异常。换句话说,此方法不能正常返回;它必须始终导致引发异常。

  • forwardInvocation:
    该方法并没有记录在日志文件中,是因为流程没有走到它这一步就报错了。

1、当一个对象发送一个未实现的方法的时候,运行时系统会给他一个把消息委托给另一个对象的机会。运行时系统会通过创建一个代表消息的NSInvocation对象并且会向接收者发送一个包含NSInvocation作为参数的forwardInvocation:消息。(如果该对象也无法响应消息,它也有机会转发该消息。)
2、forwardInvocation:允许消息发送对象与其他对象建立关系,对于某些消息,这些对象将代表消息发送对象执行消息。在某种意义上,这些对象能够“继承”消息发送对象的某些特性。
3、methodSignatureForSelector :forwardInvocation:必须都要实现才能起作用。第一个方法生成方法签名并返回,当签名正确并且不为nil,第二个方法才会生成一个NSInvocation

看一下消息转发的流程图,如下图:


消息转发流程

可以看出动态方法解析失败的时候,还会有两次消息转发的步骤,快速流程和慢速流程。

  • 快速流程
    当动态方法解析失败的时候,可以通过forwardingTargetForSelector:快速指定新的消息接收对象,这就是快速流程。

  • 慢速流程
    当快速流程forwardingTargetForSelector:返回值为nil的时候,会调用methodSignatureForSelector :返回方法签名,然后调用forwardInvocation:生成NSInvocation,可以在此方法中做一些对未实现消息的处理。

  • 快速流程实现
    LGStudent调用未实现方法saySomething,创建LGTeacher类实现此方法,如下:
@interface LGTeacher : NSObject

@end

@implementation LGTeacher

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

@end

实现forwardingTargetForSelector:如下:

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

运行起来控制台打印结果如下:

2020-02-14 15:26:40.711832+0800 008-方法查找-消息转发[3038:77509] -[LGStudent forwardingTargetForSelector:] -- saySomething
2020-02-14 15:26:40.713167+0800 008-方法查找-消息转发[3038:77509] -[LGTeacher saySomething]
Program ended with exit code: 0

可以看出,- (id)forwardingTargetForSelector:(SEL)aSelector方法把saySomething这个消息转发给了LGTeacher对象,调用了LGTeacher类中的saySomething实现。

  • 慢速流程实现
    LGStudent调用未实现方法saySomething,创建LGTeacher类实现此方法,实现methodSignatureForSelector :返回一个非空的方法签名,实现forwardInvocation :做一些处理(也可以不做),如下:
- (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-02-14 15:41:08.681776+0800 008-方法查找-消息转发[3181:84329] -[LGStudent methodSignatureForSelector:] -- saySomething
2020-02-14 15:41:08.682382+0800 008-方法查找-消息转发[3181:84329] -[LGStudent forwardInvocation:]
2020-02-14 15:41:08.682786+0800 008-方法查找-消息转发[3181:84329] -[LGTeacher saySomething]
Program ended with exit code: 0

总结

运行时对于未实现的方法的处理有三种:
1、动态方法解析
通过重写+resolveInstanceMethod(对象方法)或者+resolveClassMethod(类方法)动态的添加方法imp,再次通过lookUpImpOrForwardgoto retry;查找一遍,找到imp。找不到进入消息转发流程。

2、消息转发机制

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

推荐阅读更多精彩内容