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,进行转发。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

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