1、关联对象
1.1、使用场景
默认情况下,由于分类底层结构的限制,不能直接给 Category 添加成员变量,但是可以通过关联对象间接实现 Category 有成员变量的效果。
1.2、使用方法
#import "Person.h"
@interface Person (Test)
@property (nonatomic, assign) int height;
@end
#import "Person+Test.h"
#import <objc/runtime.h>
@implementation Person (Test)
- (void)setHeight:(int)height
{
objc_setAssociatedObject(self, @selector(height), [NSNumber numberWithInt:height], OBJC_ASSOCIATION_ASSIGN);
}
- (int)height
{
return [objc_getAssociatedObject(self, @selector(height)) intValue];
}
@end
2、类与分类添加成员变量的区别
2.1、普通的类添加成员变量
// 定义 一个 Person 类
@interface Person : NSObject
@property (nonatomic, assign) int age;
@end
@implementation Person
@end
在 Person 类中,使用 @property (nonatomic, assign) int age
创建一个变量的话, 系统会默认为我们做三件事
1、生成 _name 成员变量
{
int _age;
}
2、生成 get/set 方法的声明
- (void)setAge:(int)age;
- (int)age;
3、生成 get/set 方法的实现
- (void)setAge:(int)age {
_age = age;
}
- (int)age {
return _age;
}
2.2、 分类中添加成员变量
// 定义 一个 Person+Test 分类
@interface Person (Test)
@property (nonatomic, assign) int weight;
@end
@implementation Person (Test)
@end
在 Person+Test 这个分类中,使用@property (nonatomic, assign) int weight;
创建一个变量 weight ,系统只会为我们做一件事情。
1、只会生成 get/set 方法的声明,不会生成 get/set 方法的实现。
- (void)setWeight:(int)weight;
- (int)weight;
那么,如果我们自己实现 成员变量的 getter和setter 方法呢?
@interface Person (Test)
{
int weight;
}
@property (nonatomic, assign) int weight;
- (void)setWeight:(int)weight;
- (int)weight;
@end
运行上面的代码可以看出,直接报错,提示说Instance variables may not be placed in categoties
,实例变量不能放在分类里面。
3、手动实现 分类的 setter 和 getter 方法
@interface Person (Test)
@property (nonatomic, assign) int weight;
@end
@implementation Person (Test)
- (void)setWeight:(int)weight {
}
- (int)weight {
return 0;
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
person.age = 18;
person.weight = 60;
NSLog(@"age is %d, weight is %d", person.age, person.weight);
}
return 0;
}
运行上面的代码,打印结果为age is 18, weight is 0
,为什么我们赋值给 weight = 60 这个不好使,但是 age =18 就能有效果呢?
person.age = 18;
这句代码,相当于把 18 赋值给了_age 这个成员变量。
person.age
就是去getAge 直接返回了 _age 这个变量的值。
person.weight = 60
这句代码,可以看到分类的实现里面,并没有保存 _weight
这个成员变量,
person.weight
,分类代码里写死的 返回 0 ,所以直接返回结果 0
3.1、保存分类的成员变量的值
3.1.1、使用全局变量保存
对于上面的 demo ,不能保存 _weight 的值,我们试想下,可以单独写一个全局变量来保存外面传进来的值
int weight_;
@implementation Person (Test)
- (void)setWeight:(int)weight {
weight_ = weight;
}
- (int)weight {
return weight_;
}
@end
运行上面的代码,的确能做到外面的值保存下来,但是存在问题,相当于一个 全局的变量,创建多个对象,会产生多个对象公用一个 weight 变量。
Person *person = [[Person alloc] init];
person.age = 18;
person.weight = 60;
Person *person1 = [[Person alloc] init];
person1.age = 23;
person1.weight = 100;
// age is 18, weight is 100
NSLog(@"age is %d, weight is %d", person.age, person.weight);
// age is 23, weight is 100
NSLog(@"age is %d, weight is %d", person1.age, person1.weight);
3.1.2、使用dictionary 保存
NSMutableDictionary *weightDic_;
@implementation Person (Test)
+ (void)load {
weightDic_ = [NSMutableDictionary dictionary];
}
- (void)setWeight:(int)weight {
NSString *key = [NSString stringWithFormat:@"%p", self];
weightDic_[key] = @(weight);
}
- (int)weight {
NSString *key = [NSString stringWithFormat:@"%p", self];
return [weightDic_[key] intValue];
}
@end
运行上面的代码打印结果:
2020-12-21 23:31:44.820168+0800 MyTestDemo[29582:5762922] age is 18, weight is 60
2020-12-21 23:31:44.820785+0800 MyTestDemo[29582:5762922] age is 23, weight is 100
看到打印结果给人的感觉像是和系统生成的实现方法一样,但是内部却大有不同
- 1、存储内存不同
person1.age = 23;
23 是存储在 person1 对象内部。
person1.weight = 100;
100是存放正在全局的字典对象里面。 - 2、安全问题
- 3、如果分类在创建一个变量,那么就会在创建一个全局字典,代码冗余。
3.1.3、使用关联对象方法
主要使用 Runtime 的 API ,来修改 成员变量 name 的值。
@interface Person (Test)
@property (nonatomic, copy) NSString *name
@end
#import <objc/runtime.h>
@implementation Person (Test)
- (void)setName:(NSString *)name {
objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name {
return objc_getAssociatedObject(self, @selector(name));
}
@end
Person *person = [[Person alloc] init];
person.age = 18;
person.name = @"tom";
Person *person1 = [[Person alloc] init];
person1.age = 23;
person1.name = @"yang";
NSLog(@"age is %d, name is %@", person.age, person.name);
NSLog(@"age is %d, name is %@", person1.age, person1.name);
打印结果为:
2020-12-21 23:55:49.461183+0800 MyTestDemo[29679:5770200] age is 18, name is tom
2020-12-21 23:55:49.461634+0800 MyTestDemo[29679:5770200] age is 23, name is yang
在上面的例子中可以有多种方法实现:
// 1、使用 static const void * 作为key
static const void *MyKey = &MyKey;
objc_setAssociatedObject(obj, MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, MyKey)
// 2、使用 static char 作为key
static char MyKey;
objc_setAssociatedObject(obj, &MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, &MyKey)
// 3、直接使用属性名作为key
objc_setAssociatedObject(obj, @"property", value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_getAssociatedObject(obj, @"property");
// 4、使用get方法的@selecor作为key,或者在get方法中使用_cmd,objc_getAssociatedObject(obj,_cmd);
objc_setAssociatedObject(obj, @selector(getter), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
objc_getAssociatedObject(obj, @selector(getter))
上面的几种方法,我们使用最多和最方便的就是第4种方案。
4、关联对象剖析
4.1、关联对象 API
1、添加关联对象 objc_setAssociatedObject
使用给定的key和关联策略为给定的对象设置关联的value。
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy policy)
- object: 关联的对象,一般值当前的类
- value: 需要关联的值
- policy:策略,主要有以下5种策略模式,具体使用见下面表格
-
OBJC_ASSOCIATION_ASSIGN
、 -
OBJC_ASSOCIATION_RETAIN_NONATOMIC
、 -
OBJC_ASSOCIATION_COPY_NONATOMIC
、 -
OBJC_ASSOCIATION_RETAIN
、 OBJC_ASSOCIATION_COPY
-
策略主要对应的使用方法
修饰符 | objc_AssociationPolicy |
---|---|
assign | OBJC_ASSOCIATION_ASSIGN |
strong,nonatomic | OBJC_ASSOCIATION_RETAIN_NONATOMIC |
copy,nonatomic | OBJC_ASSOCIATION_COPY_NONATOMIC |
strong,atomic | OBJC_ASSOCIATION_RETAIN |
copy,atomic | OBJC_ASSOCIATION_COPY |
// objc_setAssociatedObject,底层代码实现
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);
// 判断传进来的 value 是否为 nil,如果有值则调用 acquireValue(value, policy)
id new_value = value ? acquireValue(value, policy) : nil;
{
// 初始化一个 AssociationsManager 对象
// 它维护了一个单例 Hash 表 AssociationsHashMap 对象
AssociationsManager manager;
// 初始化一个 AssociationsHashMap 对象
// 它维护 disguised_ptr_t 和 ObjectAssociationMap 对象之间的关系
AssociationsHashMap &associations(manager.associations());
// 根据传进来的 object 生成一个 key(disguised_ptr_t对象),不存在 和 object 的引用关系
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
// break any existing association.
// 根据 key(disguised_object)从 AssociationsHashMap 中获取遍历器 i
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// secondary table exists
ObjectAssociationMap *refs = i->second;
// 根据传进来的 key 从 ObjectAssociationMap 中获取遍历器 j
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);
}
2、获取关联对象objc_getAssociatedObject
返回给定key的给定对象关联的value。
objc_getAssociatedObject(id _Nonnull object_, const void * _Nonnull key)
/// 底层代码
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);
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()) {
ObjcAssociation &entry = j->second;
value = entry.value();
policy = entry.policy();
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;
}
3、移除关联对象 objc_removeAssociatedObjects
删除给定对象的所有关联。
objc_removeAssociatedObjects(id _Nonnull object)
如果只想移除给定对象的某个key的关联,可以使用objc_setAssociatedObject给参数value传值nil。
objc_setAssociatedObject(self, @selector(height), nil, OBJC_ASSOCIATION_ASSIGN);
/// 底层代码
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;
for (ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j != end; ++j) {
elements.push_back(j->second);
}
// remove the secondary table.
delete refs;
associations.erase(i);
}
}
// the calls to releaseValue() happen outside of the lock.
for_each(elements.begin(), elements.end(), ReleaseValue());
}
5、 关联对象的原理
分析上面的源码,得到实现关联对象技术的核心对象有下面几个
- AssociationsManager
- AssociationsHashMap
- ObjectAssociationMap
- ObjcAssociation
class AssociationsManager {
static AssociationsHashMap *_map;
};
class AssociationsHashMap : public unordered_map<disguised_ptr_t, ObjectAssociationMap>
class ObjectAssociationMap : public std::map<void *, ObjcAssociation>
class ObjcAssociation {
uintptr_t _policy;
id _value;
};
5.1、AssociationsManager
- 关联对象并不是存储在关联对象本身内存中,而是存储在全局统一的一个容器中;
- 由 AssociationsManager 管理并在它维护的一个单例 Hash 表 AssociationsHashMap 中存储;
- 使用 AssociationsManagerLock 自旋锁保证了线程安全。
// 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;
}
};
5.2、AssociationsHashMap
- 一个单例的 Hash 表,存储 disguised_ptr_t 和 ObjectAssociationMap 之间的映射。
- disguised_ptr_t 是根据 object 生成,但不存在引用关系。
// AssociationsHashMap
// disguised_ptr_t disguised_object = DISGUISE(object);
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); }
};
5.3、ObjectAssociationMap
- 存储 key 和 ObjcAssociation 之间的映射。
// ObjectAssociationMap
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); }
};
5.4、ObjcAssociation
- 存储着关联策略 policy 和关联对象的值 value。
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; }
};
这四个函数对应代码的关系如下
6、底层逻辑流程
6.1、objc_setAssociatedObject
① 实例化一个 AssociationsManager 对象,它维护了一个单例 Hash 表 AssociationsHashMap 对象;
② 实例化一个 AssociationsHashMap 对象,它维护 disguised_ptr_t 和 ObjectAssociationMap 对象之间的关系;
③ 根据object生成一个 disguised_ptr_t 对象;
④ 根据 disguised_ptr_t 获取对应的 ObjectAssociationMap 对象,它存储key和 ObjcAssociation 之间的映射;
⑤ 根据policy和value创建一个 ObjcAssociation 对象,并存储在 ObjectAssociationMap 中;
⑥ 如果传进来的value为 nil,则在 ObjectAssociationMap 中删除该 ObjcAssociation 对象。
6.2、objc_getAssociatedObject
① 实例化一个 AssociationsManager 对象;
② 实例化一个 AssociationsHashMap 对象;
③ 根据object生成一个 disguised_ptr_t 对象;
④ 根据 disguised_ptr_t 获取对应的 ObjectAssociationMap 对象;
⑤ 根据 key 获取到它所对应的 ObjcAssociation 对象;
⑥ 返回 ObjcAssociation 中的 value。
6.3、objc_removeAssociatedObjects
① 实例化一个 AssociationsManager 对象;
② 实例化一个 AssociationsHashMap 对象;
③ 根据object生成一个 disguised_ptr_t 对象;
④ 根据 disguised_ptr_t 获取对应的 ObjectAssociationMap 对象;
⑤ 删除该 ObjectAssociationMap 对象。
思考
1、 关联对象的成员变量存放在哪里
- 关联对象并不是存储在被关联对象本身内存中
- 关联对象存储在全局的统一一个的 AssociationsManager 中
2、如何移除关联对象?
1、移除一个object的某个key的关联对象:调用objc_setAssociatedObject设置关联对象value为nil。
objc_setAssociatedObject函数会调用_object_set_associative_reference函数,并在该函数中判断传进来的value是否为nil,是的话会调用erase(j)擦除函数,将j变量擦除。j即为ObjectAssociationMap对象里的一对【key: key value: ObjcAssociation(_policy、_value)】。
2、移除一个object的所有关联对象:调用函数objc_removeAssociatedObjects。objc_removeAssociatedObjects函数会调用_object_remove_assocations函数,并在该函数中调用对象的erase(i)擦除函数,将i变量擦除。i即为AssociationsHashMap对象中的一对【key: object value: ObjectAssociationMap】。
3、如果 object 被销毁,那它所对应的 ObjectAssociationMap 是否也会自动销毁?
会。
4、如果没有关联对象,怎么实现 Category 有成员变量的效果?
使用字典。创建一个全局的字典,将self对象在内存中的地址作为key。
缺点:① 内存泄漏问题:全局变量会一直存储在内存中;
② 线程安全问题:可能会有多个对象同时访问字典,加锁可以解决;
③ 每添加一个成员变量就要创建一个字典,很麻烦。
#import "Person.h"
@interface Person (Test)
@property (nonatomic, assign) int height;
@end
#import "Person+Test.h"
#import <objc/runtime.h>
@implementation Person (Test)
NSMutableDictionary *heights_;
+ (void)load {
heights_ = [NSMutableDictionary dictionary];
}
- (void)setHeight:(int)height {
NSString *key = [NSString stringWithFormat:@"%@",self];
heights_[key] = @(height);
}
- (int)height {
NSString *key = [NSString stringWithFormat:@"%@",self];
return [heights_[key] intValue];
}