一、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的消息转发流程
消息转发包括两个步骤:
- 先征询接收者所属的类,看其能否动态添加方法,以处理当前这个“未知的选择子”,该过程叫——“动态方法解析”。
- 如步骤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()方法实现了函数查找和匹配,该方法通过一下步骤来查找和调用:
- 根据对象obj找到对象类中存储的函数列表methodLists。
- 根据选择子@selector(doSth)在methodLists中查找对应的函数指针method_imp。
- 根据函数指针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;
- 流程:
- obj->isa(Class类型) :obj对象通过isa属性拿到对应的Class。
- Class->methodLists(objc_method_list类型): Class通过methodLists属性拿到存放所有方法的列表。
- objc_method_list->method_list: 在objc_method_list中通过SEL查找到对应的objc_method。
- objc_method->method_imp(IMP类型): objc_method通过method_imp属性拿到函数指针。
- 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。包括以下两个步骤:
- 通过SEL在cache中查找objc_method,若找到了直接返回,若未找到执行2 。
- 在methodLists中查找objc_method,找到之后先将objc_method插入cache中以方便下次查找,再返回objc_method。