给分类添加属性
咱们知道,分类中可以添加方法,却无法添加实例变量。那咱们有什么方法在既有类中存放自定义数据呢?
先来看看下面这段代码:
@interface UIView (nl_Frame)
@property (nonatomic, assign) CGFloat nl_width;
@end
@implementation UIView (nl_Frame)
- (void)setNl_width:(CGFloat)nl_width {
CGRect frame = self.frame;
frame.size.width = nl_width;
self.frame = frame;
}
- (CGFloat)nl_width {
return CGRectGetWidth(self.frame);
}
@end
在这里给 UIView增加了一个宽度属性:nl_width
,而且为其实现了相应的 getter和 setter方法(nl_width
、setNl_width:
)。这两个方法实际上访问的 frame
属性,如果你有注意过的话,你会发现frame
也是定义在分类里边的:
@interface UIView(UIViewGeometry)
@property(nonatomic) CGRect frame;
//...
@end
可以看到,这种定义在分类里的属性,实际上是实现了相应的方法,并在方法里边通过访问其它属性来达到目的。这通常用来简化某些操作,比如定义咱们这个分类后,获取视图的宽度只要view.nl_width
就可以了,再不用CGRectGetWidth(view.frame)
来得到宽度,而且可读性也增强了很多。
再来看看这个需求:在 sqlite中,第一个表如果在没有指定主键的情况下,那默认就会定义一个主键rowid
。咱们就把这个 rowid
直接放到 NSObject
里边,作为属性,那么任何对象也会有这个主键rowid
了。但是这个rowid
却无法像上边的nl_width
一样通过访问其它属性来达到目的。那该怎么办?
关联对象
本节的主角出场了:关联对象
在使用关联对象之前,得先引入头文件:
#import <objc/runtime.h>
可以在该头文件中找到三个允许你将任何键值在运行时关联到对象上的函数:
objc_setAssociatedObject // 设置关联对象
objc_getAssociatedObject // 获取关联对象
objc_removeAssociatedObjects // 移除关联对象
既然有了这个工具,那么咱们再来看看:
@interface NSObject (nl_sqlite)
@property (nonatomic, assign) NSUInteger rowid;
@end
@implementation NSObject (nl_sqlite)
static void *nl_sqlite_rowid_key = &nl_sqlite_rowid_key;
- (void)setRowid:(NSUInteger)rowid {
objc_setAssociatedObject(self, nl_sqlite_rowid_key, @(rowid), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSUInteger)rowid {
return [objc_getAssociatedObject(self, nl_sqlite_rowid_key) unsignedLongValue];
}
@end
上面的代码,就是通过关联对象给NSObject
增加了一个rowid
“属性”。关联对象在使用时,需要咱们提供一个指针,即key
,用来识别被关联的对象。咱们这里的key
就是一个空指针:nl_sqlite_rowid_key
。当然,你也可以@selector(rowid)
来作为 key
(常用)。
于是,就可以这么来用了:
id person = [NSObject new];
person.rowid = 1;
很爽吧!以后就可以给已有类添加“属性”了。这可是一个很强大的功能哟,如果你查看过一些强大的第三方库的话,就会发现,这是一个常用的技巧。
关联类型 | 等效的@property属性 |
---|---|
OBJC_ASSOCIATION_ASSIGN | assign |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | nonatomic, retain |
OBJC_ASSOCIATION_COPY_NONATOMIC | nonatomic, copy |
OBJC_ASSOCIATION_RETAIN | retain |
OBJC_ASSOCIATION_COPY | copy |
为什么分类无法添加实例变量
这个问题牵扯到内存布局。有一个个人信息的类,定义的实例变量如下:
@interface Person : NSObject {
NSString *_firstName;
NSString *_lastName;
NSString *_someInternalData;
}
如果你对 C++或 Java比较熟悉的话,那你也应该比较熟悉这种写法,在这些语言中,可以定义实例变量的作用域。然而编写 OC代码时,却很少这么做。这种写法的问题是:对象布局在编译期就已经固定了。只要碰到访问_firstName变量的代码,编译器就把其替换为“偏移量”(offset),这个偏移量是“硬编码”(hardcode),表示该变量距离存放对象的内存区域的起始地址有多远。这样做目前来看没问题,但是如果又加了一个实例变量,那就麻烦了。比如说,假设在_firstName之前又多了一个实例变量:
@interface Person : NSObject {
NSDate *_dateOfBirth;
NSString *_firstName;
NSString *_lastName;
NSString *_someInternalData;
}
原来表示_firstName的偏移量现在却指向_dateOfBirth了。把偏移量硬编码于其中的那么代码都会读取到错误的值。
如果代码使用了编译期计算出来的偏移量,那么在修改类定义之后必须重新编译,否则就会出错。例如,某个代码库中的代码使用了一份旧的类定义。如果和其相链接的代码使用了新的定义,那么运行时就会出现不兼容现象。当然,真实的情况远比这复杂。
所以在分类中,自然也就无法添加实例变量了。不然会打乱类中的偏移量。
关联对象的基本原理
OC已开源:http://opensource.apple.com/tarballs/objc4/objc4-646.tar.gz
这里只分析其基本原理:
在运行时环境中,有一个全局的 hashMap:associations。它用来记录那些有“关联对象”的对象。当一个对象object第一次关联对象时,这个 associations就将其作为 key加入到 hashMap中,而 value则是一另一个 map对象。就以上面的 rowid来说,当第一次 setRowid时,伪代码如下:
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[object] = refs;
(*refs)[key] = ObjcAssociation(policy, rowid);
看到这里,对象跟 NSDictionary很像呢!把关联到该对象的值看成字典中的 value。于是,存取关联对象的值就相当于在 NSDictionary对象上调用[object setObject:value forKey:key]与[object objectForKey:key]方法。然而两者之间有个重要的差别:设置关联对象时用的 key是个“不透明的指针”。如果在两个 key上调用“isEqual:”方法的返回值是 YES,那么 NSDictionary就认为二者相等;然而在设置关联对象值时,若想令两个键匹配到同一个值,则二者必须是完全相同的指针才行。