OC-关联对象AssociatedObject

关联对象

前言

我们都知道ARC环境下, 在一个类中声明一个属性@property (nonatomic, assign) int age;, 系统类似的帮我们生成如下代码:

  1. 生成下划线的成员变量
  2. 生成setter, getter方法的声明
  3. 生成setter, getter方法的实现
@interface Person : NSObject
{
    int _age;
}

- (void)setAge:(int)age;
- (int)age;

@end


@implementation Person

- (void)setAge:(int)age
{
    _age = age;
}

- (int)age
{
    return _age;
}
@end

Category中添加属性

在category中添加属性, 系统只会做一件事情, 生成setter, getter方法的声明.
我们知道category中不可以添加实例变量, 因为category是一个结构体, 它只可以添加对象/类方法, 协议, 属性

struct _category_t {
    const char *name;
    struct _class_t *cls;
    const struct _method_list_t *instance_methods;
    const struct _method_list_t *class_methods;
    const struct _protocol_list_t *protocols;
    const struct _prop_list_t *properties;
};

如果要让我们实现可以类似的可以添加实例变量的效果, 那该如何做呢?

  • 方式一: 使用全局字典

    因为我们想让category实现每一个person对象有一个实例变量的效果, 所以我们可以想到在全局创建一个可变字典, 每个person对应一个实例变量, 如下实现:

    NSMutableDictionary *ages_;
    
    @implementation Person (Test1)
    
    + (void)load {
        ages_ = [NSMutableDictionary dictionary];
    }
    
    - (void)setAge:(int)age
    {
        NSString *key = [NSString stringWithFormat:@"%p", self];
        ages_[key] = @(age);
    }
    
    - (int)age {
        NSString *key = [NSString stringWithFormat:@"%p", self];
        return [ages_[key] intValue];
    }
    
    @end
    
    
    1. person对象的实例变量是存储在person对象的内部, 而这种实现方式, 将实例变量存在了全局字典中, 实例变量存储的位置不同
    2. 因为是全局的字典, 所以存在线程安全的问题, 需要在setter方法中加锁
  • 方式二: 使用关联对象

    /**
        object: 需要关联的对象
        key: 指针 类似于字典的key void *
        value: 关联的值
        policy: 内存策略
    */
    objc_setAssociatedObject(<#id  _Nonnull object#>, <#const void * _Nonnull key#>, <#id  _Nullable value#>, <#objc_AssociationPolicy policy#>)
    
    objc_getAssociatedObject(self, <#const void * _Nonnull key#>)
    
    

    内存策略:

    objc_AssociationPolicy 对应的修饰符
    OBJC_ASSOCIATION_ASSIGN assign
    OBJC_ASSOCIATION_RETAIN_NONATOMIC strong, nonatomic
    OBJC_ASSOCIATION_COPY_NONATOMIC copy, nonatomic
    OBJC_ASSOCIATION_RETAIN strong, atomic
    OBJC_ASSOCIATION_COPY copy, atomic
  • key的定义方式一:

    static const void *ageKey = &ageKey;
    - (void)setAge:(int)age
    {
        objc_setAssociatedObject(self, ageKey, @(age), OBJC_ASSOCIATION_ASSIGN);
    }
    
    

    因为key类似于字典的key, 所以每个关联的值的key是唯一的, 为了唯一性, 我们可以使用: static const void *ageKey = &ageKey; (ageKey这个指针变量存储的是它自己这个变量的地址, 这样写可以保证如果有很多关联的key的话, 可以确保每个key是唯一的)
    static修饰也可以防止其他文件用extern关键字获取这个key
    static 保证这个全局变量只在内部使用

    变量 内存
    ageKey 0x10000 0x10000
    nameKey 0x10008 0x10008
  • key的定义方式二:

    //更加省事而且声明的这个变量只占一个字节 char
    static const char ageKey;
    - (void)setAge:(int)age
    {
        objc_setAssociatedObject(self, &ageKey, @(age), OBJC_ASSOCIATION_ASSIGN);
    }
    
  • key的定义方式三:
    知识点: NSString的内存分配

    // 使用@"age", NSString *str = @"age"; 字面量的字符串变量存储在常量区, 所以@"age", 所以两个方法中的@"age"字符串的内存地址都是一样的. 
    - (void)setAge:(int)age
    {
        objc_setAssociatedObject(self, @"age", @(age), OBJC_ASSOCIATION_ASSIGN);
    }
    
    - (int)age {
        objc_getAssociatedObject(self, @"age")
    }
    
  • key的定义方式四:

    - (void)setAge:(int)age
    {
        objc_setAssociatedObject(self, @selector(age), @(age), OBJC_ASSOCIATION_ASSIGN);
    }
    
    - (int)age {
        // _cmd表示当前方法的@selector, _cmd == @selector(age)
        objc_getAssociatedObject(self, _cmd);
    }
    /*OC的编译器在编译后会在每个方法中加两个隐藏的参数:
    一个是_cmd,当前方法的一个SEL指针。
    一个是self,指向当前对象的一个指针
    (id)self, (SEL)_cmd
    */
    // 当然使用@seletor(setAge:)等其他方法也可以
    

关联对象的原理

实现关联对象技术的核心对象有:
AssociationsManager
AssociationsHashMap
ObjectAssociationMap
ObjcAssociation
可以通过苹果的开源代码 objc4: objc-references.mm //引用

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

举例说明:

@implementation Person (Test)

- (void)setAge:(int)age
{
    objc_setAssociatedObject(self, @selector(age), @(age), OBJC_ASSOCIATION_ASSIGN);
}

- (void)setName:(NSString *)name
{
    objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

@end

上面的代码给Person实例对象关联的两个值(age和name), 底层上是由全局的AssociationsManager管理, AssociationsManager中有一个AssociationsHashMap(字典), 其中以(姑且认为)person为键, AssociationsHashMap(字典)为值. AssociationsHashMap(字典)中以关联值传入的key为键, 以ObjcAssociation对象为值, ObjcAssociation中包含内存策略和value值

开源代码如下:

// setter
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
    _object_set_associative_reference(object, (void *)key, value, policy);
}

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());
        // 传入的对象object, 经过DISGUISE(object)函数, 进行内存操作, 作为key
        disguised_ptr_t disguised_object = DISGUISE(object);
        if (new_value) { //如果
            // break any existing association.
            // 根据disguised_object, 找到该对象对应的AssociationsHashMap
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i != associations.end()) {
                // secondary table exists
                // 根据i->second找到ObjectAssociationMap的指针
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    //如果key有对应的`ObjcAssociation`, 则替换
                    old_association = j->second;
                    j->second = ObjcAssociation(policy, new_value);
                } else {
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else { //如果key没有对应的`ObjcAssociation`, 则创建新的key, ObjcAssociation键值对
                // 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.
            // 如果传入的value为nil值
            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()) { //遍历该对象对应的字典中所有的ObjectAssociationMap, 进行抹除操作
                    old_association = j->second;
                    refs->erase(j); // 抹除
                }
            }
        }
    }
    // release the old value (outside of the lock).
    if (old_association.hasValue()) ReleaseValue()(old_association);
}


