分类能否添加属性?实例变量?

在讲解 Associative References 为为何物之前,我们先看一下这个问题:

分类中能否添加实例变量?如果可以如何添加?如果不可以,解释为什么?

可以通过编译器看看能否在分类中添加实例变量:

如上图所示,在分类中虽然可以添加属性,但是并不会生成对应的实例变量,也无法手动添加实例变量。编译器提示,实例变量不能放在分类中。那么现在的问题是,为什么 Objective-C 语言设计了分类语法,但是分类中却无法给类添加实例变量?

Objective-C 的分类 (@category) 被设计为一个​​轻量级、无侵入式​​扩展已有类的方式,其首要目标是​​添加方法(包括实例方法和类方法)​​,而不是修改类的实例结构。不允许添加成员变量(实例变量/ivar)是经过深思熟虑的设计选择,主要原因如下:

  1. 保持运行时稳定性和避免内存布局冲突(核心原因):
    • 当你定义一个类时,编译器会为其创建固定的内存布局信息(类结构在底层的定义 objc_class),其中包含了实例变量(ivars)的名称、类型、偏移量等信息。编译后的代码(包括其他可能依赖这个类的代码)以及运行时环境都基于这个布局信息来访问对象的数据(比如 object->ivar)。
    • 如果分类可以添加 ivars,这就意味着​​需要在运行时动态改变类的内存布局​​!想象一下:
      • 程序启动,类 A 被加载,它的内存布局是固定的 (包含 ivar1, ivar2)。
      • 稍后,一个包含新增 ivar3 的分类被加载。运行时必须尝试“扩展”类 A 的内存布局来容纳 ivar3。
      • 所有已经存在的类 A 的实例都需要被“调整”!因为之前创建时并没有预留 ivar3 的空间。这不仅成本极高(遍历所有现有实例并调整大小几乎不可能高效完成),而且在多线程环境下极其脆弱。
      • 更重要的是,任何编译时依赖于原内存布局的代码(如直接使用偏移量访问成员变量的代码——编译器优化后常有)会立即出错,因为 ivar1 和 ivar2 的偏移量很可能因为新增了 ivar3 而改变。
    • 这种对内存布局的动态修改会引入巨大的不稳定性、兼容性问题和性能瓶颈。禁止分类添加 ivars 确保了类一旦被编译和加载,其基结构就是稳定的。
  2. 内存管理和生命周期复杂性:
    • 成员变量是对象的一部分,与对象本身具有相同的生命周期(随对象创建而创建,随对象销毁而销毁)。内存管理也由其所属类(或其父类)负责。
    • 如果分类可以添加 ivars,那么这个变量内存的分配、初始化(构造函数逻辑放在哪?)、析构(析构函数逻辑放在哪?)、内存管理(如 retain/release,ARC 时代自动合成)的职责归属就变得模糊不清。它打破了类本身对其实例数据的单一管理责任原则。特别是当多个分类都试图管理自己的 ivars 时,问题会变得非常混乱。分类本身也不具有像类那样的初始化(+initialize)和析构机制。
  3. 分类的设计哲学:无状态的行为扩展:​​
    • 分类的主要设计目的是​​扩展类的行为(方法)​​,而不是​​修改类或实例的状态(数据)​​。它让你可以:
      • 为系统类或第三方类添加新功能(方法)。
      • 将一个庞大类的实现分拆到多个文件中(模块化管理)。
      • 声明那些本应是私有的方法(在实现文件里声明匿名分类/类扩展 @interface ClassName ())来避免公开声明。
    • 它被设计为一种​​相对安全、隔离的方式​​来添加功能。添加方法通常不会破坏已有的代码或内存布局(重写现有方法或类方法除外,那需要谨慎)。保持分类“无状态”(指不携带自己的数据)是这个安全隔离的关键。
  4. 替代方案:关联对象
    • 虽然不能直接添加 ivars,但 Objective-C 运行时提供了 ​​关联对象(Associated Objects)​​ 机制来模拟给已有类的实例动态添加关联值(不是真正的实例变量!)。
    • 原理:运行时维护一个全局的弱引用表,将键值对(key-value)关联到指定的对象实例上。访问关联对象时,通过这个表和对象实例来查找。
    • 关键区别:
      • 内存位置:​​ Ivars 存储在对象实例内部的内存块中。关联对象存储在运行时管理的独立表中。
      • 访问速度:​​ Ivar 访问是直接的内存偏移访问,非常快。关联对象访问需要查表,相对慢一些(虽然对于绝大多数应用场景可忽略)。
      • 生命周期:​​ Ivars 与对象共存亡。关联对象可以通过策略(objc_AssociationPolicy)来控制其生命周期是随对象保留(OBJC_ASSOCIATION_RETAIN)、复制(OBJC_ASSOCIATION_COPY)还是赋值(OBJC_ASSOCIATION_ASSIGN),并且在对象释放时自动清理(根据策略)。但这需要手动管理策略。
      • 管理责任:​​ Ivars 是类定义的一部分,内存管理责任清晰(由类或父类负责)。关联对象的管理责任在添加分类的代码上。
    • 关联对象的权衡:​​ 关联对象提供了一种灵活性,但也引入了间接访问、潜在的循环引用风险(如果策略选择不当)和轻微的性能开销。语言设计者选择了在分类中添加“成员变量”的责任交给开发者显式地通过关联对象来实现,而不是破坏类实例内存布局的稳定性和简洁性。

