OC关联objc_setAssociatedObject, 如何使用及原理

面试题

  1. Category能否添加成员变量?如果可以,如何给Category添加成员变量?
    答:不能直接添加成员变量,但是可以通过runtime的方式间接实现添加成员变量的效果。
  2. 为什么Category 不能添加成员变量?
    我们从分类的结构的角度来考虑一下分类中为什么不能添加成员变量:        

 通过分类的底层结构我们可以看到,分类中可以存放实例方法,类方法,协议,属性,但是没有存放成员变量的地方。所以不能添加.


使用Runtime给系统的类添加属性,首先需要了解对象与属性的关系。对象一开始初始化的时候其属性为nil,给属性赋值其实就是让属性指向一块存储内容的内存,使这个对象的属性跟这块内存产生一种关联。而想要给系统的类添加属性,可以通过分类。

这里给NSObject添加name属性,创建NSObject的分类
我们可以使用@property给分类添加属性

@property(nonatomic,strong)NSString *name;

通过探寻Category的本质我们知道,虽然在分类中可以写@property
添加属性,但是不会自动生成私有属性,也不会生成set,get方法的实现,只会生成set,get的声明,需要我们自己去实现。

RunTime提供了动态添加属性和获得属性的方法。

-(void)setName:(NSString *)name {
    objc_setAssociatedObject(self, @"name",name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(NSString *)name {
    return objc_getAssociatedObject(self, @"name");    
}

动态添加属性

objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);

参数一:id object : 给哪个对象添加属性,这里要给自己添加属性,用self。
参数二:void * == id key : 属性名,根据key获取关联对象的属性的值,在objc_getAssociatedObject中通过次key获得属性的值并返回。推荐直接使用 @"变量名" 或者 @selector(变量的set/get方法) . 
参数三:id value : 关联的值,也就是set方法传入的值给属性去保存。
参数四:objc_AssociationPolicy policy : 策略,属性以什么形式保存。
有以下几种, 可以看到并没有weak这样的修饰词, 最后会有weak的实现方式

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,  // 使用assign关联对象
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // 指定相关对象的强引用,非原子性
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,  // 指定相关的对象被复制,非原子性
    OBJC_ASSOCIATION_RETAIN = 01401,  // 指定相关对象的强引用,原子性
    OBJC_ASSOCIATION_COPY = 01403     // 指定相关的对象被复制,原子性   
};

获得属性

objc_getAssociatedObject(id object, const void *key);

参数一:id object : 获取哪个对象里面的关联的属性。
参数二:void * == id key : 什么属性,与objc_setAssociatedObject中的key相对应,即通过key值取出value。

移除所有关联对象 , 通常情况下不建议使用这个函数,因为他会断开所有关联, 系统在对象dealloc中调用了。只有在需要把对象恢复到“原始状态”的时候才会使用这个函数。

void objc_removeAssociatedObjects(id object)

此时已经成功给NSObject添加name属性,并且NSObject对象可以通过点语法为属性赋值。

NSObject *objc = [[NSObject alloc]init];
objc.name = @"aaaa";
NSLog(@"%@",objc.name);

可以看出关联对象的使用非常简单,接下来我们来探寻关联对象的底层原理

源码分析

先说结论, 关联对象并没有存放在对象的实体中,而是runtime维护了一个全局二维map来管理所有关联对象。

比如调用  objc_setAssociatedObject(self, @"selectedComponent", trueValue, OBJC_ASSOCIATION_RETAIN_NONATOMIC) ;会生成这样一个结构 . 2层的字典嵌套 , 最外层用对象(self)做key , value是一个字典 , 内层字典中 , 用传进来的key(@"selectedComponent") 做key , value是 真正的值和存储策略( trueValue, OBJC_ASSOCIATION_ASSIGN) 的组合 . 比如这样

@{self :

     @{@"selectedComponent":( trueValue+OBJC_ASSOCIATION_RETAIN_NONATOMIC) }

}

函数中涉及几个4个重要的数据结构:
AssociationsManager //管理全局AssociationsHashMap
AssociationsHashMap //存放对象的关联对象map的map(key为传入的object,value为map,也就是ObjectAssociationMap)
ObjectAssociationMap //存放关联对象的map(key为传入的key,value为关联对象)
ObjcAssociation //关联对象实体包含了value和policy两个重要信息(policy决定了value的内存管理方式)

objc_setAssociatedObject函数

来到runtime源码,首先找到objc_setAssociatedObject函数,看一下其实现

我们看到其实内部调用的是_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);
}

先不要细看, 下面会分析, 着重看ObjcAssociation(policy, new_value) 把缓存策略和值传入, 返回一个ObjcAssociation对象, 接着我们来到ObjcAssociation中

我们发现ObjcAssociation存储着_policy_value,而这两个值我们可以发现正是我们调用objc_setAssociatedObject函数传入的值,也就是说我们在调用objc_setAssociatedObject函数中传入的value和policy这两个值最终是存储在ObjcAssociation中的。

那么接下来我们来细读源码,看一下objc_setAssociatedObject函数中传入的四个参数分别放在哪个对象中充当什么作用。

