动态生成关联对象属性的存取方法
Objective-C的Category可以灵活的为已经存在的类增加方法,但是不能增加“存储属性”,如果想要扩展类的存储空间,可以使用关联对象来实现。比如扩展一个存储 NSNumber * 类型的属性,代码如下:
static char kPropertyConstraintsKey;
- (void)setExternProperty:(NSNumber *)value {
objc_setAssociatedObject(self, &kPropertyConstraintsKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSNumber *)externProperty {
return objc_getAssociatedObject(self, &kPropertyConstraintsKey);
}
几乎所有的关联对象的写法都是这样,增加一个属性还好,如果多了,会有很多重复代码。这种代码已经模式化毫无技术含量,而且写起来容易出错,很可能把key放在了错误的属性身上。能不能不写这样的重复代码,而实现功能呢,下面提出一种思路。
Objective-C方法的最终实现体是IMP函数,它是特殊的c函数,前两个参数必须为(id self, SEL _cmd),第一个是当前方法的调用者,第二个是与之绑定的SEL。c函数的入口地址必须在编译时确定,所以无法动态增加。强大的运行时机制,可以为类新加方法,其实就是绑定新的SEL到一个确定的IMP函数,动态增加方法等同于动态绑定函数。上述设置属性和获取属性的IMP函数分别为:
void objSetterIMP(id self, SEL _cmd, id obj) {
objc_setAssociatedObject(self, _cmd, obj, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
id objGetterIMP(id self, SEL _cmd) {
return objc_getAssociatedObject(self, _cmd);
}
所有Category对象属性的Setter、Getter方法,都可以使用上述两个函数来实现的。为了完成动态生成关联对象属性的存取方法的目的,还有两个重要的知识点要掌握,一个是指定属性为@dynamic时,编译器会无视属性的Setter、Getter方法,直到运行时发现不能处理再抛出异常,这样就可以不用写一堆方法实现而顺利通过编译。另一个是Objective-C方法查找机制,由于并没有真正的方法实现,通过SEL查找方法实现会失败,但是这时还有两次机会来处理消息。第一次是动态方法决议Dynamic Method Resolution,第二次是消息转发Message Forwarding。我们选择在动态方法决议里来处理消息,使用runtime的class_addMethod方法将该消息绑定到对应的IMP上,重新进行消息传递。
@interface SomeClass(someCategory)
@property(noatomic, strong) NSNumber *object1;
@property(noatomic, strong) NSNumber *object2;
@property(noatomic, strong) NSNumber *object3;
@property(noatomic, strong) NSNumber *object4;
@end
@implementation SomeClass (someCategory)
@dynamic object1;
@dynamic object2;
@dynamic object3;
@dynamic object4;
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSString *msg = NSStringFromSelector(sel);
if ([msg hasPrefix:@"setObject"]) {
class_addMethod(self, sel, (IMP)objSetterIMP, "v@:@");
return YES;
}
else if([msg hasPrefix:@"object"]) {
class_addMethod(self, sel, (IMP)objGetterIMP, "@@:");
return YES;
}
else {
return [super resolveInstanceMethod:sel];
}
}
@end
当然上述的实现是简单粗略的,判断具体是什么消息时只用了简单的字符串比较,而且只能正确处理对象类型的strong属性。不同的属性类型,甚至是不同的内存管理语义,对应的IMP都是不同的。写好不同的IMP函数,并且能正确判断是何种消息,然后绑定,就酱。