Objective-C 限制分类添加成员变量是一个深思熟虑的设计决策,核心目的是:

  • 保证运行时稳定:​​ 防止动态修改已加载类的内存布局而导致崩溃和不可预测行为。
  • ​​明确内存管理责任:​​ 避免与类和对象固有生命周期管理模型冲突。
  • 维护分类的设计初衷:​​ 专注于安全、无状态地扩展行为(方法),而非状态(数据)。分类应该是功能的“插件”,而不是修改类根基的“手术刀”。

Associative References

Associative References 中文直译过来是关联引用,在 Xcode 中,使用快捷键 Command + Shift + O 再输入 objc/runtime.h 就能看到文件中有一个部分就是 Associative References。它包括了三个 API 和一个枚举类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, /**< Specifies an unsafe unretained reference to the associated object. */
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object.
* The association is not made atomically. */
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**< Specifies that the associated object is copied.
* The association is not made atomically. */
OBJC_ASSOCIATION_RETAIN = 01401, /**< Specifies a strong reference to the associated object.
* The association is made atomically. */
OBJC_ASSOCIATION_COPY = 01403 /**< Specifies that the associated object is copied.
* The association is made atomically. */
};

OBJC_EXPORT void
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
id _Nullable value, objc_AssociationPolicy policy)
OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);

OBJC_EXPORT id _Nullable
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);

OBJC_EXPORT void
objc_removeAssociatedObjects(id _Nonnull object)
OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);

可以看到 Associative References 是 iOS 3.1 引入的技术。其中经常使用的只有前面两个 API。objc_removeAssociatedObjects(id _Nonnull object) 在 99.999% 的应用开发场景下都绝不应该调用,它仅推荐在某些极端的场景下谨慎的调用。比如:

  1. 调试与测试:
    • 在编写单元测试时,你需要确保被测试对象在测试结束后没有任何残留状态(包括关联对象),以便下一个测试在一个干净的环境开始。
    • 在调试极其复杂的内存泄漏或对象状态问题时,你可能想观察移除所有关联对象后对问题的影响(但这通常是最后手段)。
  2. 非常特殊的框架或工具:
    • 如果你在开发一个底层调试工具、一个对象序列化/反序列化框架(需要精确控制内存镜像)、或一个对象池实现(需要在对象回收时彻底清除所有外部状态),或许可以考虑在极端谨慎和严格控制上下文中使用它。即使在这里,也需要考虑兼容性问题。
  3. 对第三方对象行为的应急干预(极其危险,不推荐):
    • 遇到一个行为异常、怀疑其内部关联对象状态混乱的第三方对象,且在无其他解决方案时铤而走险尝试用它“重置”对象。这无异于赌博,极易引入新问题且难以维护。

