Category是一个Objective-C语法中允许你用额外的方法扩展类的一个很不错的的特性;它通过直接为类添加方法的方式帮我们写出简洁的的代码,故被称作天生的面向对象的方法。
下面是个典型的分类方法的例子,这里我们给NSString
添加了一个isNumeric
方法:
@interface NSString (NumberUtils)
- (BOOL)isNumeric;
@end
@implementation NSString (NumberUtils)
- (BOOL)isNumeric
{
NSScanner *scanner = [NSScanner scannerWithString:self];
return [scanner scanFloat:NULL]? [scanner isAtEnd]: NO;
}
@end
在分类中你是可以声明新属性的,但是你不能synthesize
他们。因为通过分类添加一个实例变量到一个类上在语法上是不可行的--类的内部数据结构在编译的时候已经确定了,分类是不能改变的(因为分类是在运行时的时候才被加载的)。
你可以通过getter/setter
方法创造一个在语义上相似的虚拟属性,但这是不被变量支持的。例如,我们可以声明上面的isNumeric
方法作为一个只读的属性,如下:
@interface NSString (NumberUtils)
@property (nonatomic, readonly, getter=isNumeric) BOOL numeric;
@end
做完之后看起来我们可以使用属性的语法去调用方法了,但是如果我们想要在类中保存额外数据的时候,我们是不可能在这个属性中取到我们想要的数据的;
假设我们有一个可以让用户标记(tag)图片的APP,且我们想要保存这个tag
值作为每个UIImage
对象的一部分.这个tag
值可能会是一个逗号分隔的字符串。我们可以在分类中声明这个tag
属性,但是如果我们不能用一个变量去保存字符串,那么getter/setter
方法又该怎么用呢?
(在你提出疑问之前,我知道正确的解决这个问题的途径可能会是创建一个包含image
和tag
的一个封装的对象,但假设对于这个示例来说,我们可能没有时间去重构所有的image
处理方法去接受一个新的对象类型,毕竟老板此时正在催你及时搞定它!)
拯救你的Runtime
Cocoa的API封装是非常强大的,强大到很多开发者不敢冒险去使用这些表层接口以下的方法,从而错过使用像runtime
这样的牛X利器。runtime
的其中一个优雅的特征就是它允许你使用一个称为"关联对象"的机制去扩展一个拥有额外数据的对象。
就像这个名字一样,关联对象实质就是去关联一个对象。它使用唯一的一个key
值去保存和获取数据,非常像Objective-C中的字典。你可以通过下面这两个runtime
函数去get
和set
这些关联的对象,在这个被关联的对象销毁的时候,这两个函数所占的空间也会自动释放。
// Get a value associated with the object by key
id objc_getAssociatedObject(id object, const void *key)
// Associate a value with the object by key
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
函数中的参数object
就是那个将要被扩展的对象。参数key
是唯一一个在整个进程的生命周期中存在的标识。你可以使用NSString
来作为这个key
值,但是如果你试图使用一个相同文本且地址不同的字符串对象去访问这个值你是不可能访问到的。一个最好的方式是去声明一个私有的静态指针变量作为这个key
值的参数。
参数policy
是一个定义如何将对象进行保存的常量。这很像是你用来声明属性时使用的属性修饰符atomic/nonatomic
和retain/copy/assign
.
下面是我们如何使用关联对象方式来实现我们标记的图像分类。首先我们在分类头部文件中声明一个tag
属性:
@interface UIImage (Tagged)
@property (nonatomic, copy) NSString *tag;
@end
然后我们用关联对象runtime
的方法为这个属性实现setter
和getter
方法:
#import <objc/runtime.h>
static const void *ImageTagKey = &ImageTagKey;
@implementation UIImage (Tagged)
- (void)setTag:(NSSting *)tag
{
objc_setAssociatedObject(self, ImageTagKey, tag, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)tag
{
return objc_getAssociatedObject(self, ImageTagKey);
}
@end
下面是一些需要你注意的地方:
我们为关联对象所使用的参数
key
是一个类型为static const void *
的指针。我们需要对这个指针变量进行初始化,否则它的值可能会为NULL
(那样作为key
值就没有任何意义了),只要它是独一无二的,我们并不在乎这个指针指向的值到底是什么。为了避免分配任何不必要的额外内存空间,我们将它设置为指向自己!这个指针的值和这个指针的唯一的地址是一样的,且是唯一的(因为是static
), 同时不会改变(因为是const
).我们使用
OBJC_ASSOCIATION_COPY_NONATOMIC
为参数policy
赋值。这和我们在分类头文件中声明的tag
属性的属性修饰符是相对应的。我们用nonatomic
是因为这是一个UIKit
的类同时我们假设属性只能在主线程中被访问到。当处理的字符串或者其他有mutable
子类的类时,使用copy
是一个最佳的方式。
将Selector作为Key
在我们需要创建出许多的分类属性时,以上述的方式定义出许多的static const keys
无疑是一件很烦人的事。还好,现在还有个不错的选择:
Objective-C
中的selector
其实质上也是一个常量指针。这就意味着它们很适合作为关联对象时的key
值。假如你用关联对象的方式去实现一个属性,你可以使用这个属性的getter
方法的方法名作为这个key
.在我们上面所述的tag
例子中,我们可以这样改造它:
#import <objc/runtime.h>
@implementation UIImage (Tagged)
- (void)setTag:(NSSting *)tag
{
objc_setAssociatedObject(self, @selector(tag), tag, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)tag
{
return objc_getAssociatedObject(self, @selector(tag));
}
@end
这种方法除了使代码更简洁外,没有什么特别的优点。唯一潜在的缺点是,这种方式使的从外部获得类中相关联的对象更容易。静态key
是真正的private
,但@selector
确是public
的。鉴于我们是使用相关联的对象来实现public
的属性这一情况,不管怎么看,以上都是没毛病的。
一些注意点
有一些类型的对象,你不应该试图去和其他对象进行关联:
对于值类型(不可变且用于数据传输的对象,如NSString
、NSNumber
, NSValue
, UIColor
, NSIndexPath
等等),iOS中使用一种被称为de-duplication
(重复数据删除)的方式来减少内存开销和提高性能。de-duplication
意味着用一个实例替换多个相同的对象。对NSString
而言这意味着相同的两个字符串文本即使分别被初始化,可能最终在幕后也只会映射到同一个对象。
通常情况下,de-duplication
方式不会影响你编写代码,因为你不需要关心两个具有相同值的NSString
或NSNumber
对象是否是完全相同的对象。那是因为你总是用isEqual:
方法而不是用 == 来比较两者。但当你用一个值类型如NSString
真正来关联一个对象时,你会发现你关联过的字符串突然间被另一个实例对象所替换了,或者另一个没有任何联系的字符串变量已经继承了这种相同的关联。
当然在正常使用情况下你不太可能想把对象和值类型进行关联,但这还是需要你记住。
延伸阅读
更多信息请参阅苹果的Objective-C Runtime Reference:
- Associative Object Behaviors
- objc_getAssociatedObject
- objc_setAssociatedObject
- objc_removeAssociatedObject