iOS底层系列14 -- 消息流程的动态方法决议与转发

iOS底层系列12 -- 消息流程的快速查找iOS底层系列13 -- 消息流程的慢速查找这两篇文章中分别介绍了objc_msgSend快速查找与方法列表的慢查找,如果都没有找到方法实现就会进入动态方法决议和消息转发。

  • 动态方法决议:慢速查找流程未找到方法实现时,会执行一次动态方法决议;
  • 消息转发:如果动态方法决议仍然没有找到方法实现时,则进行消息转发;
  • 如果动态方法决议消息转发都没有做任何操作,就会出现崩溃报错,即unrecognized selector sent to instance xxxx

动态方法决议

  • 在慢速查找中没有找到方法实现,会尝试进行一次动态方法决议,源码实现如下:
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);
        }
    }

    return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}
  • 判断当前类如果不是元类,执行实例方法的动态方法决议resolveInstanceMethod
  • 当前类是元类,执行类方法的动态方法决议resolveClassMethod,如果在元类中没有找到或者为空,则在元类的实例方法的动态方法决议resolveInstanceMethod中查找,主要是因为类方法在元类中是实例方法,所以还需要查找元类中实例方法的动态方法决议
  • 实例方法的动态方法决议resolveInstanceMethod源代码实现如下:
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);

    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消息前,首先查找cls类中是否有该方法的实现,即通过lookUpImpOrNil方法又会进入lookUpImpOrForward慢速查找流程,但这次查找的是resolveInstanceMethod方法
    • 如果没有实现,则直接返回;
    • 如果有实现,则执行resolveInstanceMethod方法;
  • 实例方法的动态方法决议代码测试:
@interface YYPerson : NSObject

- (void)walk;

+ (void)speak;

@end
#import "YYPerson.h"
#import <objc/runtime.h>

@implementation YYPerson

- (void)walk_resolve{
    NSLog(@"walk_resolve");
}

//给当前类动态添加一个方法和方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if ([NSStringFromSelector(sel) isEqualToString:@"walk"]) {
        NSLog(@"walk -- ");
        IMP imp = class_getMethodImplementation(self, @selector(walk_resolve));
        Method method = class_getInstanceMethod(self,@selector(walk_resolve));
        const char *type = method_getTypeEncoding(method);
        return class_addMethod(self,sel,imp,type);
    }
    return [super resolveInstanceMethod:sel];
}
@end
  • 在resolveInstanceMethod方法内部打下断点,当应用停在断点,控制台输入bt命令,打印出函数调用堆栈如下所示:
Snip20210301_112.png
  • 类方法的动态方法决议代码测试:
#import "YYPerson.h"
#import <objc/runtime.h>

@implementation YYPerson

+ (void)speak_resolve{
    NSLog(@"speak_resolve");
}

+ (BOOL)resolveClassMethod:(SEL)sel{
    if ([NSStringFromSelector(sel) isEqualToString:@"speak"]) {
        IMP imp = class_getMethodImplementation(objc_getMetaClass("YYPerson"), @selector(speak_resolve));
        Method method  = class_getInstanceMethod(objc_getMetaClass("YYPerson"), @selector(speak_resolve));
        const char *type = method_getTypeEncoding(method);
        return class_addMethod(objc_getMetaClass("YYPerson"),sel,imp,type);
    }
    return [super resolveClassMethod:sel];
}

@end
  • 断点同上设置,函数调用堆栈如下:
Snip20210301_113.png
  • 若动态方法决议没有手动去实现,就会进入消息转发的流程;

消息转发

  • 消息转发的处理主要分为两个部分:
    • 快速转发:当慢速查找,以及动态方法决议均没有找到实现时,进行消息转发,首先是进行快速消息转发,即执行forwardingTargetForSelector方法;
      • 如果返回消息接收者,在消息接收者中还是没有找到,则进入另一个方法的查找流程;
      • 如果返回nil,则进入慢速消息转发;
    • 慢速转发:执行到methodSignatureForSelector方法;
      • 如果返回的方法签名为nil,则直接崩溃报错;
      • 如果返回的方法签名不为nil,走到forwardInvocation方法中,对invocation事务进行处理,如果不处理也会造成崩溃报错;
  • 消息快速转发的代码测试如下:
#import <Foundation/Foundation.h>

@interface YYStudent : NSObject

- (void)walk;

@end
#import "YYStudent.h"

@implementation YYStudent

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

@end
#import "YYPerson.h"
#import <objc/runtime.h>
#import "YYStudent.h"

@implementation YYPerson

- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"%s",__func__);
    if (aSelector == @selector(walk)) {
        return [YYStudent new];
    }
    return [super forwardingTargetForSelector:aSelector];
}

