iOS runtime,详细介绍消息转发流程

iOS runtime,详细介绍消息转发流程

方法查找原理

在之前的文章中,写过在进行方法调用的时候,runtime的消息转发流程

  1. 先去缓存中查找
  2. 如果缓存没有找到,通过 isa 指针找到当前的类的对象,然后去方法列表中查找
  3. 如果当前方法列表中还是没有,就通过 superClass 指针在自己的父类中已上面两个流程去查找,一直找到根NSObject类

为什么要有第一步?因为我们的方法太多了,每次都遍历一次 objc_method_list 列表去查找,这样就效率太低了,所以就把经常用的方法给缓存起来了, objc_method_list 是 objc_class 结构体的一员,下面是 runtime 源码 objc4-756.2 版本的 objc_class 结构体

源码结构体分析

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

其中 objc_cache 就是用来干这个事情的,看到这个源码的第一印象,我想到的就是 YYModel 的源码,他的源码中的几个类封装的就是类似于源码的这个结构体,再来看几个

struct objc_protocol_list {
    struct objc_protocol_list * _Nullable next;
    long count;
    __unsafe_unretained Protocol * _Nullable list[1];
};


struct objc_ivar {
    char * _Nullable ivar_name                               OBJC2_UNAVAILABLE;
    char * _Nullable ivar_type                               OBJC2_UNAVAILABLE;
    int ivar_offset                                          OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
}  

struct objc_ivar_list {
    int ivar_count                                           OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_ivar ivar_list[1]                            OBJC2_UNAVAILABLE;
}

struct objc_method {
    SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
    char * _Nullable method_types                            OBJC2_UNAVAILABLE;
    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
} 

struct objc_method_list {
    struct objc_method_list * _Nullable obsolete             OBJC2_UNAVAILABLE;

    int method_count                                         OBJC2_UNAVAILABLE;
#ifdef __LP64__
    int space                                                OBJC2_UNAVAILABLE;
#endif
    /* variable length structure */
    struct objc_method method_list[1]                        OBJC2_UNAVAILABLE;
}   

看到这些,我想很多人都眼前一亮,或多或少看到过或者用到过.

从 objc_class 可以看到,它里面保存了,父类指针,类名,版本号,信息,实力大小,变量列表,方法列表,方法缓存列表,协议列表.

我们再来看下源码中的 objc_object


/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

可以看到有个 isa 指针,类对象的 isa 指针指向元类,这里有一篇博客的图画的很好可以参考他的总结


* objc_object 的isa指针指向他的类对象
* 类对象的isa指针指向元类,superClass指针指向他的父类的类对象,也就是nsobject
* 元类的isa指针指向nsobject元类。
* 上面说了isa指针指向他的元类,所以nsobject的isa指针就指向nsobject元类
* 所以这样就形成了一个闭环

测试

