关联对象详解

分类(category)与关联对象(Associated Object)作为objective-c的扩展机制的两个特性:分类:可以通过它来扩展方法,Associated Object:可以通过它来扩展属性。
在iOS开发中,可能Category比较常见,相对的Associated Object,就用的比较少,要用它之前,必须导入<objc/runtime.h>的头文件。

一 基本使用

关联对象提供了以下API:

// 1.添加关联对象:
void objc_setAssociatedObject(id object, const void * key, id value, objc_AssociationPolicy policy)
// 2.获得关联对象:
id objc_getAssociatedObject(id object, const void * key)
// 3.移除所有的关联对象
void objc_removeAssociatedObjects(id object)

在添加关联对象的方法中有一个policy属性,它是一个枚举值,对应我们平时定义属性时设置的修饰词

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

下面是使用示例:

#import <Foundation/Foundation.h>

@interface Person : NSObject

@end

#import "Person.h"
#import <objc/runtime.h>
@implementation Person

@end

#import "Person.h"
@interface Person (Baba)

@property(assign,nonatomic) int age;
@property(copy,nonatomic) NSString *name;

@end

#import "Person+Baba.h"
#import <objc/runtime.h>
@implementation Person (Baba)

-(void)setAge:(int)age {

    objc_setAssociatedObject(self, @selector(age),@(age), OBJC_ASSOCIATION_ASSIGN);
}

- (int)age{
   return [objc_getAssociatedObject(self, @selector(age)) intValue];
}

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

}

- (NSString *)name {
   return objc_getAssociatedObject(self, @selector(name));
}

@end

#include<stdio.h>
#include "Person.h"
#import <Foundation/Foundation.h>
#import "Person+Baba.h"
 int main()
{
    Person *person = [[Person alloc] init];
    person.age = 10;
    person.name = @"张三";
    NSLog(@"姓名:%@,年龄:%d",person.name,person.age);

}

output:
2019-02-11 13:08:18.630262+0800 test[7310:121043] 姓名:张三,年龄:10

我们利用关联对象给分类添加属性,使用是很简单的,就三个方法,下面我们看一下它的底层实现原理,当然了,我们主要研究三个关联对象方法的底层实现。

二 底层原理

我们先来看一下 void objc_setAssociatedObject(id object, const void * key, id value, objc_AssociationPolicy policy)底层实现,我们在objc源代码,objc-runtime.mm可以看到


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

id objc_getAssociatedObject(id object, const void *key) {
    return _object_get_associative_reference(object, (void *)key);
}

void objc_removeAssociatedObjects(id object) 
{
    if (object && object->hasAssociatedObjects()) {
        _object_remove_assocations(object);
    }
}

我们跟进去看一下

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

我们可以看到与实现关联对象相关的几个对象对象如下:
AssociationsManager
AssociationsHashMap
ObjectAssociationMap
ObjcAssociation
它们之间的关系图如下:

image

根据文初示例代码 objc_setAssociatedObject(self, @selector(age),@(age), OBJC_ASSOCIATION_ASSIGN);我们给出对应结构图如下:

image

关联对象底层就是这几个对象相互配合实现的
AssociationsManager:操作所有的关联属性 和 获取关联属性 移除关联属性
AssociationsHashMap:存储这通过传递进来的对象地址作为key, ObjectAssociationMap为value的映射对象,可以存储不同对象的关联,扩展性强。
ObjectAssociationMap:传递进来的key为key, ObjcAssociation作为value
ObjcAssociation:存储这关联策略和关联的值。
下面我们来看具体的源码分析:

2.1 AssociationsManager
class AssociationsManager {
    static spinlock_t _lock;
    static AssociationsHashMap *_map;               // associative references:  object pointer -> PtrPtrHashMap. 这个_ map 里边存储的有关联列表
public:
    AssociationsManager()   { _lock.lock(); }
    ~AssociationsManager()  { _lock.unlock(); }

