美文网首页首页投稿(暂停使用,暂停投稿)iOS开发记录
RunTime中消息转发机制及其底层实现逻辑

RunTime中消息转发机制及其底层实现逻辑

作者: 神采飞扬_2015 | 来源:发表于2017-05-23 15:35 被阅读77次

    一、RunTime概念

    Runtime 是一个运行时库(Runtime Library),它是一个主要使用 C 和汇编写的库,为 C 添加了面相对象的能力并创造了 Objective-C。这就是说它在类信息(Class information) 中被加载,完成所有的方法分发,方法转发,等等。Objective-C runtime 创建了所有需要的结构体。


    RunTime作用

    • RunTime可以遍历对象的属性。
    • RunTime可以动态添加/修改属性,动态添加/修改/替换方法,动态添加/修改/替换协议。
    • RunTime可以动态创建类/对象/协议等。
    • RunTime可以方法拦截调用。
    • ......

    如遍历对象属性:

    @interface Person : NSObject
    
    @property (nonatomic, copy) NSString *name;
    @property (nonatomic, assign) NSInteger age;
    
    @end
    
        Person *person = [Person new];
        id personClass = object_getClass(person);
        unsigned int outCount;
        
        objc_property_t *properties = class_copyPropertyList(personClass, &outCount);
        for (int i = 0; i < outCount; i++) {
            objc_property_t property = properties[i];
            NSLog(@"%s:%s\n", property_getName(property), property_getAttributes(property));
        }
        free(properties);
    

    输出:

    2017-05-22 20:50:59.040087 TestRunTime[13427:12747880] name:T@"NSString",C,N,V_name
    2017-05-22 20:50:59.040154 TestRunTime[13427:12747880] age:Tq,N,V_age
    

    二、RunTime中的函数调用

    1、OC中的函数调用

    C语言中,仅申明一个函数不去实现,其他地方调用此函数,编译时就会报错(C语言编译时查找要执行的函数,找不到所以报错)。在OC中并不会报错,只有在运行时候才会报错(OC运行时才查找要执行的函数)。

    RunTime把对象的方法调用转化成消息发送的代码:

    OC: [obj doSth];
    runtime:objc_msgSend(obj, @selector(doSth);
    
    • objc_msgSend函数会依据接收者与选择子的类型来调用适当的方法。为完成此操作,该方法需要在接收者所属的类中寻找其“方法列表”(下文会提到),如果能找到与选择子名称相符的方法,就跳转至其实现代码。若找不到,那就沿着继承体系继续向上查找,等找到合适的方法再跳转。如最终还是找不到相符的方法,那就执行“消息转发”操作。
    • 同时,objc_msgSend会将匹配结果缓存到“快速映射表”(下文会提到)中,每个类都有这样一块缓存。如稍后还向该类发送与选择子相同消息,执行起来快很多。

    2、objc_msgSend的消息转发流程

    objc_msgSend的流程objc_msgSend的流程

    消息转发包括两个步骤:

    1. 先征询接收者所属的类,看其能否动态添加方法,以处理当前这个“未知的选择子”,该过程叫——“动态方法解析”。
    2. 如步骤1执行完,接收者无法以动态新增方法来响应。执行如下:首先,接受者看是否有其他对象能处理这条消息,若有则运行期系统把消息转给那个对象(即备援接收者),消息转发结束。若没有“备援接收者”,则启动完整消息转发机制:会把与消息相关的细节封装到NSInvocation中,再给接收者最后一次机会,令其设法解决当前未处理的这条消息。
    • 动态方法解析

    RunTime调用+ (BOOL)resolveInstanceMethod:(SEL)sel方法允许开发者对当前收到的消息func做出响应。此方案常用来实现@dynamic属性。

    // 给Person类加一个体重weight属性
    @property (nonatomic, assign) NSInteger weight;
    
    /**
     重写resolveInstanceMethod方法:动态方法解析
    
     @param sel <#sel description#>
     @return <#return value description#>
     */
    + (BOOL)resolveInstanceMethod:(SEL)sel {
        if (sel == @selector(setWeight:)) {
            class_addMethod([self class], sel, (IMP)setPropertyDynamic, "v@:");
            
            return YES;
        }
        
        return [super resolveInstanceMethod:sel];
    }
    
    void setPropertyDynamic(id self, SEL _cmd) {
        NSLog(@"Dynamic setWeight");
    }
    
    // 调用Person的setWeight方法
    Person *lision = [[Person alloc] init];
    lision.weight = 75;
    
    // 如果不重写+ (BOOL)resolveInstanceMethod:(SEL)sel方法本应异常,但打印出信息:
    2017-05-23 08:53:24.189509 TestRunTime[13457:12851395] Dynamic setWeight
    
    • 重定向

    如果没有重写+ (BOOL)resolveInstanceMethod:(SEL)sel方法,那就就会调用- (id)forwardingTargetForSelector:(SEL)aSelector方法,把这个消息让另一个对象来处理,这叫做重定向。

    现在Person类中添加一个weight属性。新建一个People类来等待重定向。

    @interface Person : NSObject
    @property (nonatomic, copy) NSString *name;
    @property (nonatomic, assign) NSInteger age;
    @property (nonatomic, assign) NSInteger weight;
    @end
    
    @interface People : NSObject
    @end
    

    给新写的People类加一个weight方法,注意:People没有weight属性。

    @implementation People
    
    - (NSInteger)weight {
        return 70;
    }
    
    - (void)setWeight:(NSInteger)weight {
        NSLog(@"%s", __func__);
    }
    
    @end
    

    在Person类中重写- (id)forwardingTargetForSelector:(SEL)aSelector方法:

    @implementation Person
    
    @dynamic weight;
    
    ///**
    // 重写resolveInstanceMethod方法:动态方法解析
    //
    // @param sel <#sel description#>
    // @return <#return value description#>
    // */
    //+ (BOOL)resolveInstanceMethod:(SEL)sel {
    //    if (sel == @selector(setWeight:)) {
    //        class_addMethod([self class], sel, (IMP)setPropertyDynamic, "v@:");
    //        
    //        return YES;
    //    }
    //    
    //    return [super resolveInstanceMethod:sel];
    //}
    //
    //void setPropertyDynamic(id self, SEL _cmd) {
    //    NSLog(@"Dynamic setWeight");
    //}
    
    - (id)forwardingTargetForSelector:(SEL)aSelector {
        if (aSelector == @selector(setWeight:) || aSelector == @selector(weight)) {
            People *people = [[People alloc] init];
            return people;
        }
        
        return [super forwardingTargetForSelector:aSelector];
    }
    
    @end
    
    
    Person *lision = [[Person alloc] init];
    lision.weight = 75;
    NSLog(@"weight = %ld", lision.weight);
        
    // 输出
    2017-05-23 10:38:45.377410 TestRunTime[13547:12879235] weight = 70
    

    发现虽然你给weight属性赋值明明是75,可是打印结果是:weight = 70。这就是Person类- (id)forwardingTargetForSelector:(SEL)aSelector方法中把这条信息抛给了people对象,调用了People类的weight方法。

    • 消息转发

    如果上面的两个方法都没有重写,并且消息依然是当前对象没有实现的方法,RunTime才会启用消息转发调用– (void)forwardInvocation:(NSInvocation *)anInvocation,需要注意的是这个方法花费代价较大,如果要实现把消息转发类似的功能建议最好使用重定向,而且再调用这个方法前RunTime会先调用- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector方法。

    继续给Person类加入属性:

    @interface Person : NSObject
    
    @property (nonatomic, copy) NSString *name;
    @property (nonatomic, assign) NSInteger age;
    @property (nonatomic, assign) NSInteger weight;
    @property (nonatomic, copy) NSString *ID;
    
    @end
    

    实现上面提到的两个方法:

    @implementation Person
    
    @dynamic ID;
    
    - (void)forwardInvocation:(NSInvocation *)anInvocation {
        People *people = [[People alloc] init];
        if ([people respondsToSelector:anInvocation.selector]) {
            [anInvocation invokeWithTarget:people];
        }
    }
    
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
        if (aSelector == @selector(setID:)) {
            // "v@:"代表的意思参见Objective-C Type Encodings,这里的意思是返回值为空
            return [NSMethodSignature signatureWithObjCTypes:"v@:"];
        }
    
        return nil;
    }
    
    @end
    

    在People类中添加对应的set方法:

    @implementation People
    
    - (void)setID:(NSString *)ID {
        NSLog(@"People setID:%@", ID);
    }
    
    @end
    
    

    输出:

    Person *lision = [[Person alloc] init];
    lision.ID = @"xxxxx";
    
    // 输出
    2017-05-23 11:11:11.368598 TestRunTime[13598:12891137] People setID:xxxxx
    

    三、OC中函数调用底层实现

    将调用函数的对象obj和函数的方法名对应的选择子@selector(doSth)作为参数传入objc_msgSend()方法中,由objc_msgSend()方法实现了函数查找和匹配,该方法通过一下步骤来查找和调用:

    1. 根据对象obj找到对象类中存储的函数列表methodLists。
    2. 根据选择子@selector(doSth)在methodLists中查找对应的函数指针method_imp。
    3. 根据函数指针method_imp调用响应的函数。

    objc_msgSend的底层原理

    • 任意一个NSObject对象,都有一个isa属性,指向对象对应的Class类
    @interface NSObject <NSObject> {
        Class isa  OBJC_ISA_AVAILABILITY;
    }
    
    • 对象对应的Class,是一个结构体指针,指向objc_class结构体
    /// An opaque type that represents an Objective-C class.
    typedef struct objc_class *Class;
    
    struct objc_class {
        // 指向metaclass
        Class isa  OBJC_ISA_AVAILABILITY;
    
    #if !__OBJC2__
        // 指向父类Class
        Class super_class                                        OBJC2_UNAVAILABLE;
        // 类名 
        const char *name                                         OBJC2_UNAVAILABLE;
        // 类的版本信息
        long version                                             OBJC2_UNAVAILABLE;
        // 一些标识信息,标明是普通的Class还是metaclass
        long info                                                OBJC2_UNAVAILABLE;
        // 该类的实例变量大小(包括从父类继承下来的实例变量)
        long instance_size                                       OBJC2_UNAVAILABLE;
        //类中成员变量的信息
        struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
        // 类中方法列表
        struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
        // 查找方法的缓存,用于提升效率
        struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
        // 存储该类遵守的协议  
        struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
    #endif
    
    } OBJC2_UNAVAILABLE;
    /* Use `Class` instead of `struct objc_class *` */
    
    • objc_class中有一个methodLists,是一个objc_method_list结构体
    struct objc_method_list {
        // 废弃、过时的属性
        struct objc_method_list *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;
    }  
    

    为什么是method_list[1],数组的大小怎么会是1呢?由于数组的大小是不定的,不同的类对应的不同的方法个数,所以定义时只存储首地址,在实际使用过程中再扩展长度。

    • objc_method结构体
    struct objc_method {
        // 函数的SEL
        SEL method_name                                          OBJC2_UNAVAILABLE;
        // 函数的类型
        char *method_types                                       OBJC2_UNAVAILABLE;
        // 函数指针
        IMP method_imp                                           OBJC2_UNAVAILABLE;
    }                                                            OBJC2_UNAVAILABLE;
    
    • 流程:
    1. obj->isa(Class类型) :obj对象通过isa属性拿到对应的Class。
    2. Class->methodLists(objc_method_list类型): Class通过methodLists属性拿到存放所有方法的列表。
    3. objc_method_list->method_list: 在objc_method_list中通过SEL查找到对应的objc_method。
    4. objc_method->method_imp(IMP类型): objc_method通过method_imp属性拿到函数指针。
    5. method_imp->调用函数:通过函数指针调用函数。

    函数调用中cache的使用

    • SEL是什么
    /// An opaque type that represents a method selector.
    /// 一种不透明的类型,它代表着一个方法选择器。
    typedef struct objc_selector *SEL;
    

    SEL本质是一个int类型的地址,指向存储的方法名。对于每一个类,都会分配一块特殊空空间,专门存储类中的方法名,SEL就是指向对应方法名的地址。由于方法名字符串是唯一的,所以SEL也是唯一的。

    • cache的使用

    从上面的流程:obj->isa(Class类型)->methodLists(objc_method_list类型)->objc_method->method_imp(IMP类型)->调用函数,可以看出,函数调用的时间主要消耗在“objc_method_list->method_list”,即在objc_method_list中通过SEL查找到对应的objc_method。cache就是对该过程进行优化。

    可以把cache简单当成一个哈希表,key是SEL,Value是objc_method。包括以下两个步骤:

    1. 通过SEL在cache中查找objc_method,若找到了直接返回,若未找到执行2 。
    2. 在methodLists中查找objc_method,找到之后先将objc_method插入cache中以方便下次查找,再返回objc_method。

    相关文章

      网友评论

        本文标题:RunTime中消息转发机制及其底层实现逻辑

        本文链接:https://www.haomeiwen.com/subject/hnfgxxtx.html