- (void)testClass{
 
    TestClass *test = [[TestClass alloc] init];
    Class c1 = [test class];
    Class c2 = [TestClass class];
    NSLog(@"%d",c1 == c2);
    NSLog(@"%@",[c1 class]);
    // 获取isa指针指向的对象,这里其实获取的是实例对象c1的类对象,也就是TestClass
    NSLog(@"%@",test);
    NSLog(@"%@",[test class]);
    NSLog(@"%@",[TestClass class]);
    NSLog(@"%@",object_getClass(test));
    NSLog(@"%@",object_getClass([test class]));
    NSLog(@"%@",object_getClass([TestClass class]));
    NSLog(@"%d",class_isMetaClass([test class]));
    NSLog(@"%d",class_isMetaClass([TestClass class]));
    NSLog(@"%d",class_isMetaClass(object_getClass(test)));
    NSLog(@"%d",class_isMetaClass(c);
    NSLog(@"%d",class_isMetaClass(object_getClass([TestClass class])));

}

打印结果

2019-12-18 20:05:28.646099+0800 blogTest[43808:3772452] 1
2019-12-18 20:05:28.646263+0800 blogTest[43808:3772452] TestClass
2019-12-18 20:05:28.646422+0800 blogTest[43808:3772452] <TestClass: 0x6000015e0a90>
2019-12-18 20:05:28.646565+0800 blogTest[43808:3772452] TestClass
2019-12-18 20:05:28.646680+0800 blogTest[43808:3772452] TestClass
2019-12-18 20:05:28.646777+0800 blogTest[43808:3772452] TestClass
2019-12-18 20:05:28.646890+0800 blogTest[43808:3772452] TestClass
2019-12-18 20:05:28.646989+0800 blogTest[43808:3772452] TestClass
2019-12-18 20:05:28.647170+0800 blogTest[43808:3772452] 0
2019-12-18 20:05:28.647393+0800 blogTest[43808:3772452] 0
2019-12-18 20:05:28.647725+0800 blogTest[43808:3772452] 0
2019-12-18 20:05:28.648001+0800 blogTest[43808:3772452] 1
2019-12-18 20:05:28.648252+0800 blogTest[43808:3772452] 1


可以看出 c1 == c2,因为c1 通过 class 获取到的是他的类对象,类本身获取class也是其本身,所以c1=c2,object_getClass 是通过他的 isa 指针获取他的类对象,所以 [test class] 获取的是他的类对象,object_getClass([test class]),是通过他的类对象的isa指针,指向他的元类对象, class_isMetaClass 是获取是否为元类,所以分析上面的代码,class_isMetaClass([test class]) 为0,因为通过class获取的是当前的类对象,而类对象不是元类对象,所以为0,而 class_isMetaClass(object_getClass([test class])) ,是先获取他的类对象,然后通过类对象的isa指针,指向他的元类对象,class_isMetaClass(object_getClass([test class])),然后再打印是否为元类对象,所以为1.

总结:

实例对象objc_object其实是一个结构体,他只有一个成员变量isa,指向他的类对象,这个类对象存放了变量和实例方法等数据,而类对象是通过元类创建的,元类中保存了类方法和类变量

消息转发

当我们调用衣蛾方法的时候,如[self test],其实就是为这个方法添加了一个编号SEL(test),sel 是一个实例,而IMP是最终指向实现程序的内存地址的指针,当调用这个方法的时候,系统去方法列表中查找,ios 之所以不可以使用重载,就是方法名字相同而参数不同,是因为sel只记住了方法名字,而没有记住参数,所以是不行的
如果我们调用了一个方法之后,就会在当前的类对象的方法列表中去查找,如果查不到,就会沿着继承树一直向上查找,知道根部nsobject,如果还是找不到就会调用doesNotRecognizeSelector:方法报unrecognized selector。

消息转发流程


1.+(BOOL)resolveInstanceMethod:(SEL)sel

2.-(id)forwardingTargetForSelector:(SEL)aSelector

3.  -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

    -(void)forwardInvocation:(NSInvocation *)anInvocation


1.动态方法解析(resolveInstanceMethod)

oc 在运行时,会调用 +(BOOL)resolveInstanceMethod:(SEL)sel 或者 +(BOOL)resolveClassMethod:(SEL)sel 两个方法,让你有机会提供一个函数实现,如果提供了函数实现返回了 yes , 那么系统会重新进行一次消息发送,如下

void  toPrint(id obj,SEL  _cmd){
    printf("to test resolveInstanceMethod");
}
+(BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(print)) {
        class_addMethod([self class], sel, (IMP)toPrint, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

可以看到我们运行时,添加了一个方法,并且提供了实现返回了yes,当我们外面调用的时候,也成功打印了。

2.交给其他接受者处理(forwardingTargetForSelector)

如果实现了这个方法,那么runtime 就允许你,转发给其他对象的机会


@interface Forwarding : NSObject

@end
@implementation Forwarding
- (void)print{
    NSLog(@"forwarding to print");
}
@end

-(id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector == @selector(print)) {
        return [Forwarding new];
    }
    return [super forwardingTargetForSelector:aSelector];
}

这里我们告诉runtime,让 forwarding 类来处理这个方法。

3.消息转发最后一步,完整的消息转发 (methodSignatureForSelector)

如果上面两个两个方法都没有相应,没关系,runtime还提供了最后一个转发,完整消息转发

首先会调用 methodSignatureForSelector 来获取完整的 签名,也就是返回值类型,参数类型,如果返回nil,那么runtime会发出doesNotRecognizeSelector的异常,如果返回了一个签名,那么runtime 就会创建一个NSInvocation对象,并且 发送forwardInvocation,给目标对象.

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if (aSelector == @selector(print)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return nil;
}
-(void)forwardInvocation:(NSInvocation *)anInvocation{
    SEL sel = anInvocation.selector;
    Forwarding *f = [[Forwarding alloc] init];
    if ([f respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:f];
    } else {
        [self doesNotRecognizeSelector:sel];
    }
}

可以看到,我们首先返回给了runtime 一个方法签名,v@:,表示返回值是void,将self和sel作为参数传递,关于方法签名相关,之后的文章我会讲解,其实官方文档有相关的介绍,当我们返回了一个签名之后,就调用forwardInvocation,进行转发。

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

推荐阅读更多精彩内容

  • 我们常常会听说 Objective-C 是一门动态语言,那么这个「动态」表现在哪呢?我想最主要的表现就是 Obje...
    Ethan_Struggle阅读 2,159评论 0 7
  • 本文转载自:http://yulingtianxia.com/blog/2014/11/05/objective-...
    ant_flex阅读 735评论 0 1
  • 本文详细整理了 Cocoa 的 Runtime 系统的知识,它使得 Objective-C 如虎添翼,具备了灵活的...
    lylaut阅读 788评论 0 4
  • 文中的实验代码我放在了这个项目中。 以下内容是我通过整理[这篇博客] (http://yulingtianxia....
    茗涙阅读 901评论 0 6
  • 一、Runtime简介 Runtime简称运行时。OC就是运行时机制,也就是在运行时候的一些机制,其中最主要的是消...
    林安530阅读 1,056评论 0 2