iOS Objective-C 关联对象

iOS Objective-C 关联对象

1. 关联对象简介

对于关联对象,我们熟悉它的地方就是给分类添加属性。虽然我们可以在分类中通过@property编写代码来声明一个属性,但是当我们使用的时候就报方法找不到错误,其实缺失的方法就是属性的gettersetter的实现,那么关联对象就可以完美的解决这个问题。

官方定义:

Associative references, available starting in OS X v10.6, simulate the addition of object instance variables to an existing class. Using associative references, you can add storage to an object without modifying the class declaration. This may be useful if you do not have access to the source code for the class, or if for binary-compatibility reasons you cannot alter the layout of the object.

Associations are based on a key. For any object you can add as many associations as you want, each using a different key. An association can also ensure that the associated object remains valid for at least the lifetime of the source object.

译: 从OS X v10.6开始可用的关联引用模拟了将对象实例变量添加到现有类中。使用关联引用,可以在不修改类声明的情况下将存储添加到对象。如果您无权访问该类的源代码,或者由于二进制兼容性原因而无法更改对象的布局,则这可能很有用。

关联基于key。对于任何对象,您都可以根据需要添加任意数量的关联,每个关联都使用不同的key。关联还可以确保关联的对象至少在源对象的生存期内保持有效。

通过苹果官方文档我们可以知道,关联引用不仅仅可以用在给分类添加属性。但是给分类添加属性是我们最常用的场景。

关联对象的两个函数

  • 设置关联对象
void
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
    SetAssocHook.get()(object, key, value, policy);
}
  1. 参数一:id object : 要关联的对象
  2. 参数二:const void *key : 关联使用的key值
  3. 参数三:id value : 关联的值,也就是我们要设置的值。
  4. 参数四:objc_AssociationPolicy policy : 策略属性,以什么形式保存
  • 获取关联对象
id
objc_getAssociatedObject(id object, const void *key)
{
    return _object_get_associative_reference(object, key);
}
  1. 参数一:id object : 获取哪个对象里面的关联的值
  2. 参数二:const void *key : 关联使用的key值,通过这个key取出对应的值

关联使用的策略:

/**
 * Policies related to associative references.
 * These are options to objc_setAssociatedObject()
 */
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
    OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied. 
                                            *   The association is not made atomically. */
    OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                            *   The association is made atomically. */
    OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                            *   The association is made atomically. */
};
策略 对应 @property 描述
OBJC_ASSOCIATION_ASSIGN (assign)或者(unsafe_unretained) 指定一个关联对象的弱引用
OBJC_ASSOCIATION_RETAIN_NONATOMIC (nonatomic, strong) 不能原子化的强引用
OBJC_ASSOCIATION_COPY_NONATOMIC (nonatomic, copy) copy引用,不能原子化
OBJC_ASSOCIATION_RETAIN (atomic, strong) 原子化的强引用
OBJC_ASSOCIATION_COPY (atomic, copy) 原子化的copy引用

2. 关联对象的应用

举个例子,说了半天关联对象可以为分类添加属性,那么我们就把这个例子写一下。

@interface CTObject (Category)

@property (nonatomic, copy) NSString *cate_p1;

@end
@implementation CTObject (Category)
- (void)setCate_p1:(NSString *)cate_p1{
    
    objc_setAssociatedObject(self, @"cate_p1",cate_p1, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)cate_p1{
    
    return  objc_getAssociatedObject(self, @"cate_p1");
}
@end

3. 关联对象的底层原理

上面两节对关联对象做了简单的介绍和其使用的举例,下面我们来研究一下它的底层实现。

3.1 objc_setAssociatedObject

我们先看看objc_setAssociatedObject的源码,由于使用了各种C++的语法和嵌套,嵌套过程就不多说了,以下是嵌套的代码:

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

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

static ChainedHookFunction<objc_hook_setAssociatedObject> SetAssocHook{_base_objc_setAssociatedObject};

由以上代码我们可以知道objc_setAssociatedObject实际调用的是_object_set_associative_reference函数,下面我们就来到_object_set_associative_reference看看它究竟是如何实现的。

_object_set_associative_reference 源码:

void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
    // This code used to work when nil was passed for object and key. Some code
    // probably relies on that to not crash. Check and handle it explicitly.
    // rdar://problem/44094390
    if (!object && !value) return;

    if (object->getIsa()->forbidsAssociatedObjects())
        _objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));

    DisguisedPtr<objc_object> disguised{(objc_object *)object};
    ObjcAssociation association{policy, value};

    // retain the new value (if any) outside the lock.
    association.acquireValue();

    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.get());

        if (value) {
            auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
            if (refs_result.second) {
                /* it's the first association we make */
                object->setHasAssociatedObjects();
            }

            /* establish or replace the association */
            auto &refs = refs_result.first->second;
            auto result = refs.try_emplace(key, std::move(association));
            if (!result.second) {
                association.swap(result.first->second);
            }
        } else {
            auto refs_it = associations.find(disguised);
            if (refs_it != associations.end()) {
                auto &refs = refs_it->second;
                auto it = refs.find(key);
                if (it != refs.end()) {
                    association.swap(it->second);
                    refs.erase(it);
                    if (refs.size() == 0) {
                        associations.erase(refs_it);

                    }
                }
            }
        }
    }

    // release the old value (outside of the lock).
    association.releaseHeldValue();
}

