前言
Category能否添加成员变量?如果可以,如何给Category添加成员变量?
答:不能直接添加成员变量,但是可以通过runtime的方式间接实现添加成员变量的效果。
Category动态关联对象
方法一:我们可以通过使用静态全局变量给分类添加属性
static NSString *_name;
-(void)setName:(NSString *)name
{
_name = name;
}
-(NSString *)name
{
return _name;
}
但是这样_name静态全局变量与类并没有关联,无论对象创建与销毁,只要程序在运行_name变量就存在,并不是真正意义上的属性。
方法二:使用runtime动态添加属性
-(void)setName:(NSString *)name
{
objc_setAssociatedObject(self, @"name",name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(NSString *)name
{
return objc_getAssociatedObject(self, @"name");
}
关联对象的实现原理
实现关联对象技术的核心对象有
AssociationsManager
AssociationsHashMap
ObjectAssociationMap
ObjcAssociation
其中Map同我们平时使用的字典类似。通过key-value一一对应存值。
本文基于objc-723
版本,在Apple Github和Apple OpenSource上有源码,来到runtime源码,首先找到objc_setAssociatedObject函数,看一下其实现
objc_setAssociatedObject函数
void
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
objc_setAssociatedObject_non_gc(object, key, value, policy);
}
void objc_setAssociatedObject_non_gc(id object, const void *key, id value, objc_AssociationPolicy policy) {
_object_set_associative_reference(object, (void *)key, value, policy);
}
我们看到其实内部调用的是_object_set_associative_reference函数,我们来到_object_set_associative_reference函数中
_object_set_associative_reference函数
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// retain the new value (if any) outside the lock.
ObjcAssociation old_association(0, nil);
id new_value = value ? acquireValue(value, policy) : nil;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
// break any existing association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// secondary table exists
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
j->second = ObjcAssociation(policy, new_value);
} else {
(*refs)[key] = ObjcAssociation(policy, new_value);
}
} else {
// create the new association (first time).
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
object->setHasAssociatedObjects();
}
} else {
// setting the association to nil breaks the association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
refs->erase(j);
}
}
}
}
// release the old value (outside of the lock).
if (old_association.hasValue()) ReleaseValue()(old_association);
}
_object_set_associative_reference函数内部我们可以全部找到我们上面说过的实现关联对象技术的核心对象。接下来我们来一个一个看其内部实现原理探寻他们之间的关系。
AssociationsManager
class AssociationsManager {
static spinlock_t _lock;
static AssociationsHashMap *_map; // associative references: object pointer -> PtrPtrHashMap.
public:
AssociationsManager() { _lock.lock(); }
~AssociationsManager() { _lock.unlock(); }
AssociationsHashMap &associations() {
if (_map == NULL)
_map = new AssociationsHashMap();
return *_map;
}
};
通过AssociationsManager内部源码发现,AssociationsManager内部有一个AssociationsHashMap对象,还有个自旋锁 spinlock_t。
AssociationsHashMap
通过AssociationsHashMap内部源码我们发现AssociationsHashMap继承自unordered_map首先来看一下unordered_map内的源码
unordered_map内部分源码
从unordered_map源码中我们可以看出_Key和_Tp也就是前两个参数对应着map中的Key和Value,那么对照上面AssociationsHashMap内源码发现_Key中传入的是disguised_ptr_t,_Tp中传入的值则为ObjectAssociationMap。
紧接着我们来到ObjectAssociationMap中,上图中ObjectAssociationMap已经标记出,我们发现ObjectAssociationMap中同样以key、Value的方式存储着ObjcAssociation。
接着我们来到ObjcAssociation中
ObjcAssociation
我们发现ObjcAssociation存储着_policy、_value,而这两个值我们可以发现正是我们调用objc_setAssociatedObject函数传入的值,也就是说我们在调用objc_setAssociatedObject函数中传入的value和policy这两个值最终是存储在ObjcAssociation中的。
现在我们已经对AssociationsManager、 AssociationsHashMap、 ObjectAssociationMap、ObjcAssociation四个对象之间的关系有了简单的认识,那么接下来我们来细读源码,看一下objc_setAssociatedObject函数中传入的四个参数分别放在哪个对象中充当什么作用。
细读上述源码我们可以发现,首先根据我们传入的value经过acquireValue函数处理获取new_value。acquireValue函数内部其实是通过对策略的判断返回不同的值
static id acquireValue(id value, uintptr_t policy) {
switch (policy & 0xFF) {
case OBJC_ASSOCIATION_SETTER_RETAIN:
return ((id(*)(id, SEL))objc_msgSend)(value, SEL_retain);
case OBJC_ASSOCIATION_SETTER_COPY:
return ((id(*)(id, SEL))objc_msgSend)(value, SEL_copy);
}
return value;
}
之后创建AssociationsManager manager;以及拿到manager内部的AssociationsHashMap即associations。
之后我们看到了我们传入的第一个参数object
object经过 DISGUISE 函数被转化为了disguised_ptr_t类型的disguised_object。
typedef uintptr_t disguised_ptr_t;
inline disguised_ptr_t DISGUISE(id value) { return ~uintptr_t(value); }
inline id UNDISGUISE(disguised_ptr_t dptr) { return id(~dptr); }
DISGUISE函数
DISGUISE函数其实仅仅对object做了位运算
- 我们看到被处理成new_value的value,同policy被存入了ObjcAssociation中。
- 而ObjcAssociation对应我们传入的key被存入了ObjectAssociationMap中。
- disguised_object和ObjectAssociationMap则以key-value的形式对应存储在associations中也就是AssociationsHashMap中。
value为nil
从上述代码中可以看出,如果我们value设置为nil的话那么会执行下面的代码,就会将关联对象从ObjectAssociationMap中移除。
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
refs->erase(j);
}
}
最后我们通过一张图可以很清晰的理清楚其中的关系
关联对象底层对象关系
通过上图我们可以总结为:
一个实例对象就对应一个ObjectAssociationMap,而ObjectAssociationMap中存储着多个此实例对象的关联对象的key以及ObjcAssociation,为ObjcAssociation中存储着关联对象的value和policy策略。
由此我们可以知道关联对象并不是放在了原来的对象里面,而是自己维护了一个全局的map用来存放每一个对象及其对应关联属性表格。
objc_getAssociatedObject函数
objc_getAssociatedObject内部调用的是_object_get_associative_reference
id objc_getAssociatedObject(id object, const void *key) {
return objc_getAssociatedObject_non_gc(object, key);
}
id objc_getAssociatedObject_non_gc(id object, const void *key) {
return _object_get_associative_reference(object, (void *)key);
}
_object_get_associative_reference函数
从_object_get_associative_reference函数内部可以看出,向set方法中那样,反向将value一层一层取出最后return出去。
id _object_get_associative_reference(id object, void *key) {
id value = nil;
uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object);
> 1. //Hash表中查找disguised_object
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
> 2. //找到的话根据key取出对应的ObjcAssociation
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
ObjcAssociation &entry = j->second;
value = entry.value();
policy = entry.policy();
> 3. //根据policy对取出的value做相应的操作
if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) ((id(*)(id, SEL))objc_msgSend)(value, SEL_retain);
}
}
}
if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
((id(*)(id, SEL))objc_msgSend)(value, SEL_autorelease);
}
return value;
}
objc_removeAssociatedObjects函数
objc_removeAssociatedObjects用来删除所有的关联对象,objc_removeAssociatedObjects函数内部调用的是_object_remove_assocations函数
void objc_removeAssociatedObjects(id object)
{
#if SUPPORT_GC
if (UseGC) {
auto_zone_erase_associative_refs(gc_zone, object);
} else
#endif
{
if (object && object->hasAssociatedObjects()) {
_object_remove_assocations(object);
}
}
}
objc_removeAssociatedObjects函数
总结:
关联对象并不是存储在被关联对象本身内存中,而是存储在全局的统一的一个AssociationsManager中,如果设置关联对象为nil,就相当于是移除关联对象。
我们现在来看objc_AssociationPolicy
policy 参数: 属性以什么形式保存的策略。
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, // 指定一个弱引用相关联的对象
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // 指定相关对象的强引用,非原子性
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, // 指定相关的对象被复制,非原子性
OBJC_ASSOCIATION_RETAIN = 01401, // 指定相关对象的强引用,原子性
OBJC_ASSOCIATION_COPY = 01403 // 指定相关的对象被复制,原子性
};
我们会发现其中只有RETAIN和COPY而为什么没有weak呢?
总过上面对源码的分析我们知道,object经过DISGUISE函数被转化为了disguised_ptr_t类型的disguised_object。
disguised_ptr_t disguised_object = DISGUISE(object);
weak修饰的属性,当没有拥有对象之后就会被销毁,并且指针置位nil,那么在对象销毁之后,虽然在map中既然存在值object对应的AssociationsHashMap,但是因为object地址已经被置位nil,会造成坏地址访问而无法根据object对象的地址转化为disguised_object了。
使用 OBJC_ASSOCIATION_ASSIGN
策略其实等于 assign/unsafe_unretained
,本质上是保存了对象的地址而不是真正的弱引用,在一些情定的情况下 属性对象释放时再调用方法会出现野指针异常
更安全的 association weak 属性
从 DZNEmptyDataSet 中可以学习使用 strong + WeakContainer 的方式,实现对属性对象的 weak 引用。
思路是:
- 声明一个 WeakContainer 类对真实的属性对象进行 weak 属性引用
- 通过
OBJC_ASSOCIATION_RETAIN_NONATOMIC
策略对 WeakContainer 进行 retain association - 这样在 get 关联属性对象时由于 WeakContainer 对真是属性对象的 weak 引用,会返回 nil 而不是野指针
实现 WeakContainer 如下:
@interface XWeakObjectContainer : NSObject
@property (nonatomic, readonly, weak) id weakObject;
- (instancetype)initWithWeakObject:(id)object;
@end
@implementation XWeakObjectContainer
- (instancetype)initWithWeakObject:(id)object {
self = [super init];
if (self) {
_weakObject = object;
}
return self;
}
@end
从而变相地实现 weak association 如下:
@implementation NSObject (XAssociate)
- (void)setXProperty:(id)xProperty {
XWeakObjectContainer *container = [[XWeakObjectContainer alloc] initWithWeakObject:xProperty];
objc_setAssociatedObject(self, @selector(xProperty), container, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)xProperty {
XWeakObjectContainer *container = objc_getAssociatedObject(self, _cmd);
return container.weakObject;
}
@end
虽然 retain 了一个 WeakContainer,但是 WeakContainer 最终会随着属性的持有对象一起销毁,不存在泄露。