总之应该把这个 API 当作核按钮看待,威力巨大,但按下后的后果通常是灾难性的、不可控的。日常开发中应该使用 objc_setAssociatedObject() 并传入 nil 作为 value 参数来实现移除关联对象。

接着就是如何设置关联对象和获取关联对象了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@interface Person : NSObject
@property (atomic, strong) NSString *name;
@end
@implementation Person
@end

@interface Person (associatedObject)
@property (nonatomic, assign) NSUInteger age;
@end
@implementation Person (associatedObject)
- (void)setAge:(NSUInteger)age {
objc_setAssociatedObject(self, @selector(age), @(age), OBJC_ASSOCIATION_ASSIGN);
}
- (NSUInteger)age {
return [objc_getAssociatedObject(self, @selector(age)) unsignedIntegerValue];
}
@end

int main(int argc, const char *argv[]) {

Person *person1 = [Person new];
person1.name = @"jack";
person1.age = 22;

Person *person2 = [Person new];
person2.name = @"rose";
person2.age = 20;

NSLog(@"%@,%zd", person1.name, person1.age);
NSLog(@"%@,%zd", person2.name, person2.age);

return 0;
}

这里使用 @selector(age) 作为 key 用来存取关联对象。个人认为这种方式是最简洁方便的,当然除了这种方式,还有很多其他取 key 值的方式:

  • static const void *kKeyName = &kKeyName; 这种方式及很多其他变体,都需要额外一个全局变量,不论如何都不如 @selector() 来的方便。
  • 直接使用属性名字符串。这种方式同样也比较简洁,不需要额外的变量。但是需要自己拼写属性名字符串,何不直接使用 @selector() 还有编译器提示。

关联对象的底层实现

objc/runtime.h 的实现在 objc4 库里面,Apple 开源了这个库,可以在 https://github.com/apple-oss-distributions/objc4 下载到。搜索其中的 API 很容易就能找到实现的位置,位于 objc-references.mm 文件中。其实如果熟悉 C++ 语言的话,实现的代码看起来也不难。

关联对象(Associated Object)功能的大致流程和几个核心 C++ 类之间的关系可以归纳如下:

  1. AssociationsManager:全局管理者
    • 职责:管理所有对象的关联数据,确保线程安全
    • 实现机制:
      • 内部通过静态变量 _mapStorage 存储全局唯一的 AssociationsHashMap。
      • 使用自旋锁 spinlock_t AssociationsManagerLock 保证多线程环境下的数据安全,构造函数加锁,析构函数解锁。
  2. AssociationsHashMap:全局存储表
    • 定义:typedef DenseMap<DisguisedPtr, ObjectAssociationMap> AssociationsHashMap;
    • 职责:以对象地址为键,存储每个对象的关联数据表(ObjectAssociationMap)
    • 数据结构:
      • 键:将对象包装为 DisguisedPtr
      • 值:ObjectAssociationMap
  3. ObjectAssociationMap:对象专属关联表
    • 定义:typedef DenseMap<const void *, ObjcAssociation> ObjectAssociationMap;
    • 职责:存储单个对象的所有关联键值对。
    • 数据结构:
      • 键:用户传入的 void *key 参数(例如使用 @selector(propertyName) 或静态变量地址)
      • 值:ObjcAssociation(封装关联值和内存管理策略)
  4. ObjcAssociation:关联值包装器
    • 职责:存储关联值(id _value)和内存管理策略 (uintptr_t _policy)
    • 要点:负责在 objc_setAssociatedObject,objc_getAssociatedObject 中根据 _policy 进行 retain、release、autorelease 等引用计数操作。