    AssociationsHashMap &associations() { //可以看成是只初始化一次 类似与单例
        if (_map == NULL)
            _map = new AssociationsHashMap();
        return *_map;
    }
};

AssociationsManager 是一个 C++的类 用来进行对关联对象的属性添加 和 查找 移除等操作,它里边有个 spinlock_t锁 对 _map 这个全局唯一的实例 进行加锁和解锁 ,由于懒汉模式的单例 需要在多个线程访问 _map 时候进行加锁保护

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

关联列表是一个 hashMap 类似于 OC 的 NSDictionary ,其中用 disguised_ptr_t 作为key, ObjectAssociationMap *作为一个 value disguised_ptr_tuintptr_t 的类型 intptr_tuintptr_t类型用来存放指针地址。它们提供了一种可移植且安全的方法声明指针,而且和系统中使用的指针长度相同,对于把指针转化成整数形式来说很有用。可以把disguised_ptr_t理解为一个指针类型的变量

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

ObjectAssociationMap也是一个 HashMap 存放的是 一个void * key就是关联属性时传进来的 key , ObjcAssociation 存放的关联属性策略和值的信息

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

ObjcAssociation 关联属性信息类 存放了关联策略 和 传递进来关联的值 id 类型


2.5 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.
   /// 旧的关联对象 因为关联属性时如果传 nil 可能会替换旧的关联属性 ,这就是移除某个关联属性时传 nil 的原因
    ObjcAssociation old_association(0, nil);
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        AssociationsManager manager;
       ///获取关联属性列表 ,取出来的列表是以对象为单位的 ,即某个对象的关联列表 ,这样就可以单独的关联某个对象的关联属性 而不与其他对象隔离开
        AssociationsHashMap &associations(manager.associations()); 
      /// 将要添加关联属性的对象产生一个内存地址 做 key 存储 它的关联属性
        disguised_ptr_t disguised_object = DISGUISE(object);
      /// 如果要关联的值不为空 ,不为空时 就需要判断这个属性和 key 是不是第一天添加 ,即  void *key, id value 都是第一次传递进来 
        if (new_value) {
            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; //取出旧值 后边对这个旧值进行 release 操作
                   ///将新值存放到 key 对应的字典中去 
                    j->second = ObjcAssociation(policy, new_value);
                } else { ///没有旧值直接将新值添加到字典里
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else { 
                  如果 key 对象的字典不存在 就创建一个字典 (hashMap 类似于字典的功能,本文为了方便理解将它称为字典)
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[disguised_object] = refs; 
              ///将要关联属性和策略封装到一个ObjcAssociation类里边 并根据 key 添加到这个字典里
                (*refs)[key] = ObjcAssociation(policy, new_value);
                object->setHasAssociatedObjects();
            }
        } else {
          ///如果添加关联的属性为空时 就需要取出之前关联的值 并把它擦除掉 相当于removeObjectForKey 
        ///还是根据对象内存地址找到它的关联属性列表 ,然后通过 key 找到它关联属性的实体(ObjcAssociation这个类) 最后擦除掉 相当于 free 从内存中移除
            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);
}

objc_setAssociatedObject 添加关联属性的 API

2.6 objc_getAssociatedObject
id _object_get_associative_reference(id object, void *key) {
    id value = nil;
    uintptr_t policy = OBJC_ASSOCIATION_ASSIGN; 
    {
      ///还是通过 AssociationsManager 找到所有关联对象类别 ,然后通过传入 object 找到某个对象的关联列表 ,然后通过 key 找到这个对象关联属性列表的某个实体(ObjcAssociation) 最后根据关联策略返回这个属性的值 
        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();
                ///取出关联值和策略 发送消息 类似与 [obj retain]
                if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) ((id(*)(id, SEL))objc_msgSend)(value, SEL_retain);
            }
        }
    }
   /// 如果这个对象是延时释放的类型 类似与 OC Array String 这些不是 alloc 来的对象 都要执行 [obj autorelease]来释放 
    if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
        ((id(*)(id, SEL))objc_msgSend)(value, SEL_autorelease);
    }
    return value;
}

