iOS 关联对象详解

在平时的工作中经常碰到给类别添加属性的操作,那么实现思路是怎么样的呢?
代码实现:新建一个Person类和Person+Text的类别
//Person 代码
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (assign, nonatomic) int age;
@end


//类别代码
#import "Person.h"
@interface Person (Test)
@property (copy, nonatomic) NSString *name;
@end

//调用代码
Person *person = [[Person alloc] init];
person.age = 10;
person.name = @"jack";
NSLog(@"person - age is %d, name is %@", person.age, person.name);

输出:age 10,name 没有值
name赋值失败原因:age是类里面的属性,系统会自动生成成员变量_agegetAgesetAge方法的声明和实现。所以赋值成功。name是利用类别添加的属性,在类别里面添加属性并不会生成_name成员变量,只会getNamesetName方法的声明没有实现。所以赋值失败。详情请看:iOS Category的本质 iOS OC对象的本质窥探(一)
本质原因:Category 结构体,并没有存储成员变量
struct category_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods; // 对象方法
    struct method_list_t *classMethods; // 类方法
    struct protocol_list_t *protocols; // 协议
    struct property_list_t *instanceProperties; // 属性
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
实现类别添加属性思路1,设置一个全局字典自己保存成员变量的值,代码实现如下。
#import "Person+Test.h"

#define Key [NSString stringWithFormat:@"%p", self]

@implementation Person (Test)

NSMutableDictionary *names_;
+ (void)load
{
    names_ = [NSMutableDictionary dictionary];
}

- (void)setName:(NSString *)name
{
    names_[Key] = name;
}

- (NSString *)name
{
    return names_[Key];
}
@end
通过这种思路确实可以实现给类别添加属性的功能,但是也有明显的弊端。

1.每次给这个添加一个新的属性时需要重新创建一个新的字典保存。
2.给属性赋值或者取值时会出现线程完全问题,需要加锁控制。
3.字典什么时候释放,也存在内存泄漏的隐患。

如果使用上述思路维护难度较大,使用runtime关联对象方法,代码如下
- (void)setName:(NSString *)name
{
    objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)name
{
    // 隐式参数
    // _cmd == @selector(name)
    return objc_getAssociatedObject(self, _cmd);
}

1.动态添加属性

//id  _Nonnull object  关联的对象
//const void * _Nonnull key 存储的key
//id  _Nullable value 存储的value
//objc_AssociationPolicy policy 对应的修饰符
objc_setAssociatedObject(<#id  _Nonnull object#>, <#const void * _Nonnull key#>, <#id  _Nullable value#>, <#objc_AssociationPolicy policy#>)

参数一:id object : 给哪个对象添加属性,这里要给自己添加属性,用self
参数二:void * == id key : 属性名,根据key获取关联对象的属性的值,在objc_getAssociatedObject中通过次key获得属性的值并返回。
参数三: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     // 指定相关的对象被复制,原子性   
};

策略对应的属性修饰符图示


策略对应的属性修饰符图示.png

key值只要是一个指针即可,我们可以传入@selector(name)

2.获取属性

objc_getAssociatedObject(id object, const void *key);

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

3.移除所有关联对象

- (void)removeAssociatedObjects{
    // 移除所有关联对象
    objc_removeAssociatedObjects(self);
}
运行代码
//调用代码
Person *person = [[Person alloc] init];
person.age = 10;
person.name = @"jack";
NSLog(@"person - age is %d, name is %@", person.age, person.name);
赋值成功,那么关联对象的原理是什么呢?
打开源码:找到objc_setAssociatedObject函数
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
    _object_set_associative_reference(object, (void *)key, value, policy);
}
来到_object_set_associative_reference里面
_object_set_associative_reference.png
我们发现

实现关联对象技术的核心对象有
AssociationsManager
AssociationsHashMap
ObjectAssociationMap
ObjcAssociation
其中Map同我们平时使用的字典类似。通过key-value一一对应存值。

分析这四个对象其内部实现原理探寻他们之间的关系。
AssociationsManager

通过AssociationsManager内部源码发现,AssociationsManager内部有一个AssociationsHashMap对象。

AssociationsManager内部方法.png
AssociationsHashMap
AssociationsHashMap.png

通过AssociationsHashMap内部源码我们发现AssociationsHashMap继承自unordered_map首先来看一下unordered_map内的源码

unordered_map.png