重新回到_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, 获取到全局的map
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        // 计算object在map中的key
        disguised_ptr_t disguised_object = DISGUISE(object);
        // 如果有值, 设置新值到map中
        if (new_value) {
            // break any existing association.
            // 查找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);
                // 说明ObjectAssociationMap存在了,在查到object下的key是否存在
                if (j != refs->end()) {
                    // key存在,设置新值,同时保存旧值,一会儿会把旧值release一次
                    old_association = j->second;
                    j->second = ObjcAssociation(policy, new_value);
                } else {
                    // key不存在,生成ObjcAssociation,ObjcAssociation里面就是(value+缓存策略),
                    // 建立起了key<-->ObjcAssociation的关联
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else {
                // create the new association (first time).
                // ObjectAssociationMap不存在, 生成一个,并建立key<-->ObjcAssociation的关联
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[disguised_object] = refs;
                (*refs)[key] = ObjcAssociation(policy, new_value);
                // 修改object的isa指针,标记为有关联对象,在object的dealloc中有用
                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函数内部其实是通过对策略的判断返回不同的值, 根据对应的内存策略进行一次 retain/copy 操作.

之后创建AssociationsManager manager;以及拿到manager内部的AssociationsHashMap即associations
之后我们看到了我们传入的第一个参数object, 
object经过DISGUISE函数被转化为了disguised_ptr_t类型的disguised_object (直接翻译过来是伪对象)

DISGUISE函数其实仅仅对object做了位运算, 按位取反 , 转换成了unsigned long 类型 , 后面会用disguised_object做key查找. 

这是是判断处理过后的disguised_object是否已经在associations存在了, 如果存在则写入ObjcAssociation, 不存在则创建ObjectAssociationMap

如果我们value设置为nil的话那么会执行下面的代码

从上述代码中可以看出,如果我们设置value为nil时,就会将关联对象从ObjectAssociationMap中移除。

总结一下, value被处理成new_value,同policy被存入了ObjcAssociation中。
而ObjcAssociation对应我们传入的key被存入了ObjectAssociationMap中。
disguised_object和ObjectAssociationMap则以key-value的形式对应存储在associations中也就是AssociationsHashMap中。

最后我们通过一张图可以很清晰的理清楚其中的关系

关联对象底层对象关系

通过上图我们可以总结为:一个实例对象就对应一个ObjectAssociationMap,而ObjectAssociationMap中存储着多个此实例对象的关联对象的key以及ObjcAssociation,为ObjcAssociation中存储着关联对象的value和policy策略。

由此我们可以知道关联对象并不是放在了原来的对象里面,而是存在runtime维护了一个全局的map用来存放每一个对象及其对应关联属性表格。

objc_getAssociatedObject函数

objc_getAssociatedObject内部调用的是_object_get_associative_reference

_object_get_associative_reference函数

同样的套路, 获取到runtime的全局AssociationsHashMap, 然后根据object找出ObjectAssociationMap, 在ObjectAssociationMap找ObjcAssociation, 最后ObjcAssociation就是我们要找的对象了.

从_object_get_associative_reference函数内部可以看出,像set方法中那样,反向将value一层一层取出最后return出去。

objc_removeAssociatedObjects函数

objc_removeAssociatedObjects用来删除所有的关联对象,objc_removeAssociatedObjects函数内部调用的是_object_remove_assocations函数

_object_remove_assocations函数

获取到runtime的全局AssociationsHashMap, 然后根据object找出ObjectAssociationMap, 然后把ObjectAssociationMap中的每一个元素加入到一个栈结构中, 调用元素的release方法, 清除掉ObjectAssociationMap

上述源码可以看出_object_remove_assocations函数将object对象向对应的所有关联对象全部删除。

总结 : 

如何给分类添加实例变量?
默认情况下,因为分类底层结构的限制,不能添加成员变量到分类中。但可以通过关联对象来间接实现
添加关联对象:void objc_setAssociatedObject
获得关联对象:id objc_getAssociatedObject
移除所有的关联对象: void objc_removeAssociatedObjects

关联对象存在哪里?
关联对象并不是存储在被关联对象本身内存中
关联对象存储在全局统一的一个AssociationsManager,AssociationsHashMap中 , 这个是一个二维hash表, 

实现关联对象技术的核心对象有

  1. AssociationsManager
  2. AssociationsHashMap
  3. ObjectAssociationMap
  4. ObjcAssociation
    其中Map同我们平时使用的字典类似。通过key-value一一对应存值。


对象销毁,分类的关联对象会移除吗?
会移除, 对象dealloc执行的时候,会检查是否有ObjcAssociation  , 内部会有 erase 操作

怎么实现一个weak对象的关联?

可以使用一个block捕获一个weak遍历, 然后使用objc_setAssociatedObject 关联这个block, 等需要取值的时候, 取出这个block, 然后调用block返回weak对象即可. 

具体代码如下: 

typedef id (^WeakBlock)(void);

- (void)setWeakObj:(NSObject *)weakObj {
    id __weak __weak_object = weakObj;
    WeakBlock block = ^{
        return __weak_object;
    };
    objc_setAssociatedObject(self, @selector(weakObj), block, OBJC_ASSOCIATION_COPY);
}

- (NSObject *)weakObj {
    WeakBlock block = objc_getAssociatedObject(self, @selector(weakObj));
    NSObject *result = nil;
    if (block) {
        result = block();
    }
    return result;
}

Objective-C Category Property Macro · GitHub

 

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值