Association关联对象

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; }
    };

这四个函数对应代码的关系如下

图片.png

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];
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容