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


一、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  {
    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中消息转发机制及其底层实现逻辑)