acquireValue 源码:

inline void acquireValue() {
        if (_value) {
            switch (_policy & 0xFF) {
            case OBJC_ASSOCIATION_SETTER_RETAIN:
                _value = objc_retain(_value);
                break;
            case OBJC_ASSOCIATION_SETTER_COPY:
                _value = ((id(*)(id, SEL))objc_msgSend)(_value, @selector(copy));
                break;
            }
        }
    }
  • 首先还是做一些非空判断防止一些空对象空值可能会引起的崩溃
  • 判断类是否禁用了关联引用,如果是就打印错误信息
  • 初始化一个disguised对象,是对object按位取反
  • 初始化一个ObjcAssociation对象用于持有关联对象
  • 通过acquireValue函数给我们的value返回一个新值,acquireValue源码在上面,主要是根据策略进行不同的处理
  • 接下来就是初始化一个AssociationsManager对象,获取一个AssociationsHashMap哈希表
  • 接下来分两个流程一个是值存在此时是赋值
    • 首先获取到类的关联表
    • 如果没获取到说明我们是第一次给该类关联,所以需要创建一个新的表
    • 接下来获取表的首地址,并判断对应的key是否已经存在,不存在就直接写入
    • 存在就用新值替换旧值
  • 第二种是值为空,此时是删除关联对象
    • 首先是获取到该类对应的哈希表
    • 判断表不为空
    • 找到key对应的节点
    • 节点不为空判断
    • 替换节点值为空
    • 清空节点
    • 清空节点后,如果表也为空,则清空表
  • 最后释放旧值

3.2 objc_getAssociatedObject

objc_getAssociatedObject就没有那么多嵌套了,直接就可以看出是调用的_object_get_associative_reference函数。

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

_object_get_associative_reference 源码:

id
_object_get_associative_reference(id object, const void *key)
{
    ObjcAssociation association{};

    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.get());
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        if (i != associations.end()) {
            ObjectAssociationMap &refs = i->second;
            ObjectAssociationMap::iterator j = refs.find(key);
            if (j != refs.end()) {
                association = j->second;
                association.retainReturnedValue();
            }
        }
    }

    return association.autoreleaseReturnedValue();
}

_object_get_associative_reference的实现也很简单:

  • 首先还是初始化一个ObjcAssociation对象,AssociationsManager对象,获取AssociationsHashMap哈希表
  • 获取到当前对象的关联表
  • 如果表不为空则通过key在表中查找数据
  • 如果找到了并且不为空则调用retainReturnedValue函数根据策略赋值
  • 最后返回通过autoreleaseReturnedValue函数根据策略处理的值

3.3 objc_removeAssociatedObjects

对于关联对象其实还有一个函数objc_removeAssociatedObjects,只不过我们基本不用他,根据名字我们就可以知道该函数是移除关联对象的。这里也嵌套了一层代码,最终调用的是_object_remove_assocations

objc_removeAssociatedObjects 源码:

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

_object_remove_assocations 源码:

// Unlike setting/getting an associated reference,
// this function is performance sensitive because of
// raw isa objects (such as OS Objects) that can't track
// whether they have associated objects.
void
_object_remove_assocations(id object)
{
    ObjectAssociationMap refs{};

    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.get());
        AssociationsHashMap::iterator i = associations.find((objc_object *)object);
        if (i != associations.end()) {
            refs.swap(i->second);
            associations.erase(i);
        }
    }

    // release everything (outside of the lock).
    for (auto &i: refs) {
        i.second.releaseHeldValue();
    }
}

根据注释我们可以知道_object_remove_assocations函数是会对性能有影响的。

  • 这里还是要初始化ObjectAssociationMap对象,AssociationsManager对象,AssociationsHashMap对象。
  • 找出要释放对象的关联表
  • 判断不为空,则将移除这些关联关系,并释放
  • 最后循环释放类的所有关联表

4. 总结

  • 关联对象实际上在底层是一个ObjcAssociation对象结构
  • 全局由一个AssociationsManager管理类存储了一个静态的哈希表AssociationsHashMap
  • 这个哈希表存储的是以对象指针为键,以该对象所有关联对象为值
  • 关联对象又是以ObjectAssociationMap来存储的
  • ObjectAssociationMap的存储结构以key为键,ObjcAssociation为值
  • 判断一个对象是否存在关联对象可以通过对象isahas_assoc

至此我们的关联对象就基本分析完毕了,但是由于本人才疏学浅,有些地方用词不当,一些C++语法不是很熟悉,有些表述不完整,不贴切,但是我也想不出什么好词的,可能也会有些不准确。如有问题欢迎指正。

5. 参考资料

**Apple Associative References
**

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