工作流程图:

Objective-C runtime 就借助这几个 C++ 类高效、安全地在任意对象上动态「挂载」或「取回」任意类型的关联数据。我知道对于很多不熟悉 C++ 的同学来说,不论说明的再如何详细,其实看起来都还是一脸蒙蔽,于是,我使用 Objective-C 语法实现了类似结构的伪代码帮助理解。

使用 Objective-C 模仿关联对象的底层实现

以下是基于 Objective-C 运行时关联对象底层原理的简化代码模拟,核心还原了四级结构(AssociationsManager → AssociationsHashMap → ObjectAssociationMap → ObjcAssociation)的设计逻辑。代码以伪实现为主,忽略线程安全等细节,重点展示数据结构关联性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// 1. 模拟 ObjcAssociation(封装值和策略)
typedef NS_ENUM(NSUInteger, ObjcAssociationPolicy) {
K_OBJC_ASSOCIATION_ASSIGN,
K_OBJC_ASSOCIATION_RETAIN,
K_OBJC_ASSOCIATION_COPY
};

@interface ObjcAssociation : NSObject
@property (nonatomic, strong) id value;
@property (nonatomic, assign) ObjcAssociationPolicy policy;
@end
@implementation ObjcAssociation
@end

// 2. 模拟 ObjectAssociationMap(对象专属关联表)
// 使用 NSMutableDictionary 存储 key → ObjcAssociation 的映射
typedef NSMutableDictionary<NSValue *, ObjcAssociation *> ObjectAssociationMap;

// 3. 模拟 AssociationsHashMap(全局存储表)
// Key: 对象指针的伪装值(DISGUISE(object)),Value: ObjectAssociationMap
typedef NSMutableDictionary<NSNumber *, ObjectAssociationMap *> AssociationsHashMap;

// 4. 模拟 AssociationsManager(全局管理者)
@interface AssociationsManager : NSObject
+ (AssociationsHashMap *)sharedMap;
@end

@implementation AssociationsManager
+ (AssociationsHashMap *)sharedMap {
static AssociationsHashMap *_globalMap;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_globalMap = [NSMutableDictionary new];
});
return _globalMap;
}
@end

// 5. 关联对象 API 的简化实现
void objc_setAssociatedObject_simulated(id object, const void *key, id value, ObjcAssociationPolicy policy) {
// Step 1: 获取全局 AssociationsHashMap
AssociationsHashMap *globalMap = [AssociationsManager sharedMap];

// Step 2: 生成对象伪装键(模拟 DISGUISE(object))
NSNumber *disguisedKey = @((NSUInteger)(__bridge void *)object);

// Step 3: 查找或创建 ObjectAssociationMap
ObjectAssociationMap *objMap = globalMap[disguisedKey];
if (!objMap) {
objMap = [NSMutableDictionary new];
globalMap[disguisedKey] = objMap;
}

// Step 4: 处理关联值
if (value) {
ObjcAssociation *association = [ObjcAssociation new];
association.value = (policy == OBJC_ASSOCIATION_COPY) ? [value copy] : value;
association.policy = policy;
// 存储关联(key 转换为 NSValue 存储)
objMap[[NSValue valueWithPointer:key]] = association;
} else {
// value=nil 时移除关联
[objMap removeObjectForKey:[NSValue valueWithPointer:key]];
}
}

id objc_getAssociatedObject_simulated(id object, const void *key) {
// 获取全局 AssociationsHashMap
AssociationsHashMap *globalMap = [AssociationsManager sharedMap];
NSNumber *disguisedKey = @((NSUInteger)(__bridge void *)object);
// 获取 object 对应的 ObjectAssociationMap
ObjectAssociationMap *objMap = globalMap[disguisedKey];
// 获取 object 的 key 对应的关联对象
ObjcAssociation *association = objMap[[NSValue valueWithPointer:key]];
return association.value;
}