iOS 消息查找和消息转发

1. 消息慢速查找流程

1.1 forward_imp探索

@interface ZCPerson : NSObject
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, strong) NSString *name;
-(void)sayHello;
+(void)sayHappy;

@end

#import "ZCPerson.h"

@implementation ZCPerson
-(void)sayHello
{
    NSLog(@"---%s",__func__);
}

+(void)sayHappy
{
    NSLog(@"---%s",__func__);

}
@end

    Class pClass = ZCPerson.class;
    lgIMP_classToMetaclass(pClass);

    IMP imp1 = class_getMethodImplementation(pClass, @selector(sayHello));
    IMP imp2 = class_getMethodImplementation(metaClass, @selector(sayHello));

    IMP imp3 = class_getMethodImplementation(pClass, @selector(sayHappy)); 
    IMP imp4 = class_getMethodImplementation(metaClass, @selector(sayHappy));

    NSLog(@"%p-%p-%p-%p",imp1,imp2,imp3,imp4);

输出:

2020-12-25 10:59:03.621174+0800 Objc[3615:37638] 0x100001be0-0x1002c3640-0x1002c3640-0x100001bb0

源码:

  if (slowpath((curClass = curClass->superclass) == nil)) {
            // No implementation found, and method resolver didn't help.
            // Use forwarding.
            imp = forward_imp;
            break;
        }

当对象在调用方法时,会先去cls里的cache查找是否有缓存,如果查找不到会进入bit内查找methodlist,当在当前的类里查不到,会到父类中的cache以及methodlist中继续查找。在研究isa的过程中,有一张isa走位图,图上正好也有一条继承链,由图可知,当方法查找最终,会查找到nil。而在源码中,当cls等于nil时,imp会被赋值为forward_imp。因此,也可知,当定义的方法没有实现时,imp的地址也不会为0x0,而是forward_imp的地址。

1.2 慢速查找方法lookUpImpOrForward

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;

    runtimeLock.assertUnlocked();

    // Optimistic cache lookup
    if (fastpath(behavior & LOOKUP_CACHE)) {
        imp = cache_getImp(cls, sel);
        if (imp) goto done_nolock;
    }

    runtimeLock.lock();

    checkIsKnownClass(cls);

    if (slowpath(!cls->isRealized())) {
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
        // runtimeLock may have been dropped but is now locked again
    }

    if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
        cls = initializeAndLeaveLocked(cls, inst, runtimeLock);//查询是否实现了+(void)initialize方法

    }

    runtimeLock.assertLocked();
    curClass = cls;

    for (unsigned attempts = unreasonableClassCount();;) {
        // curClass method list.
        Method meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            imp = meth->imp;
            goto done;
        }

        if (slowpath((curClass = curClass->superclass) == nil)) {
            // No implementation found, and method resolver didn't help.
            // Use forwarding.
            imp = forward_imp;
            break;
        }

        // Halt if there is a cycle in the superclass chain.
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }

        // Superclass cache.
        imp = cache_getImp(curClass, sel); //  汇编方法,cache_getImp - lookup - lookUpImpOrForward
        if (slowpath(imp == forward_imp)) {
            // Found a forward:: entry in a superclass.
            break;
        }
        if (fastpath(imp)) {
            // Found the method in a superclass. Cache it in this class.
            goto done;
        }
    }

    // No implementation found. Try method resolver once.

    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

 done:
    log_and_fill_cache(cls, imp, sel, inst, curClass);
    runtimeLock.unlock();
 done_nolock:
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}

由源码可知,在lookUpImpOrForward方法中,还是先在缓存中查找了是否有imp,因为在方法调用中,可能会受多线程的影响,可能在某个时候进行了方法缓存。然后经过checkIsKnownClass(cls);方法判断当前cls是否合法,然后会进入for循环方法,查找当前类以及元类的一条继承链,看看是否有实现的方法。 当沿着继承链查找到父类为nil时,则会退出循环,进行下一步方法决议流程。
查找方法源码:

static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();

    ASSERT(cls->isRealized());
    // fixme nil cls? 
    // fixme nil sel?

    auto const methods = cls->data()->methods();
    for (auto mlists = methods.beginLists(),
              end = methods.endLists();
         mlists != end;
         ++mlists)
    {
        // <rdar://problem/46904873> getMethodNoSuper_nolock is the hottest
        // caller of search_method_list, inlining it turns
        // getMethodNoSuper_nolock into a frame-less function and eliminates
        // any store from this codepath.
        method_t *m = search_method_list_inline(*mlists, sel);
        if (m) return m;
    }

    return nil;
}

通过cls->data()->methods();拿到方法列表,因为方法列表里有很多个方法,为了节省资源,苹果这里使用了二分算法去查找方法列表。注:在二分查找方法的过程中,会有一层分类重名方法判断。因为类的方法会先加入到内存中,然后才会加载分类方法。当查找到方法后,再调用cache_fill方法将方法写入缓存中。

1.3 方法决议流程

if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

lookUpImpOrForward方法中有一段如上代码,其中slowpath(behavior & LOOKUP_RESOLVER)说明在此时有一个方法决议的控制条件,也就是说,if里的判断条件只会走一次。然后进入resolveMethod_locked方法:

static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
    runtimeLock.assertLocked();
    ASSERT(cls->isRealized());
    // 方法没有你怎么不知道
    // 报错
    // 给你一次机会
    runtimeLock.unlock();

    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        resolveInstanceMethod(inst, sel, cls);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        resolveClassMethod(inst, sel, cls);
        if (!lookUpImpOrNil(inst, sel, cls)) {
            resolveInstanceMethod(inst, sel, cls);
        }
    }

    // chances are that calling the resolver have populated the cache
    // so attempt using it
    return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}

resolveMethod_locked最后,又调用了lookUpImpOrForward方法,递归回去了,也就是说明,在第一次imp没有处理后,苹果不会立即报错,而是给了一次处理imp的机会,而处理的方法则是在resolveInstanceMethod或者resolveClassMethod中进行处理。我们注意到,在进行resolveClassMethod处理中又加了一层resolveClassMethod的处理,因为在元类中也有一条继承链,而根元类的父类是根类NSObject,也就是说,NSObject中也可能存在未实现的方法,因此需要多加一层判断。

static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
    runtimeLock.assertUnlocked();
    ASSERT(cls->isRealized());
    SEL resolve_sel = @selector(resolveInstanceMethod:);

    if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
        // Resolver not implemented.
        return;
    }

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

 //下面是一些警告判断
  .
  .
  .
}

resolveClassMethod中我们注意到最后又进行了lookUpImp的处理,说明在这之前又对imp做了处理。通过源码,我们可以发现对当前的cls有一个objc_msgSend的处理,发送的sel@selector(resolveInstanceMethod:),也就是说,我们可以实现一个resolveInstanceMethod作为中间层,处理下一层未实现的方法。

2. 消息转发

当消息方法决议未实现后,则会来到消息转发流程。

2.1 快速转发流程 - forwardingTargetForSelector

lookUpImpOrForward方法中,我们看会看到gotodone会实现log_and_fill_cache这样一个方法,点击进去进入logMessageSend,我们会看到这个方法会打印出一些重要的信息。这里,向大家介绍一个方法instrumentObjcMessageSends(BOOL flag),因为在源码中,flag默认为0,所以logMessageSend是不打开日志的,所以我们需要使用instrumentObjcMessageSends方法让flag变为1,这样,就可以打开日志了。

#import <Foundation/Foundation.h>
#import "ZCPerson.h"

extern void instrumentObjcMessageSends(BOOL flag);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        ZCPerson *person = [ZCPerson alloc];
        instrumentObjcMessageSends(YES);
        [person say666];  //方法只定义了,并没有实现
        instrumentObjcMessageSends(NO);
    }
    return 0;
}

这里,我们借用extern实现方法instrumentObjcMessageSends,意思是,我们这个文件没有这个方法,让编译器去别的文件去找。当然,这是需要在源码环境中的。
我们打开Finder,然后前往文件夹/tmp/msgSends/,运行代码,发现当前文件夹多了一个msgSends-31644的文件,打开发现,里面不仅有resolveInstanceMethod,还有forwardingTargetForSelectormethodSignatureForSelector,说明方法决议后,并没有立即报错unrecognized selector,而是又进行了两步操作。
在文件中,我们发现forwardingTargetForSelector的实际调用者是ZCPerson,也就是说,我们还有一次拯救的机会,就是在ZCPerson中实现forwardingTargetForSelector

官方解释:forwardingTargetForSelector:

Returns the object to which unrecognized messages should first be directed.(当消息没有被识别时返回它的第一接受者。)

也就是说,当这个方法未被实现时,我们可以自己创建一个类实现方法作为接受者,在forwardingTargetForSelector中用创建的类代替,在创建的累中实现方法。也可以使用runtime对当前的sel动态添加一个imp。这也就是本篇文章介绍的快速转发流程。

2.2 慢速转发流程

我们在msgSends文件中不仅发现会有forwardingTargetForSelector方法,还有一个方法methodSignatureForSelector,官方文档如下:

Returns an NSMethodSignature object that contains a description of the method identified by a given selector.
Discussion:
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.

在消息发送过程中,对那些没有进行慢速转发的消息还会进行一次处理,并且会返回一个方法签名NSMethodSignature,在Discussion解释中,还会搭配着一个方法的使用,也就是forwordInvocation
于是,我们可以在ZCPerson中实现方法:


- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

-(void)forwardInvocation:(NSInvocation *)anInvocation
{
    
}

写完后再次运行会发现,代码没有崩溃了,我们进入NSInvocation,发现其定义如下:

@interface NSInvocation : NSObject

+ (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;

@property (readonly, retain) NSMethodSignature *methodSignature;

- (void)retainArguments;
@property (readonly) BOOL argumentsRetained;

@property (nullable, assign) id target;
@property SEL selector;
.
.
.

于是,我们将targetselector打印出来:

(lldb) po anInvocation.target
<ZCPerson: 0x10070a350>

(lldb) po anInvocation.selector
"say666"   

由此可知,这个时候系统介入了,将NSInvocation这个事物流放了,类似漂流瓶一样。因此,我们在forwardInvocation方法中既可以修改target,也可以修改selector

-(void)forwardInvocation:(NSInvocation *)anInvocation
{
    anInvocation.target = [[ZCTeacher alloc]init];
    
    [anInvocation invoke];
}

你也可以不做任何处理,但是anInvocation就会浪费了。

3.0 消息转发流程图

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

推荐阅读更多精彩内容