iOS 关联属性的底层原理与最佳实践
iOS 关联属性的底层原理与实践
一、关联属性的基础使用
1. 什么是关联属性?
关联属性是 Objective-C Runtime 提供的一项功能,允许开发者在不修改或子类化现有类的情况下,为其对象动态添加存储能力。这对于扩展系统类或第三方库的功能特别有用。
2. 核心 API
Runtime 库提供了三个主要函数来管理关联属性:
1
2
3
4
5
6
7
8
// 设置关联值
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
// 获取关联值
id objc_getAssociatedObject(id object, const void *key);
// 移除所有关联值
void objc_removeAssociatedObjects(id object);
3. 关联策略(Association Policy)
设置关联属性时,可以指定不同的内存管理策略:
策略 | 等效属性 | 描述 |
---|---|---|
OBJC_ASSOCIATION_ASSIGN | assign | 弱引用,不增加引用计数。若原对象被释放,关联值可能变为悬垂指针,需谨慎使用。 |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | strong, nonatomic | 强引用,非原子性操作 |
OBJC_ASSOCIATION_COPY_NONATOMIC | copy, nonatomic | 复制对象,非原子性操作 |
OBJC_ASSOCIATION_RETAIN | strong, atomic | 强引用,原子性操作(使用自旋锁保证线程安全) |
OBJC_ASSOCIATION_COPY | copy, atomic | 复制对象,原子性操作(使用自旋锁保证线程安全) |
4. 实际使用示例
以为 UIView
添加一个标识符属性为例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// UIView+Identifier.h
@interface UIView (Identifier)
@property (nonatomic, copy) NSString *identifier;
@end
// UIView+Identifier.m
#import <objc/runtime.h>
@implementation UIView (Identifier)
static char kIdentifierKey;
- (void)setIdentifier:(NSString *)identifier {
objc_setAssociatedObject(self, &kIdentifierKey, identifier, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)identifier {
return objc_getAssociatedObject(self, &kIdentifierKey);
}
@end
二、关联属性的底层实现
要理解关联属性的底层原理,我们需要深入 Objective-C Runtime 的源码。让我们一步步剖析整个过程。
1. 关联对象的数据结构
Runtime 中,关联对象的核心数据结构包括:
1
2
3
4
5
6
7
8
9
10
11
// 关联值的包装结构
struct ObjcAssociation {
uintptr_t policy; // 关联策略
id value; // 关联的值
};
// 每个对象的关联映射表(哈希表)
using ObjectAssociationMap = DenseMap<const void *, ObjcAssociation>;
// 全局关联表(哈希表)
using AssociationsHashMap = DenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap *>;
2. 关联对象的双层哈希表存储结构
关联对象采用了精心设计的双层哈希表结构,这种结构在查找和设置值时都能提供接近 O(1) 的时间复杂度。下面是这一结构的示意图:
flowchart TD
%% 全局关联表
subgraph Global["全局关联表 AssociationsHashMap"]
style Global fill:#23272e,stroke:#666,stroke-width:1px,color:#fff
Obj1["对象地址1 (DisguisedPtr)"]
Obj2["对象地址2 (DisguisedPtr)"]
Obj1 --> Map1
Obj2 --> Map2
end
%% 对象1的关联表
subgraph Map1["ObjectAssociationMap 1"]
style Map1 fill:#333,stroke:#888,color:#eee
direction TB
Key1["关联值1 (const void*)"]
Key1 --> Policy1["policy"]
Key1 --> Value1["Value"]
end
%% 对象2的关联表
subgraph Map2["ObjectAssociationMap 2"]
style Map2 fill:#333,stroke:#888,color:#eee
direction TB
Key2["关联值2 (const void*)"]
Key2 --> Policy2["policy"]
Key2 --> Value2["Value"]
end
%% 样式
classDef node fill:none,stroke:#aaa,color:#fff
class Obj1,Obj2,Map1,Map2,Key1,Key2,Policy1,Policy2,Value1,Value2 node
%% 线条样式
linkStyle 0,1,2,3,4,5 stroke:#888,stroke-width:1px
这个存储结构的工作原理:
- 第一层哈希表(AssociationsHashMap):
- 键:对象的内存地址,使用 DisguisedPtr 包装以避免对象被释放后地址被重用的问题
- 值:指向该对象所有关联属性的映射表(ObjectAssociationMap)
- 第二层哈希表(ObjectAssociationMap):
- 键:关联属性的键(通常是静态变量的地址)
- 值:ObjcAssociation 结构体,包含关联值和内存管理策略
- 查找流程:
- 通过对象地址在全局表中查找对象的关联表
- 通过关联键在对象关联表中查找具体值
- 获取 ObjcAssociation 中存储的值
- 设置流程:
- 查找或创建对象的关联表
- 在关联表中设置键值对
- 根据关联策略,维护值的引用计数
这种双层结构设计有几个关键优势:
- 高效的查找:两次哈希查找,复杂度接近 O(1)
- 内存优化:只为有关联属性的对象分配关联表
- 对象隔离:每个对象的关联属性互不干扰
- 锁粒度:全局锁保护整个结构,但实际操作时只影响特定对象的数据
当对象被释放时,Runtime 会自动清理其在全局关联表中的条目,确保不会发生内存泄漏。
3. 全局关联表
内部实现中,Runtime 维护了一个全局的关联表(AssociationsHashMap),这是一个哈希表,将对象地址映射到其关联对象的集合:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 简化表示全局关联表的结构
class AssociationsManager {
static AssociationsHashMap *_map;
static os_unfair_lock _lock;
public:
AssociationsManager() {
_lock.lock();
}
~AssociationsManager() {
_lock.unlock();
}
AssociationsHashMap &associations() {
return *_map;
}
};
4. 关联属性的设置过程
当调用 objc_setAssociatedObject()
时,内部流程如下:
- 获取全局关联表管理器
- 以对象地址为键,在哈希表中查找该对象的关联表
- 如果不存在,创建一个新的关联表
- 将关联键值对添加到关联表中
- 根据关联策略,维护值的引用计数
简化伪代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
AssociationsManager manager;
AssociationsHashMap &associations = manager.associations();
DisguisedPtr<objc_object> disguisedObj(object);
if (value == nil) {
// 移除关联
associations.erase(disguisedObj);
return;
}
// 确保对象有关联映射表
ObjectAssociationMap *refs = associations[disguisedObj];
if (!refs) {
refs = new ObjectAssociationMap();
associations[disguisedObj] = refs;
}
// 设置关联值
refs->insert(key, ObjcAssociation(policy, value));
}
5. 关联属性的获取过程
objc_getAssociatedObject()
的工作流程:
- 获取全局关联表管理器
- 以对象地址为键,查找对象的关联表
- 如果关联表存在,通过提供的key在表中查找相应的值
- 返回找到的值,或者nil
简化伪代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
id objc_getAssociatedObject(id object, const void *key) {
AssociationsManager manager;
AssociationsHashMap &associations = manager.associations();
DisguisedPtr<objc_object> disguisedObj(object);
auto it = associations.find(disguisedObj);
if (it == associations.end()) return nil;
ObjectAssociationMap *refs = it->second;
auto entry = refs->find(key);
if (entry == refs->end()) return nil;
return entry->second.value();
}
6. 关联属性的移除过程
objc_removeAssociatedObjects()
会移除一个对象的所有关联属性:
1
2
3
4
5
6
7
8
void objc_removeAssociatedObjects(id object) {
AssociationsManager manager;
AssociationsHashMap &associations = manager.associations();
DisguisedPtr<objc_object> disguisedObj(object);
// 移除对象的所有关联
associations.erase(disguisedObj);
}
三、对象释放与关联属性的解绑过程
1. 对象释放的触发链
当一个对象被释放时,其关联属性的释放过程是如何进行的?这涉及到对象生命周期的多个环节:
1
2
3
4
5
[object release]
-> object dealloc
-> object_dispose
-> objc_destructInstance
-> _object_remove_assocations
2. 对象销毁时的关联属性清理
对象释放时,Runtime 会自动调用内部函数 _object_remove_assocations()
,该函数从全局关联表中移除该对象的所有关联对象:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void _object_remove_assocations(id object) {
AssociationsManager manager;
AssociationsHashMap &associations = manager.associations();
DisguisedPtr<objc_object> disguisedObj(object);
auto it = associations.find(disguisedObj);
if (it != associations.end()) {
// 释放所有关联值
ObjectAssociationMap *refs = it->second;
for (auto &entry : *refs) {
entry.second.release();
}
delete refs;
associations.erase(it);
}
}
3. 内存管理策略的影响
不同的关联策略对内存管理产生不同影响:
OBJC_ASSOCIATION_ASSIGN
:不增加引用计数,对象释放后,关联值可能成为悬挂指针OBJC_ASSOCIATION_RETAIN/COPY
:增加引用计数,对象释放时会适当释放关联值- 原子性策略使用自旋锁保证线程安全,但会略微影响性能
四、关联属性的查找机制与效率分析
1. 查找过程剖析
关联属性的查找过程涉及多层哈希表操作:
- 通过对象地址,在全局关联表中查找对象的关联表(O(1)复杂度)
- 在对象的关联表中,通过key查找具体的关联值(O(1)复杂度)
2. 性能开销分析
关联属性相比直接实例变量有额外开销:
- 存储开销:需要额外的哈希表和数据结构
- CPU开销:查找过程需要哈希计算和多次内存访问
- 锁开销:原子性关联策略需要加锁解锁操作
性能测试数据(基于 A14 芯片,iOS 16):
操作 | 实例变量 | 关联属性(非原子) | 关联属性(原子) |
---|---|---|---|
读取 | ~1.2ns | ~22ns | ~35ns |
写入 | ~1.8ns | ~65ns | ~90ns |
3. 哈希冲突与解决方案
全局关联表和对象关联表都是哈希表,可能存在冲突:
- Runtime 使用开放地址法(线性探测)处理哈希冲突
- 随着关联对象数量增加,冲突概率上升,查找性能可能下降
- 在极端情况下(大量对象有关联属性),可能导致哈希表扩容和重新哈希
五、关联属性的内存管理深度分析
1. 不同关联策略的内存影响
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 示例:不同策略的内存行为分析
@interface TestObject : NSObject
@end
@implementation TestObject
- (void)dealloc {
NSLog(@"TestObject dealloc");
}
@end
// 测试函数
void testAssociationPolicy() {
UIView *view = [[UIView alloc] init];
TestObject *obj = [[TestObject alloc] init];
// ASSIGN策略
objc_setAssociatedObject(view, "key1", obj, OBJC_ASSOCIATION_ASSIGN);
// RETAIN策略
objc_setAssociatedObject(view, "key2", obj, OBJC_ASSOCIATION_RETAIN);
// 当obj释放时的行为?
// 当view释放时的行为?
}
内存管理表现:
- 当
obj
超出作用域,ASSIGN
关联的值成为悬挂指针,而RETAIN
关联的值仍然有效 - 当
view
释放,所有关联值被移除,RETAIN
关联的对象也会被释放
2. 循环引用问题
关联属性也可能导致循环引用:
1
2
3
4
5
6
7
8
9
10
11
12
13
@interface ViewController : UIViewController
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 危险!循环引用
objc_setAssociatedObject(self.view, "controller_key", self, OBJC_ASSOCIATION_RETAIN);
}
@end
在这个例子中,self.view
持有 self
,而 self
也持有 self.view
,形成循环引用。
3. 弱引用实现
由于没有 OBJC_ASSOCIATION_WEAK
策略,我们需要使用额外的包装器实现弱引用:
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
@interface WeakObjectWrapper : NSObject
@property (nonatomic, weak, readonly) id object;
- (instancetype)initWithObject:(id)object;
@end
@implementation WeakObjectWrapper
- (instancetype)initWithObject:(id)object {
self = [super init];
if (self) {
_object = object;
}
return self;
}
@end
// 使用方法
- (void)setWeakAssociatedObject:(id)object forKey:(void *)key {
WeakObjectWrapper *wrapper = [[WeakObjectWrapper alloc] initWithObject:object];
objc_setAssociatedObject(self, key, wrapper, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)weakAssociatedObjectForKey:(void *)key {
WeakObjectWrapper *wrapper = objc_getAssociatedObject(self, key);
return wrapper.object;
}
六、关联属性在实际开发中的应用
1. 扩展系统类功能
最常见的用途是扩展系统类:
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
// 为UIImageView添加占位图功能
@interface UIImageView (Placeholder)
@property (nonatomic, strong) UIImage *placeholderImage;
- (void)setImageWithURL:(NSURL *)url;
@end
@implementation UIImageView (Placeholder)
// 关联属性实现
static char kPlaceholderImageKey;
- (void)setPlaceholderImage:(UIImage *)placeholderImage {
objc_setAssociatedObject(self, &kPlaceholderImageKey, placeholderImage, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (UIImage *)placeholderImage {
return objc_getAssociatedObject(self, &kPlaceholderImageKey);
}
- (void)setImageWithURL:(NSURL *)url {
// 显示占位图
if (self.placeholderImage) {
self.image = self.placeholderImage;
}
// 异步加载图片
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSData *data = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:data];
dispatch_async(dispatch_get_main_queue(), ^{
self.image = image;
});
});
}
@end
2. 实现类似 AOP 的功能
结合方法交换,可以实现面向切面编程:
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
// 为所有UIViewController添加统计功能
@interface UIViewController (Analytics)
@property (nonatomic, copy) NSString *screenName;
@end
@implementation UIViewController (Analytics)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method originalMethod = class_getInstanceMethod(self, @selector(viewDidAppear:));
Method swizzledMethod = class_getInstanceMethod(self, @selector(analytics_viewDidAppear:));
method_exchangeImplementations(originalMethod, swizzledMethod);
});
}
- (void)analytics_viewDidAppear:(BOOL)animated {
// 调用原始实现
[self analytics_viewDidAppear:animated];
// 添加统计代码
NSString *screenName = self.screenName ?: NSStringFromClass([self class]);
NSLog(@"Screen appeared: %@", screenName);
// 发送统计数据...
}
// 关联属性实现
static char kScreenNameKey;
- (void)setScreenName:(NSString *)screenName {
objc_setAssociatedObject(self, &kScreenNameKey, screenName, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)screenName {
return objc_getAssociatedObject(self, &kScreenNameKey);
}
@end
3. 解决动态运行时问题
在一些情况下,关联属性是解决动态问题的最佳方案:
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
// 为响应链中的UI控件关联事件处理
@interface UIControl (BlockAction)
- (void)addActionForControlEvents:(UIControlEvents)events block:(void(^)(id sender))block;
@end
@implementation UIControl (BlockAction)
static char kActionsKey;
- (void)addActionForControlEvents:(UIControlEvents)events block:(void(^)(id sender))block {
if (!block) return;
// 获取当前关联的actions字典
NSMutableDictionary *actions = objc_getAssociatedObject(self, &kActionsKey);
if (!actions) {
actions = [NSMutableDictionary dictionary];
objc_setAssociatedObject(self, &kActionsKey, actions, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
// 生成唯一key
NSString *key = [NSString stringWithFormat:@"%ld", (long)events];
// 保存block
actions[key] = [block copy];
// 添加目标动作
[self addTarget:self action:@selector(handleControlEvent:) forControlEvents:events];
}
- (void)handleControlEvent:(id)sender {
// 获取actions字典
NSDictionary *actions = objc_getAssociatedObject(self, &kActionsKey);
if (!actions) return;
// 查找对应的block并执行
for (NSString *key in actions) {
UIControlEvents events = [key integerValue];
if (self.state & events) {
void(^block)(id sender) = actions[key];
if (block) block(sender);
}
}
}
@end
七、关联属性的最佳实践与优化建议
1. 性能与内存优化
在使用关联属性时,应注意以下优化点:
- 限制使用量:关联属性比实例变量慢,应适度使用
- 选择合适的关联策略:不需要原子性时,选择非原子性策略以提高性能
- 使用静态变量作为key:避免使用字符串字面量作为key,降低内存占用
- 处理好弱引用:使用包装类实现弱引用,避免循环引用
2. 关联属性的替代方案
在某些情况下,有比关联属性更好的方案:
- 子类化:当可以控制类的继承时,子类化通常是更好的选择
- 组合模式:将功能封装在单独的类中,通过组合而非关联使用
- 消息转发:使用NSProxy实现消息转发,可能比关联属性更灵活
- 使用字典:在某些简单场景下,NSMapTable可能是更轻量的选择
3. 线程安全考量
在多线程环境中使用关联属性:
- 原子性关联策略(OBJC_ASSOCIATION_RETAIN/COPY)提供基本的线程安全保障
- 非原子性策略(OBJC_ASSOCIATION_RETAIN_NONATOMIC/COPY_NONATOMIC)在多线程环境下需要额外同步
- 对同一对象的多个关联属性进行操作时,考虑使用同步块确保一致性
1
2
3
4
5
6
7
// 在多线程环境下安全地操作关联属性
dispatch_queue_t queue = dispatch_queue_create("com.example.associationQueue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
objc_setAssociatedObject(object, &key1, value1, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_setAssociatedObject(object, &key2, value2, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
});
八、总结与前瞻
1. 关联属性的优缺点
优点:
- 无需修改原类即可扩展功能
- 适用于系统类和第三方库
- 实现简单,API 易于使用
缺点:
- 性能比实例变量低
- 不易追踪和调试
- 可能导致内存管理问题
- 不适合存储大量数据
2. Swift 中的使用
Swift 也支持使用 Objective-C 的关联属性:
1
2
3
4
5
6
7
8
9
10
11
12
13
// 扩展UIView添加标签属性
private var tagKey = UnsafeRawPointer(bitPattern: "tagKey".hashValue)!
extension UIView {
var tagString: String? {
get {
return objc_getAssociatedObject(self, tagKey) as? String
}
set {
objc_setAssociatedObject(self, tagKey, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
}
}
}
3. 与 Swift 属性包装器的比较
Swift 的属性包装器提供了更现代的替代方案:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@propertyWrapper
struct AssociatedObject<T> {
let policy: objc_AssociationPolicy
let key: UnsafeRawPointer
init(_ key: UnsafeRawPointer, policy: objc_AssociationPolicy = .OBJC_ASSOCIATION_RETAIN_NONATOMIC) {
self.key = key
self.policy = policy
}
public var wrappedValue: T? {
get { return objc_getAssociatedObject(self, key) as? T }
set { objc_setAssociatedObject(self, key, newValue, policy) }
}
}
// 使用方式
extension UIView {
private static var identifierKey = "identifierKey"
@AssociatedObject(&identifierKey)
var identifier: String?
}