在“分类 vs extension”一文中,我们总结过“分类不能添加存储属性”。原因是类的内存布局在编译时就已确定,后加载的分类没有位置插入 ivar。
但在实际代码中,我们经常会遇到分类中确实存在属性的情况。秘密就是 Associated Objects:一种无需 ivar 即可将值挂到对象上的运行时功能。
这是解决分类存储属性问题的官方变通方案,也是 Objective-C 运行时隐藏在对象外部的辅助存储。
原理:附着在对象旁边的侧表
Associated Objects 的核心函数有两个。
objc_setAssociatedObject(目标对象、键、值、内存策略);
objc_getAssociatedObject(目标对象、键);
值不会存储在对象的 ivar 区域中。运行时会在全局管理的 **关联表(侧表)**中,以“对象地址 → {键: 值}”的形式记录。
对象本体的内存布局一个字节也不会改变,因此不会与编译时确定的布局限制冲突。
目标对象释放后,运行时会在关联表中找到对应项,并根据策略一并清理。生命周期由系统自动管理,适合实际使用。
分类属性的完整实现
在分类中声明属性时,编译器只会生成 getter/setter 声明,不会创建存储空间。用 Associated Objects 填上这个空缺即可。
#import <objc/runtime.h>
@interface UIView (BadgeCount)
@property (nonatomic, strong) NSNumber *badgeCount;
@end
@implementation UIView (BadgeCount)
- (NSNumber *)badgeCount {
return objc_getAssociatedObject(self, @selector(badgeCount));
}
- (void)setBadgeCount:(NSNumber *)badgeCount {
objc_setAssociatedObject(self, @selector(badgeCount),
badgeCount,
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
使用 @selector(badgeCount)作为键是惯例。键不是按值比较,而是按指针地址比较,因此只要是整个应用中唯一的地址即可。
选择器的唯一性由运行时保证,也不需要额外声明 static 变量,因此最简洁。像 static void *kBadgeKey = &kBadgeKey;这样的经典方法仍然有效。
使用字符串字面量作为键是个陷阱。即使内容相同,只要编译单元不同,地址就可能不同。
“明明保存了却得到 nil”的谜团就由此产生。
内存策略:只需小心 assign
第四个参数对应属性特性。
| 策略 | 对应特性 |
|---|---|
OBJC_ASSOCIATION_RETAIN_NONATOMIC |
strong, nonatomic |
OBJC_ASSOCIATION_COPY_NONATOMIC |
copy, nonatomic |
OBJC_ASSOCIATION_RETAIN / COPY |
与上者相同,但为 atomic |
OBJC_ASSOCIATION_ASSIGN |
assign — 不是 weak |
唯一需要注意的是 ASSIGN。虽然名字看起来像 weak,但它是不会自动变成 nil 的 unsafe_unretained。
如果关联对象先被释放,就会留下悬空指针。一旦访问就会崩溃。
如果需要 weak 语义,就要将用 weak 属性包装值的包装对象以 RETAIN 方式关联起来。
要删除特定的关联值,设置为 nil 即可。objc_removeAssociatedObjects会清除该对象的全部关联值,威力过强,实际上不应使用。
实际应用与边界
实际项目中常见的组合如下。
- UIKit 类扩展:给 UIView 添加徽章数量,给 UIButton 添加基于闭包的动作处理器,给 UIViewController 添加用于分析的页面名称
- 代理到 block 的转换包装器:将代理对象关联到原对象,以绑定两者的生命周期(Alamofire 之前的网络分类经常使用这种模式)
- 与 swizzling 配套:当通过 swizzling 插入的逻辑需要保存状态时使用。这就是运行时系列中的两种技术经常一起出现的原因。
不过仍然要守住边界。Associated Objects 终究只是存放辅助数据的通道。
如果对象的核心状态分散在关联表中,读代码的人就很难掌握整体设计。如果可以创建子类,使用 ivar 才是正确答案。
只有在需要为不拥有的类附加额外信息时,才应该使用这个方案。
绕过 Swift extension 的 stored property 限制时也可以使用相同函数(仅限 NSObject 系列)。但在 Swift 中,使用协议加专用存储类型,或通过 composition 解决,通常更自然。
总结
- Associated Objects 将值存储在运行时的侧表中,而不是对象本体中,因此分类也能使用。
- 键按指针地址比较。复用
@selector是惯例,字符串字面量则是陷阱。 - 在内存策略中,
ASSIGN不是 weak,而是 unsafe_unretained。请注意悬空指针导致的崩溃。 - 目标对象释放后,关联值也会自动清理。
- 用途仅限于为不拥有的类附加辅助数据。把核心状态放在这里,就是设计上的红灯信号。
运行时系列的下一篇是 NSProxy。我们将深入分析消息转发一文中预告的另一个非 NSObject 根类,它如何作为“纯粹的代理”运行。