objc_getAssociatedObject 关联对象取值的操作

2.7 objc_removeAssociatedObjects
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);
       ///如果这个对象有关联的属性列表 那么久便利它关联的属性列表 然后通过便利将这些关联内容 一个个从字典里边擦除  先擦除对象列表关联的属性列表 然后将这个对象关联属性的 hashMap 擦除掉 相当于 [dict removeAllObjects] 然后再从全局 AssociationsManager 移除 这个对象关联的字典 , 又相当于 从一个全局大字典里 把 dict这个对象的小字典 给移除了 
        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());
}

objc_removeAssociatedObjects 移除该对象所有的关联属性列表
关联对象原理就讲完了,下面我们来看它的具体应用场景

三 应用场景
3.1 给系统提供的类添加属性

通过给分类添加属性,可以衍生到,当我们遇到某个系统提供的类,我们想要给它添加一个属性,那么我们就可以创建这个类的分类,然后使用关联对象添加属性,比如我们给NSString 添加一个isEmail 属性,标示它是不是一个邮箱地址。

#import <Foundation/Foundation.h>

@interface NSString (Email)

@property(assign,nonatomic) BOOL isEmail;

@end

#import "NSString+Email.h"
#import "objc/runtime.h"

@implementation NSString (Email)

-(void)setIsEmail:(BOOL)isEmail{

    objc_setAssociatedObject(self, @selector(isEmail),@(isEmail), OBJC_ASSOCIATION_ASSIGN);
}

- (BOOL)isEmail{

   return [objc_getAssociatedObject(self, @selector(isEmail)) boolValue];
}

@end

#include<stdio.h>
#import <Foundation/Foundation.h>
#import "NSString+Email.h"
 int main()
{
    NSString *string = @"56885688@163.com";
    string.isEmail = true;
    if(string.isEmail){
        NSLog(@"这是一个邮箱地址");
    } else {
        NSLog(@"这是不是邮箱地址");
    }

}

output:
2019-02-11 16:21:35.993353+0800 test[12438:215884] 这是一个邮箱地址

3.2 关联block(关联回调,关联执行逻辑)

UIButton为例,使用关联对象完成一个功能函数:为UIButton增加一个分类,定义一个方法,使用block去实现button的点击回调

UIButton+Handle.h

#import <UIKit/UIKit.h>
#import <objc/runtime.h>    // 导入头文件

// 声明一个button点击事件的回调block
typedef void(^ButtonClickCallBack)(UIButton *button);

@interface UIButton (Handle)

// 为UIButton增加的回调方法
- (void)handleClickCallBack:(ButtonClickCallBack)callBack;

@end

UIButton+Handle.m
#import "UIButton+Handle.h"

// 声明一个静态的索引key,用于获取被关联对象的值
static char *buttonClickKey;

@implementation UIButton (Handle)

- (void)handleClickCallBack:(ButtonClickCallBack)callBack {
    // 将button的实例与回调的block通过索引key进行关联:
    objc_setAssociatedObject(self, &buttonClickKey, callBack, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    // 设置button执行的方法
    [self addTarget:self action:@selector(buttonClicked) forControlEvents:UIControlEventTouchUpInside];
}

- (void)buttonClicked {
    // 通过静态的索引key,获取被关联对象(这里就是回调的block)
    ButtonClickCallBack callBack = objc_getAssociatedObject(self, &buttonClickKey);

    if (callBack) {
        callBack(self);
    }
}

@end

viewController 里使用

[self.testButton handleClickCallBack:^(UIButton *button) {
        NSLog(@"block --- click 回调");
    }];

3.3 更多使用示例

关联对象开发中有哪些应用场景,我们不能穷尽,但是我们可以多学习别人的使用场景,怎么学习呢?很简单,github上直接搜索objc_getAssociatedObject 选中code,就有很多别人写的示例:

image

链接:https://www.jianshu.com/p/b40929723d8c

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

推荐阅读更多精彩内容