unordered_map源码中我们可以看出_Key_Tp也就是前两个参数对应着map中的KeyValue,那么对照上面AssociationsHashMap内源码发现_Key中传入的是disguised_ptr_t_Tp中传入的值则为ObjectAssociationMap*

紧接着我们来到ObjectAssociationMap中,上图中ObjectAssociationMap已经标记出,我们发现ObjectAssociationMap中同样以keyValue的方式存储着ObjcAssociation

接着我们来到ObjcAssociation中

ObjcAssociation.png

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

现在我们已经对AssociationsManagerAssociationsHashMapObjectAssociationMapObjcAssociation四个对象之间的关系有了简单的认识,那么接下来我们来细读源码,看一下objc_setAssociatedObject函数中传入的四个参数分别放在哪个对象中充当什么作用。

重新回到_object_set_associative_reference函数实现中看看具体的实现
WeWork Helper20200114111018.png

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

acquireValue函数内部实现
acquireValue函数内部实现.png

acquireValue函数通过对策略的判断返回不同的值
之后创建AssociationsManager manager;以及拿到manager内部的AssociationsHashMap即associations。
之后我们看到了我们传入的第一个参数object
object经过DISGUISE函数被转化为了disguised_ptr_t类型的disguised_object。

disguised_ptr_t disguised_object = DISGUISE(object);
typedef uintptr_t disguised_ptr_t;
inline disguised_ptr_t DISGUISE(id value) { return ~uintptr_t(value); }
inline id UNDISGUISE(disguised_ptr_t dptr) { return id(~dptr); }

DISGUISE函数其实仅仅对object做了位运算

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

// create the new association (first time).
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
object->setHasAssociatedObjects();

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

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

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

原理解读图示
objc_setAssociatedObject源码底层实现.png
通过上图我们可以总结为:一个实例对象就对应一个ObjectAssociationMap,而ObjectAssociationMap中存储着多个此实例对象的关联对象的key以及ObjcAssociation,为ObjcAssociation中存储着关联对象的value和policy策略。

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

取值相关
objc_getAssociatedObject函数
id objc_getAssociatedObject(id object, const void *key) {
    return _object_get_associative_reference(object, (void *)key);
}

objc_getAssociatedObject内部调用的是_object_get_associative_reference
_object_get_associative_reference函数

_object_get_associative_reference函数.png

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

移除函数:objc_removeAssociatedObjects函数

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

void objc_removeAssociatedObjects(id object) 
{
    if (object && object->hasAssociatedObjects()) {
        _object_remove_assocations(object);
    }
}
_object_remove_assocations函数
WeWork Helper20200114113158.png

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

总结:

关联对象并不是存储在被关联对象本身内存中,而是存储在全局的统一的一个AssociationsManager中,如果设置关联对象为nil,就相当于是移除关联对象。

此时我们我们在回过头来看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     // 指定相关的对象被复制,原子性   
};

我们会发现其中只有RETAIN和COPY而为什么没有weak呢?
总过上面对源码的分析我们知道,object经过DISGUISE函数被转化为了disguised_ptr_t类型的disguised_object

disguised_ptr_t disguised_object = DISGUISE(object);

而同时我们知道,weak修饰的属性,当没有拥有对象之后就会被销毁,并且指针置位nil,那么在对象销毁之后,虽然在map中既然存在值object对应的AssociationsHashMap,但是因为object地址已经被置位nil,会造成坏地址访问而无法根据object对象的地址转化为disguised_object了。

我的简书主页
我的博客主页

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

推荐阅读更多精彩内容

  • 前言 associatedObject又称关联对象。顾名思义,就是把一个对象关联到另外一个对象身上。使两者能够产生...
    luonaerduo阅读 1,879评论 0 1
  • 前言 associatedObject又称关联对象。顾名思义,就是把一个对象关联到另外一个对象身上。使两者能够产生...
    VV木公子阅读 6,815评论 4 13
  • 多线程、特别是NSOperation 和 GCD 的内部原理。运行时机制的原理和运用场景。SDWebImage的原...
    LZM轮回阅读 2,003评论 0 12
  • “教师资格证式" 试讲方式 近来经常面试前来应聘的教师,他们大多是刚毕业没有经验或毕业不久讲课水平不高的老师。...
    海纳百川_b083阅读 246评论 0 1
  • 四十四回写贾琏在凤姐生日之时偷腥,被凤姐逮个正着,平儿无故遭殃。四十六回写贾赦老不正经打老太太佣人鸳鸯的主意,邢夫...
    易恒40阅读 653评论 0 2