本文介绍 Objective-C Category(分类) 与内存的关系,以及通过 关联对象(Associated Objects) 在 Category 中「挂载」数据时的内存管理:关联策略(policy)、释放时机、循环引用与最佳实践。前置知识见 04-ARC详解、06-weak与循环引用。
一、Category 与内存的关系
1.1 Category 是什么
- Category 用于在不修改原类的前提下,为已有类添加方法(以及通过关联对象间接添加「属性」式的存储)。
- Category 不能直接添加实例变量(ivar),因此不会改变类实例的内存布局与 sizeof;实例大小由原类及其子类的 ivar 决定。
1.2 对内存管理的影响
| 维度 | 说明 |
|---|
| 实例大小 | Category 不增加实例占用,无需从「对象体积」角度做特殊内存管理。 |
| 方法实现 | Category 中的方法若创建或持有对象,仍遵循 ARC/MRC 规则(谁持有谁释放、避免循环引用)。 |
| 「属性」存储 | 若在 Category 中通过 关联对象 模拟属性,则关联的 value 的持有方式 由 association policy 决定,需正确设置以避免泄漏或野指针。 |
下文重点说明关联对象的内存语义与使用注意。
二、关联对象(Associated Objects)简述
2.1 作用
- 在不增加 ivar 的前提下,把键值对绑在某个对象上:主对象被释放时,运行时会自动释放其关联的 value(按 policy 做 release 等)。
- 常用于在 Category 中为已有类添加「存储型属性」、或为任意对象挂载扩展数据。
2.2 API(Objective-C 运行时)
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
id objc_getAssociatedObject(id object, const void *key);
objc_setAssociatedObject(object, key, nil, policy);
三、关联策略(policy)与内存管理
3.1 常用策略对照表
| 策略常量 | 语义(对 value 的持有方式) | 适用场景 |
|---|
| OBJC_ASSOCIATION_RETAIN | 强引用(retain),主对象释放时对 value release | 普通 OC 对象属性(类似 strong) |
| OBJC_ASSOCIATION_COPY | 拷贝后强引用(copy),主对象释放时对拷贝 release | 字符串、block 等需拷贝的类型 |
| OBJC_ASSOCIATION_ASSIGN | 不持有(assign),主对象释放时不对 value 做 release | 基本类型、或「弱引用」场景(注意野指针) |
| OBJC_ASSOCIATION_RETAIN_NONATOMIC | 同 RETAIN,非原子 | 性能敏感、不需原子性时 |
| OBJC_ASSOCIATION_COPY_NONATOMIC | 同 COPY,非原子 | 同上 |
3.2 与 ARC 属性修饰符的对应
| 若属性声明为 | 关联时建议 policy |
|---|
| strong(对象) | OBJC_ASSOCIATION_RETAIN |
| copy(block/NSString) | OBJC_ASSOCIATION_COPY |
| assign / weak | OBJC_ASSOCIATION_ASSIGN(assign 不保证置 nil,若为对象有野指针风险;true weak 需运行时支持,关联对象常用 ASSIGN 存 weak 包装或非持有) |
3.3 释放时机
- 主对象 dealloc 时,运行时会自动对所有关联的 value 按各自 policy 执行 release(或等效操作),无需在 dealloc 里手动
objc_setAssociatedObject(..., nil, ...) 或单独 release。
- 若在业务上希望提前解除某条关联,可主动
objc_setAssociatedObject(object, key, nil, policy),原 value 会按 policy 被释放。
四、Category 中「属性」的常见写法与内存
4.1 强引用存储(RETAIN)
static const void *kMyKey = &kMyKey;
- (void)setMyProperty:(id)obj {
objc_setAssociatedObject(self, kMyKey, obj, OBJC_ASSOCIATION_RETAIN);
}
- (id)myProperty {
return objc_getAssociatedObject(self, kMyKey);
}
- 内存:set 时对新 value retain、对旧 value release;主对象 dealloc 时自动 release 当前 value,无泄漏。
- 注意:若
myProperty 内部又强引用主对象(如 block 捕获 self),会循环引用,需用 weak 打破(见下)。
4.2 拷贝存储(COPY,如 block)
- (void)setMyBlock:(void (^)(void))block {
objc_setAssociatedObject(self, kBlockKey, block, OBJC_ASSOCIATION_COPY);
}
- Block 常用 COPY,与属性
copy 一致;主对象释放时会对拷贝的 block release。
4.3 弱引用 / 不持有(ASSIGN)与循环引用
- 若用 OBJC_ASSOCIATION_ASSIGN 存一个对象指针,主对象不会持有该对象;但主对象 dealloc 时不会把该指针置 nil,若外部未持有,可能产生野指针。
- 循环引用:主对象 A 通过 RETAIN 关联了对象 B,B 又强引用了 A → 双方都不释放。解决办法:让 B 对 A 使用 weak(若 B 是自定义类可改);或 A 不通过 RETAIN 关联 B,改用 ASSIGN + 弱引用包装(需注意生命周期与野指针)。
- Category 中若「属性」是 delegate 或会反向引用 self 的对象,应避免用 RETAIN 持有该对象,可考虑 ASSIGN 存 weak 包装或不在 Category 里存该引用。
五、流程图:关联对象生命周期
flowchart LR
A[主对象存在] --> B[setAssociatedObject value policy]
B --> C[value 被 retain/copy 等]
A --> D[主对象 dealloc]
D --> E[运行时按 policy 释放所有关联 value]
E --> F[value 引用计数减一 或 置空]
六、小结与最佳实践
| 场景 | 建议 |
|---|
| Category 中存普通 OC 对象 | 使用 OBJC_ASSOCIATION_RETAIN(或 RETAIN_NONATOMIC)。 |
| Category 中存 block / 需拷贝类型 | 使用 OBJC_ASSOCIATION_COPY。 |
| 不持有、仅赋值指针(如 delegate) | 可用 OBJC_ASSOCIATION_ASSIGN,注意主对象释放后不置 nil,避免野指针。 |
| 避免循环引用 | 不在 Category 中用 RETAIN 关联「会强引用主对象」的对象;或对方对主对象使用 weak。 |
| 释放 | 主对象 dealloc 时关联会自动清理,一般无需在 dealloc 里手动移除。 |
参考文献