09-主题|内存管理@iOS-Category与关联对象内存管理

3 阅读4分钟

本文介绍 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 运行时)

// 设置:object 为主对象,key 为键,value 为值,policy 为关联策略
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);

// 获取
id objc_getAssociatedObject(id object, const void *key);

// 移除(将 value 设为 nil 即可,会按 policy 释放原 value)
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 / weakOBJC_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)

// Category 中为 NSObject 添加一个“强引用”属性
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 里手动移除。

参考文献