@end
  • YYPerson类没有实现walk实例方法,实现forwardingTargetForSelector函数可将消息转发给YYStudent实例对象(实现了walk方法);

  • forwardingTargetForSelector函数内部断点调试如下:

Snip20210302_114.png
  • 可以看出消息的快速转发调用了CoreFoundation框架;
  • 若当消息的快速转发没有进行处理,就会进入消息的慢速转发流程,测试代码如下:
#import "YYPerson.h"
#import <objc/runtime.h>
#import "YYStudent.h"

@implementation YYPerson

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

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"%s - %@",__func__,anInvocation);
    SEL sel = anInvocation.selector;
    YYStudent *student = [[YYStudent alloc]init];
    if ([student respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:student];
    }else{
        [anInvocation doesNotRecognizeSelector:sel];
    }
}

@end
  • methodSignatureForSelector为需要慢速转发的消息提供方法签名;
  • forwardInvocation系统会为需要转发的消息创建一个NSInvocation事务对象,我们可以对NSInvocation事务进行处理,如果不处理也不会崩溃报错;

总结

综合 iOS底层系列12 -- 消息流程的快速查找iOS底层系列13 -- 消息流程的慢速查找以及本篇,objc_msgSend发送消息的整体流程就分析完成了,现作出如下总结:

  • 快速查找流程:首先在类的缓存cache中查找指定方法的实现;
  • 慢速查找流程:如果缓存中没有找到,则在类的方法列表中查找(二分法),如果还是没找到,则根据类/元类的继承链在父类的缓存和方法列表中查找,一直递归到nil;
  • 动态方法决议:如果慢速查找还是没有找到时,第一次补救机会就是尝试一次动态方法决议,即实现resolveInstanceMethod/resolveClassMethod 方法;
  • 消息转发:如果动态方法决议没有处理,则进行消息转发,消息转发中有两次补救机会:快速转发+慢速转发
  • 如果消息转发也没有处理,则程序直接报错崩溃unrecognized selector sent to instance

super的本质

  • 定义类YYPerson,实现一个run方法;
  • 定义一个子类YYStudent,继承自YYPerson;
  • 测试代码如下:
#import "YYStudent.h"

@implementation YYStudent

- (instancetype)init{
    self = [super init];
    if (self) {
        NSLog(@"[self class] = %@",[self class]);
        NSLog(@"[self superclass] = %@",[self superclass]);
        
        NSLog(@"[super class] = %@",[super class]);
        NSLog(@"[super superclass] = %@",[super superclass]);
    }
    return self;
}

- (void)run{
    [super run];
    NSLog(@"%s",__func__);
}
@end
  • 终端输入xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc YYStudent.m,看到YYStudent类的run方法的C++底层实现如下:
static void _I_YYStudent_run(YYStudent * self, SEL _cmd) {
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("YYStudent"))}, sel_registerName("run"));
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_c5_l8bnxw0d2w92f4439t_r8qjc0000gn_T_YYStudent_1b9e06_mi_4,__func__);
}
  • 看到[super run],转成了objc_msgSendSuper(struct objc_super,selector)
  • 其中第一个参数是一个objc_super类型的结构体,内部有两个参数分别为:selfself的父类,也就是YYStudent的实例对象与YYPerson类;
  • [super message],底层转成objc_msgsendSuper({self,父类对象},@selector(message)),消息的接受者依然是当前实例对象,只不过消息的查找越过了当前类,直接去其父类YYPerson中去查找;
#import <Foundation/Foundation.h>
#import "YYStudent.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        YYStudent *student = [[YYStudent alloc]init];
    }
    return 0;
}
  • 控制台打印结果:
Snip20210630_37.png
  • 看到 [self class]与[super class]打印结果相同;
  • 首先[self class] --> objc_msgSend(self,@selector(class))
  • 然后[super class] --> objc_msgSendSuper({self,YYPerson},@selector(class)
  • class方法实现是在NSObject基类里面的,其实现如下:
- (Class)class{
     object_getClass(self);
}
  • 也就是说class方法返回的结果,取决于消息的接受者self;
  • 所以 [self class]与[super class] 消息的接受者都是self,即YYStudent类的实例对象,所以最终的调用结果是相同的,都返回YYStudent类;
  • superClass的方法实现是在NSObject基类里面,其实现如下:
- (Class) superClass{
     object_get SuperClass(object_getClass(self));
}
  • 所以[self superClass]与[super superClass] 返回的都是YYPerson类;

objc_msgsend(instance,@selector)底层实现

  • 底层实现逻辑如下图所示:
objc_msgSend.png
  • 主要分为三个阶段:
    • 第一个阶段:在缓存Cache_t中查找,是用汇编语言实现的;
    • 第二个阶段:在类class结构体内部的class_rwe_t中的方法列表查找,是通过C语言实现的;
    • 第三个阶段:动态方法决议与消息的转发;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容