本文已经添加到专辑:《彻底弄懂OC》。 欢迎加入我的QQ群:661461410
,一起探讨iOS底层原理。
我们知道在一个类中增加一个属性,编译器会帮我们做3件事,比如,我们给Person
这个类增加一个属性age
,编译之后,类中会增加一个成员变量_age
, 增加get方法和set方法的生命与实现 -(int)age
和 -(void)setAge:(int)age
。
但是我们在分类里面声明一个属性,会帮我们声明两个方法set, get。 但不会生成实现,也不会产生成员变量。所以我们不能直接给Category添加成员变量,但是可以间接实现Category有成员变量的效果。
下面代码中,我们为person的分类增加了一个age属性,我们通过声明一个全局变量_age,将age的值存储到_age中。
@interface Person (Test) @property (nonatomic, assign) int age; @end @implementation Person (Test) int _age; - (void)setAge:(int)age { _age = age; } -(int)age { return _age; } @end 复制代码
但,这种方式有致命的确定,就是多个实例对象共用_age,导致数据错误。
#import "Person+Test.h" @implementation Person (Test) NSMutableDictionary *_ageDic; + (void)load { _ageDic = [[NSMutableDictionary alloc] init]; } - (int)age { NSString *selfKey = [NSString stringWithFormat:@"%p", self]; return [[_ageDic valueForKey: selfKey] intValue]; } - (void)setAge:(int)age { NSString *selfKey = [NSString stringWithFormat:@"%p", self]; [_ageDic setValue:@(age) forKey:selfKey]; } @end 复制代码
使用字典来存储值解决上一种方法中多个对象导致数据错乱的问题,但它也有一些问题,如下:
#import "Person+Test.h" #import <objc/runtime.h> @implementation Person (Test) - (int)age { return [objc_getAssociatedObject(self, @selector(age)) intValue]; } - (void)setAge:(int)age { objc_setAssociatedObject(self, @selector(age), @(age), OBJC_ASSOCIATION_ASSIGN); } @end 复制代码
这种方式,我们使用runtime提供的两个方法objc_setAssociatedObject
和 objc_setAssociatedObject
来分别设置和获取属性值。就objc_setAssociatedObject
而言其接受三个参数:
id,表示为当前对象关联属性。
key,关联属性对应的唯一key,这里我们使用get方法的函数地址值。
value,key对应的值。
objc_AssociationPolicy, 存储策略。有5个取值,根据属性的类型选择对应的内存存储策略。
OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403
关联对象的底层实现涉及到了4个类,分别是:
其实现结构如下:
其中 AssociationsHashMap
中 disguised_ptr_t
表示对象,AssociationMap
中的 void *
表示 key
。ObjectAssociation
中包含value
和存储策略。
我们以上面的Person对象为例,图示一下整体的结构。
通过上面的分析,我们回答一下开头的问题:
分类不可以直接添加属性,但可以间接添加,最优雅的方式是通过关联对象进行属性与分类的绑定。
那么,留一个问题,你认为关联对象需要手动释放吗? 在类销毁的时候,需要释放其通过关联对象绑定的属性吗?