消息转发流程

消息转发机制

消息的查找流程分为:快速查找和慢速查找
消息转发机制也分为:快速和慢速
先来一个转发流程图


消息转发流程

之前我们的消息查找流程中有这端代码

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

如果resolver为true,triedResolver为false,那么就会走_class_resolveMethod

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

首先判断当前类是否为元类,如果是元类就意味着为类方法,否则就是实例方法.

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 (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

现在发送一个SEL_resolveInstanceMethod的消息,SEL_resolveInstanceMethod是个什么东西呢
来看下注释 * Call +resolveInstanceMethod, looking for a method to be added to class cls.他是一个+ resolveInstanceMethod方法,那么我本类中没有实现resolveInstanceMethod这个方法啊为啥他就不蹦呢,因为他的父类中实现了,我们搜一下发现NSObject中实现了

image.png

,那么我们再来看一下上边的判断条件

if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

这里是做了一个容错处理,因为有可能你的类没有继承NSObject,那就意味着找不到该方法,就没有必要再去发送消息,然后根据上边我们讲的消息查找流程,去父类中找最后找到NSObject中,所以这里不会出现崩溃
调用了一个只声明没有实现的方法直接崩溃,而且崩溃前会先调用一下resolveInstanceMethod.
那么怎么去挽救一下呢.
来重写resolveInstanceMethod.之所以崩溃是因为没有找到imp,那么我们可以给他加一个imp

//动态方法决议
+(BOOL)resolveInstanceMethod:(SEL)sel{
    NSLog(@"来了老弟");
//如果注释掉下边`{}`中代码,发现这里会来两次,打印两次来了老弟
//
    if (sel == @selector(saySomething)){
        IMP sayHIMP = class_getMethodImplementation(self, @selector(sayHello));
        Method method = class_getInstanceMethod(self, @selector(sayHello));
        const char *sayType = method_getTypeEncoding(method);
        return  class_addMethod(self, sel, sayHIMP, sayType);
    }
    return [super resolveInstanceMethod:sel];
}

完美解决崩溃问题
如果这里没有做特殊处理,返回的是一个NO,那么会来到一个快速转发流程,

快速转发流程
  • 首先来看一下打印
    我们忽略了一个函数 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);
}

如果objcMsgLogEnabled为true则打印,点击objcMsgLogEnabled看下他的实现

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

这里有一个文件路径.那么我们只需要将objcMsgLogEnabled设置为true就可以看到打印信息了.
我们发现下边这个函数instrumentObjcMessageSends

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

我们只需要调用这个函数传入一个trueok了,比如

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

然后打开/private/tmp文件目录

如图

+ 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
看一下文档

Discussion
If an object implements (or inherits) this method, and returns a non-nil (and non-self) result,
 that returned object is used as the new receiver object and the message dispatch resumes 
to that new object. (Obviously if you return self from this method, the code would just fall into an infinite loop.)

大致意思就是如果自己没有实现可以返回一个新的实现了改方法的对象
那么如果动态方法决议没有处理,那么来到这个方法

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

我们这里返回一个teacher没让他来实现这个方法,那么如果他也没有实现呢,就会来到下一个消息转发流程
慢速转发流程

慢速转发
//根据selector生成一个NSMethodSignature方法签名并返回
//方法签名其实就是,方法的参数返回值类型的一些信息
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
  • 使用
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
   NSLog(@"%s -- %@",__func__,NSStringFromSelector(aSelector));
   if (aSelector == @selector(saySomething)) { // v @ :
       return [NSMethodSignature signatureWithObjCTypes:"v@:"];
   }
   return [super methodSignatureForSelector:aSelector];
}

方法签名TypeEncoding

那么只有签名没有实现是不行的

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        LGStudent *student = [LGStudent alloc] ;

        // 动态方法决议
        // 对象方法
        // 类方法 -
        
        instrumentObjcMessageSends(true);
        [student saySomething];
        instrumentObjcMessageSends(false);

    }
    return 0;
}

@implementation LGStudent
// 慢速转发
- (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];
}
@end

如上,如果student调用一个没有实现的方法,那么回来到慢速转发流程,返回签名之后,我们把这个消息指派给实现了saySomething方法的teacher类,如果没有指派,那么直接崩溃.至此我们的消息转发流程走完

利用消息转发预防崩溃方案

上边讲了,runtime给出的三种补救方式

  1. 调用resolveInstanceMethod给个机会让类添加这个实现这个函数

  2. 调用forwardingTargetForSelector让别的对象去执行这个函数

  3. 调用forwardInvocation(函数执行器)灵活的将目标函数以其他形式执行。

如果都不中,调用doesNotRecognizeSelector抛出异常。选择哪一种合适呢?
1.resolveInstanceMethod需要在类上动态添加他本身不存在的方法,这些方法对于类本身来说是冗余的,不合适

  1. forwardInvocation 可以通过 NSInvocation 的形式将消息转发给多个对象,但是其开销大,需要创建 NSInvocation对象,并且forwardInvocation的函数经常被使用者调用,来做多层消息转发机制,不适合多次重写
  2. fowardIngTargetForSelector可以将消息转发给一个对象,开销小,并且被重写的概率较低,适合重写

这里要注意,如果原本类已经重写了forwardInvocation的话,就能对forwardingTargetForSelector进行重写了,这样会影响对象原本的消息转发

如何做?

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