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");    
}
  1. 动态添加属性
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 : 策略,属性以什么形式保存。
有以下几种

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值只要是一个指针即可,我们可以传入@selector(name)

  1. 获得属性
objc_getAssociatedObject(id object, const void *key);

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

  1. 移除所有关联对象 , 通常情况下不建议使用这个函数,因为他会断开所有关联。只有在需要把对象恢复到“原始状态”的时候才会使用这个函数。
- (void)removeAssociatedObjects
{
    // 移除所有关联对象
    objc_removeAssociatedObjects(self);
}

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

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

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

源码分析

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

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

@{self:

     @{@"self":@(selectedComponent)+OBJC_ASSOCIATION_ASSIGN}

}

函数中涉及几个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函数中

_object_set_associative_reference函数

接着我们来到ObjcAssociation中

ObjcAssociation

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

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

重新回到_object_set_associative_reference函数实现中

_object_set_associative_reference函数内部

细读上述源码我们可以发现,首先根据我们传入的value经过acquireValue函数处理获取new_value。acquireValue函数内部其实是通过对策略的判断返回不同的值

acquireValue函数内部

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

DISGUISE函数其实仅仅对object做了位运算 , 转换成了unsigned long 类型 , 可能是为了提高读取的速度吧

object->isa与objectClass的地址不同,从64bit开始,isa需要进行一次位运算,才能计算出真实地址,这个是为了算出来真实的地址 , 用真实的地址做key来存.,而位运算的值我们可以通过下载objc源代码找到。

ISA_MASK

我们通过位运算进行验证。

isa通过位运算计算出正确的地址

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

关键代码

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

value为nil

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

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

关联对象底层对象关系

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

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

objc_getAssociatedObject函数

objc_getAssociatedObject内部调用的是_object_get_associative_reference

objc_getAssociatedObject

_object_get_associative_reference函数

_object_get_associative_reference函数

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

objc_removeAssociatedObjects函数

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

objc_removeAssociatedObjects函数

_object_remove_assocations函数

_object_remove_assocations函数

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

总结 : 

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

关联对象存在哪里?
关联对象并不是存储在被关联对象本身内存中
关联对象存储在全局统一的一个AssociationsManager,AssociationsHashMap中 , 这个比较复杂 , 具体原理看源码分析

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

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


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


部分内容参考链接 : https://www.jianshu.com/p/0f9b990e8b0a

 

猜你喜欢

转载自blog.csdn.net/u014600626/article/details/51435943