// getter
id objc_getAssociatedObject(id object, const void *key) {
    return _object_get_associative_reference(object, (void *)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) {
                    objc_retain(value);
                }
            }
        }
    }
    if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
        objc_autorelease(value);
    }
    return value;
}


总结:

1.关联对象并不是存储在被关联对象本身内存中
2.关联对象存储在全局的统一的一个AssociationsManager
3.设置关联对象为nil, 就相当于移除关联对象
4.移除某个对象上的所有的关联对象 void objc_removeAssociatedObjects(id object)
5.如果某个person对象被销毁了, 则这个person对象所对应的ObjectAssociationMap字典也会被销毁
6.因为内存策略(objc_AssociationPolicy)中没有weak,

    Person *p = [[Person alloc] init];
        
    {
        Person *tmp = [[Person alloc] init];
        objc_setAssociatedObject(p, @"tmp", tmp, OBJC_ASSOCIATION_ASSIGN);
    }
    NSLog(@"%@", objc_getAssociatedObject(p, @"tmp"));
    // 上面代码会报错误 EXC_BAD_ACCESS, 坏内存地址访问, 因为使用的是OBJC_ASSOCIATION_ASSIGN的内存策略, 出了大括号tmp对象释放

如何设置关联值的时候使用weak策略呢?
iOS weak 关键字漫谈

方式一: 使用block包裹

- (void)setContext:(CDDContext*)object {
    id __weak weakObject = object;
    id (^block)() = ^{ return weakObject; };
    objc_setAssociatedObject(self, @selector(context), block, OBJC_ASSOCIATION_COPY);
}

- (CDDContext*)context {
    id (^block)() = objc_getAssociatedObject(self, @selector(context));
    id curContext = (block ? block() : nil);
    return curContext;
}

方式二: 使用对象包裹

新建一个类,用于包裹weak对象
@interface GYCatagoryWeakWrapper : NSObject
@property (nonatomic, weak) id weakObj;
@end

@implementation GYCatagoryWeakWrapper
@end

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