iOS 内存管理:Associated Objects (关联对象)
1. 概述
关联对象(Associated Objects)允许我们把一段额外数据与某个对象建立运行时关系,常用于给 Category 的属性补充存储。它的使用体验像“给对象增加属性”,但不会改变类的实例大小,也不是真正把 ivar 塞进对象内存。
objc_setAssociatedObjectobjc_getAssociatedObjectobjc_removeAssociatedObjects
2. 底层存储结构
关联对象不是存储在对象本身的内存中(对象的内存布局在编译期固定),而是存储在全局的 AssociationsManager 中。
// 示意结构,不同 objc4 版本的命名和锁实现可能变化
AssociationsHashMap: object -> ObjectAssociationMap
ObjectAssociationMap: key -> ObjcAssociation(policy, value)
存储层级
-
AssociationsHashMap:全局单例。
-
Key: DisguisedPtr\ (即对象的地址)。
-
Value: ObjectAssociationMap。
-
-
ObjectAssociationMap:属于某个对象的所有关联对象表。
-
Key: const void *key (设置时的 key)。
-
Value: ObjcAssociation (包含 value 和 policy)。
-
-
ObjcAssociation:
-
uintptr_t _policy (如 COPY, RETAIN_NONATOMIC)。
-
id _value (实际值)。
-
3. 核心逻辑
3.1 设置值
-
在 Runtime 的同步保护下找到或创建宿主对象对应的关联表。
-
根据对象地址找到对应的 ObjectAssociationMap。
-
根据 key 找到对应的 ObjcAssociation。
-
按 policy 对新旧值执行 retain、copy 或 assign 语义。Runtime 会谨慎安排加锁区间与对象释放时机,避免在持锁状态下执行可能产生副作用的用户代码。
-
更新表。
3.2 释放对象
当对象销毁时 (dealloc):
-
Runtime 调用 objc_destructInstance。
-
检查 isa.has_assoc 标记位。
-
如果有,调用 _object_remove_assocations。
-
从全局 Map 中移除该对象的所有关联数据,并对 Value 发送 release。
宿主对象正常销毁时,Runtime 会移除并释放其关联值,但这不等于“关联对象不会造成内存泄漏”。如果宿主强持有关联对象,而关联对象又强持宿主,就会形成 ARC 无法自动打破的强引用环。
4. Association Policy 应该怎么选
常见策略与属性语义大致对应:
| Policy | 常见用途 | 注意点 |
|---|---|---|
OBJC_ASSOCIATION_ASSIGN | 非拥有关系 | 不具备 weak 的自动置 nil 能力,目标释放后可能留下悬空地址,通常不建议拿它模拟 weak |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | 非原子强引用 | 最常见,适合多数 UI 与业务扩展 |
OBJC_ASSOCIATION_COPY_NONATOMIC | Block、字符串或需要值语义的对象 | Block 通常应 copy,具体仍要看数据语义 |
OBJC_ASSOCIATION_RETAIN / COPY | 带原子化获取/设置语义 | 有额外同步成本,也不等于让一组复合操作自动线程安全 |
关联 key 推荐使用地址稳定且模块内唯一的静态变量地址:
static void *UserTokenKey = &UserTokenKey;
objc_setAssociatedObject(self,
UserTokenKey,
token,
OBJC_ASSOCIATION_COPY_NONATOMIC);
也可以使用 selector 作为 key,例如 getter 的 @selector(userToken)。关键不是字符串内容,而是 const void * 地址在进程生命周期内稳定且不会和别处冲突。
5. 线程安全边界
关联对象 API 内部需要保护全局数据结构,所以单次 get/set 不会把表本身写坏。但这不代表你的业务逻辑天然线程安全。
例如“先读取数组、追加元素、再写回”是三个步骤。两个线程同时执行仍然可能丢数据。即使选择原子 policy,也只能约束单次访问,无法替你保护跨多步的不变量。需要一致性时,仍应在更高层使用锁、串行队列或 actor 风格的隔离方案。
6. 什么时候不该用关联对象
- 数据是类型核心状态,应优先放进正式类定义,而不是藏在 Category。
- 需要真正的 weak 语义时,不要用
ASSIGN冒充。 - 生命周期复杂、容易形成环时,应明确画出宿主与关联值之间的强弱关系。
- 高频热路径上大量读写时,要意识到它包含哈希查找与同步开销,不能当作普通 ivar 等价替代品。
总结:关联对象会随宿主清理,但错误的持有关系仍然会泄漏;API 的单次访问受 Runtime 保护,也不等于整段业务逻辑线程安全。