关联对象实现原理
问题:
Category
能否添加成员变量?如果可以,如何给Category
添加成员变量?
1. 给Category
添加属性
在之前我们通过对Category
底层源码的解析知道了:Category
是不可以添加成员变量的,因为底层的结构体没有存储成员变量列表,但是存储了属性的列表,所以一个Category
是可以添加属性的,但是这个属性不会生成成员变量和set get
方法实现,只会生成set get
方法的声明,需要自己实现set get
方法。
使用RunTime
给系统的类添加属性,首先需要了解对象与属性的关系。我们通过之前的学习知道,对象一开始初始化的时候其属性为nil
,给属性赋值其实就是让属性指向一块存储内容的内存,使这个对象的属性跟这块内存产生一种关联。
那么如果想动态的添加属性,其实就是动态的产生某种关联就好了。而想要给系统的类添加属性,只能通过分类。
赋值是通过set
方法给成员变量赋值,取值是通过get
方法得到成员变量的值:
-(void)setAge:(int)age
{
_age = age;
}
-(int)age
{
return _age;
}
这里给NSObject
添加name
属性,创建NSObject
的分类
@property (nonatomic, strong) NSString *name;
通过Category
的本质我们知道,虽然在分类中可以写@property
添加属性,但是不会自动生成私有属性(成员变量),也不会生成set
,get
方法的实现,只会生成set
,get
的声明,需要我们自己去实现。
1.1 方法一:使用静态全局变量给分类添加属性
static NSString *_name;
@implementation NSObject (Test)
-(void)setName:(NSString *)name
{
_name = name;
}
-(NSString *)name
{
return _name;
}
但是这样_name
静态全局变量与类并没有关联,无论对象创建与销毁,只要程序在运行_name
变量就存在,而且创建多个对象的时候,每个对象的name
不是独立的。应为是全局的变量, 改变一个对象的name
, 其他对象的name
也会被更改, 并不是真正意义上的属性。
1.2 方法二:使用全局字典来给分类添加属性
既然上面的方法对象之前的属性不是独立的,那么我们可以使用一个字典来分别存储对象的属性的值:
NSMutableDictionary *names_;
@implementation NSObject (Test)
+(void)load {
names_ = [NSMutableDictionary dictionary];
}
-(void)setName:(NSString *)name
{
NSString *key = [NSString stringWithFormat:@"%p", self];
names_[key] = name;
}
-(NSString *)name
{
NSString *key = [NSString stringWithFormat:@"%p", self];
return names_[key];
}
一个对象的成员变量是存放在这个对象内部的,但是这种方法和对象没有关系的,因为属性的值是存放在一个全局字典内部的,假如我们还需要给分类添加多个属性,那么又需要添加一个新的全局字典,所以这样的方式也存在问题。
所以想要给一个分类添加属性,应该使用关联对象技术来实现。
2. 使用RunTime
关联对象动态添加属性(间接添加成员变量)
RunTime
提供了动态添加属性和获得属性的方法,可以使得属性和对象产生关联。
虽然可以间接的添加成员变量,但是和原类的成员变量完全是两码事,原类的成员变量就是在一个实例对象内部,而使用关联对象只是实现了分类属性的set get
方法,使分类的属性可以像成员变量那样使用。
2.1 动态添加属性
/**
* 参数1 id object : 给哪个对象添加属性,这里要给自己添加属性,用self
* 参数2 void * == id key : 属性名,根据key获取关联对象的属性的值,在objc_getAssociatedObject中通过次key获得属性的值并返回。
* 参数3 id value : 关联的值,也就是set方法传入的值给属性去保存。
* 参数4 objc_AssociationPolicy policy : 策略,属性以什么形式保存。
*/
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
// 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 // 指定相关的对象被复制,原子性
};
key
值只要是一个指针即可,保证唯一性即可,可以使用的方法:
static const char NSObjectName;
objc_setAssociatedObject(self, &NSObjectName, name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_getAssociatedObject(self, _cmd);
// _cmd就是一个自己的方法实现,在get方法内部相当于@selector(name)
2.2 获得属性
/**
* 参数1 id object : 获取哪个对象里面的关联的属性。
* 参数2 void * == id key : 什么属性,与objc_setAssociatedObject中的key相对应,即通过key值取出value。
*/
objc_getAssociatedObject(id object, const void *key);
2.3 移除所有关联对象
// 移除所有关联对象
objc_removeAssociatedObjects(self);
此时已经成功给NSObjec
t添加name
属性,并且NSObject
对象可以通过点语法为属性赋值:
NSObject *objc = [[NSObject alloc]init];
objc.name = @"qq";
NSLog(@"%@",objc.name);
NSObject *objc1 = [[NSObject alloc]init];
objc1.name = @"nn";
NSLog(@"%@",objc1.name);
NSLog(@"%@",objc.name);
2020-04-29 21:04:38.417962+0800 关联对象实现原理[45634:18454204] qq
2020-04-29 21:04:38.418076+0800 关联对象实现原理[45634:18454204] nn
2020-04-29 21:04:38.418187+0800 关联对象实现原理[45634:18454204] qq
3 关联对象的原理
实现关联对象技术的底层核心对象有:
AssociationsManager
AssociationsHashMap
ObjectAssociationMap
ObjcAssociation
其中Map
同我们平时使用的字典类似。通过key-value
一一对应存值。
通过源码来探寻这些对象的存在形式以及其作用:
设置函数objc_setAssociatedObject
函数
来到runtime
源码,首先找到objc_setAssociatedObject
函数,看一下其实现
objc源码路径:runtime/objc-runtime.mm
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
_object_set_associative_reference(object, (void *)key, value, policy);
}
↓↓↓
_object_set_associative_reference
函数
objc源码路径:runtime/objc-references.mm
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// This code used to work when nil was passed for object and key. Some code
// probably relies on that to not crash. Check and handle it explicitly.
// rdar://problem/44094390
if (!object && !value) return;
assert(object);
if (object->getIsa()->forbidsAssociatedObjects())
_objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));
// retain the new value (if any) outside the lock.
ObjcAssociation old_association(0, nil);
id new_value = value ? acquireValue(value, policy) : nil;
{
// ---------- 核心对象1 ---------- **********
AssociationsManager manager;
// ---------- 核心对象2 AssociationsHashMap ----------
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;
// ---------- 核心对象3 ObjcAssociation ----------
j->second = ObjcAssociation(policy, new_value);
} else {
(*refs)[key] = ObjcAssociation(policy, new_value);
}
} else {
// create the new association (first time).
// ---------- 核心对象4 ObjectAssociationMap ----------
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
函数内部我们可以全部找到我们上面说过的实现关联对象技术的核心对象。接下来我们来一个一个看其内部实现原理探寻他们之间的关系。
3.1 核心对象之间的关联
通过AssociationsManager
内部源码发现,AssociationsManager
内部有一个AssociationsHashMap
对象。
class AssociationsManager {
// 内部有一个AssociationsHashMap对象
static AssociationsHashMap *_map;
public:
AssociationsManager() { AssociationsManagerLock.lock(); }
~AssociationsManager() { AssociationsManagerLock.unlock(); }
AssociationsHashMap &associations() {
if (_map == NULL)
_map = new AssociationsHashMap();
return *_map;
}
};
↓↓↓
我们来看一下AssociationsHashMap
内部的源码。
#if TARGET_OS_WIN32
typedef hash_map<void *, ObjcAssociation> ObjectAssociationMap;
typedef hash_map<disguised_ptr_t, ObjectAssociationMap *> AssociationsHashMap;
#else
typedef ObjcAllocator<std::pair<void * const, ObjcAssociation> > ObjectAssociationMapAllocator;
// ---------- ObjectAssociationMap 与 ObjcAssociation ----------
class ObjectAssociationMap : public std::map<void *, ObjcAssociation, ObjectPointerLess, ObjectAssociationMapAllocator> {
public:
void *operator new(size_t n) { return ::malloc(n); }
void operator delete(void *ptr) { ::free(ptr); }
};
// --------------------------------------------------
typedef ObjcAllocator<std::pair<const disguised_ptr_t, ObjectAssociationMap*> > AssociationsHashMapAllocator;
// ---------- AssociationsHashMap 与 ObjectAssociationMap
class AssociationsHashMap : public unordered_map<disguised_ptr_t, ObjectAssociationMap *, DisguisedPointerHash, DisguisedPointerEqual, AssociationsHashMapAllocator> {
public:
void *operator new(size_t n) { return ::malloc(n); }
void operator delete(void *ptr) { ::free(ptr); }
};
// --------------------------------------------------
#endif
通过AssociationsHashMap
内部源码我们发现AssociationsHashMap
继承自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
中
class ObjcAssociation {
uintptr_t _policy; // 策略
id _value; // 值
public:
ObjcAssociation(uintptr_t policy, id value) : _policy(policy), _value(value) {}
ObjcAssociation() : _policy(0), _value(nil) {}
uintptr_t policy() const { return _policy; }
id value() const { return _value; }
bool hasValue() { return _value != nil; }
};
我们发现ObjcAssociation
存储着_policy
、_value
,而这两个值我们可以发现正是我们调用objc_setAssociatedObject
函数传入的值,也就是说我们在调用objc_setAssociatedObject
函数中传入的value
和policy
这两个值最终是存储在ObjcAssociation
中的。
现在我们已经对AssociationsManager、 AssociationsHashMap、 ObjectAssociationMap、ObjcAssociation
四个对象之间的关系有了简单的认识,那么接下来我们来细读源码,看一下objc_setAssociatedObject
函数中传入的四个参数分别放在哪个对象中充当什么作用。
重新回到_object_set_associative_reference函数实现中:
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// This code used to work when nil was passed for object and key. Some code
// probably relies on that to not crash. Check and handle it explicitly.
// rdar://problem/44094390
if (!object && !value) return;
assert(object);
if (object->getIsa()->forbidsAssociatedObjects())
_objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));
// retain the new value (if any) outside the lock.
ObjcAssociation old_association(0, nil);
// 一. 首先根据我们传入的value经过acquireValue函数处理获取new_value
// new_value是根据不同的策略返回不同的值
id new_value = value ? acquireValue(value, policy) : nil;
{
// 二. 之后创建AssociationsManager manager,以及拿到manager内部的AssociationsHashMap即associations。
// associations是字典,里面存储形式是key-value
// key是object经过转化的disguised_ptr_t类型
// value是另外一个字典ObjectAssociationMap类型,其存储的key是传入的属性,value是一个ObjcAssociation类型
// ObjcAssociation存储了当前的值和策略
// ---------- 核心对象1 ----------
AssociationsManager manager;
// ---------- 核心对象2 AssociationsHashMap ----------
AssociationsHashMap &associations(manager.associations());
// object经过DISGUISE函数被转化为了disguised_ptr_t类型的disguised_object
// DISGUISE函数内部仅仅进行了位运算
disguised_ptr_t disguised_object = DISGUISE(object);
// 成功获取到转化之后的值
if (new_value) {
// 根据转化之后的obj从AssociationsHashMap对象中找到ObjectAssociationMap
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;
// ---------- 核心对象3 ObjcAssociation ----------
// ObjcAssociation对象里面存储值和关联策略
j->second = ObjcAssociation(policy, new_value);
} else {
(*refs)[key] = ObjcAssociation(policy, new_value);
}
} else {
// create the new association (first time).
// ---------- 核心对象4 ObjectAssociationMap ----------
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);
}
细读上述源码我们可以发现,首先根据我们传入的value
经过acquireValue
函数处理获取new_value
。acquireValue
函数内部其实是通过对策略的判断返回不同的值
static id acquireValue(id value, uintptr_t policy) {
switch (policy & 0xFF) {
case OBJC_ASSOCIATION_SETTER_RETAIN:
return objc_retain(value);
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
函数其实仅仅对object
做了位运算
之后我们看到被处理成new_value
的value
,同policy
被存入了ObjcAssociation
中。
而ObjcAssociation
对应我们传入的key
被存入了ObjectAssociationMap
中。
disguised_object
和ObjectAssociationMap
则以key-value
的形式对应存储在associations
中也就是AssociationsHashMap
中。
// ---------- 核心对象4 ObjectAssociationMap ----------
ObjectAssociationMap *refs = new ObjectAssociationMap;
ssociations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
object->setHasAssociatedObjects();
如果我们value
设置为nil
的话那么会执行下面的代码
// 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);
}
}
从上述代码中可以看出,如果我们设置value
为nil
时,就会将关联对象从ObjectAssociationMap
中移除。
最后我们通过一张图可以很清晰的理清楚其中的关系
associationObj_handler通过上图我们可以总结为:一个实例对象object
就对应一个ObjectAssociationMap
,而ObjectAssociationMap
中存储着多个此实例对象的关联对象的key
以及ObjcAssociation
,ObjcAssociation
中存储着关联对象的value
和policy
策略。
由此我们可以知道关联对象并不是放在了原来的对象里面,而是自己维护了一个全局的map
用来存放每一个对象及其对应关联属性表格。
取值objc_getAssociatedObject
函数
objc_getAssociatedObject
内部调用的是_object_get_associative_reference
↓↓↓
id _object_get_associative_reference(id object, void *key) {
id value = nil;
uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
{
// 同样的有AssociationsManager
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object);
// 在哈希表中查找disguised_object
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
// 如果存在,取出ObjectAssociationMap
// 并在ObjectAssociationMap中查找属性key对应的valued,即ObjcAssociation对象
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
ObjcAssociation &entry = j->second;
value = entry.value();
policy = entry.policy();
// 将value和policy取出
if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) {
objc_retain(value);
}
}
}
}
if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
objc_autorelease(value);
}
return value;
}
从_object_get_associative_reference
函数内部可以看出,像set
方法中那样,反向将value
一层一层取出最后return
出去。
移除objc_removeAssociatedObjects
函数
objc_removeAssociatedObjects
用来删除所有的关联对象,objc_removeAssociatedObjects
函数内部调用的是_object_remove_assocations
函数
objc源码路径:runtime/objc-runtime.mm
void objc_removeAssociatedObjects(id object)
{
if (object && object->hasAssociatedObjects()) {
_object_remove_assocations(object);
}
}
↓↓↓
objc源码路径:runtime/objc-references.mm
void _object_remove_assocations(id object) {
vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > elements;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
if (associations.size() == 0) return;
disguised_ptr_t disguised_object = DISGUISE(object);
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// copy all of the associations that need to be removed.
ObjectAssociationMap *refs = i->second;
// 遍历所有的ObjectAssociationMap
for (ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j != end; ++j) {
elements.push_back(j->second);
}
// 删除操作
delete refs;
associations.erase(i);
}
}
// the calls to releaseValue() happen outside of the lock.
for_each(elements.begin(), elements.end(), ReleaseValue());
}
上述源码可以看出_object_remove_assocations
函数将object
对象向对应的所有关联对象全部删除。
4 总结
关联对象并不是存储在被关联对象本身内存中,而是存储在全局的统一的一个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
了。