Objective-C(十二)关联对象

1,196 阅读7分钟

本文是Objective-C系列的第12篇,主要讲述了关联对象的底层结构和使用。

一、为什么需要关联对象?

1. Category能添加成员变量吗?

我们在Objective-C(十)Category中讲过,Category能添加协议、方法、属性等,参考下面的结构体。

//from objc-runtime-new.h
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;
    ....
};

既然可以添加属性,那是不是可以添加成员变量呢?

答案是:不可以

如下面的实例,调用后,Crash

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[BFPerson setAge:]: unrecognized selector sent to instance 0x60000202c4b0'

//分类声明
@interface BFPerson (BFBoy)
@property (nonatomic, assign) NSInteger age;
@end
    
//调用
BFPerson *person = [[BFPerson alloc] init];
person.age = 28;

所以,我们得知:

Category可以添加属性,但不会添加对应的成员变量,也不会实现对应的setter、getter方法。

2. 如何实现给Category添加成员变量?

实例源码参考01-Category成员变量

既然无法直接给分类中添加,我们可以通过全局变量的方式来保存成员变量的值,且实现对应的setter、getter来模拟为Category完整添加属性(成员变量)的效果。

@interface BFPerson (BFBoy)
@property (nonatomic, assign) NSInteger age;
@end

@implementation BFPerson (BFBoy)
NSMutableDictionary *ages_;
+ (void)load
{
    ages_ = [[NSMutableDictionary alloc] init];
}
- (void)setAge:(NSInteger)age
{
    NSString *key = [NSString stringWithFormat:@"%p", self];
    ages_[key] = @(age);
}
- (NSInteger)age
{
    NSString *key = [NSString stringWithFormat:@"%p", self];
    return [ages_[key] integerValue];
}
@end

上述方案有一些缺点:

  • 每添加一个属性,就需要创建一个字典来保存对应成员变量
    • 抑或共用一个字典,但必须要保证不同属性对应不同的key;
  • 实现繁琐。

3. 什么是关联对象?

关联是指把两个对象相互关联起来,使得其中的一个对象作为另外一个对象的一部分。 关联特性只有在Mac OS X V10.6、iOSV3.1以及以后的版本上才是可用的。

使用关联,我们可以不用修改类的定义而为其对象增加存储空间。这在我们无法访问到类的源码的时候或者是考虑到二进制兼容性的时候是非常有用。

关联是基于关键字的,因此,我们可以为任何对象增加任意多的关联,每个都使用不同的关键字即可。关联是可以保证被关联的对象在关联对象的整个生命周期都是可用的(在垃圾自动回收环境下也不会导致资源不可回收)。

二、关联对象的使用

示例代码02-关联对象

1. 关联对象API

image-20181202190626994

2. key

  • 关键字是一个void类型的指针,必须唯一

  • 常用静态变量来作为key。通常推荐key使用static char类型——使用指针或许更好,并只在getter和setter方法内部使用。

  • 更简单的方案是:直接使用选择器(selector),因为SEL生成的时候就是一个唯一的常量。

image-20181202191224174

3. policy

  • policy表明了value内存语义,是通过赋值,保留引用还是复制的方式进行关联的;

  • policy还表明原子的还是非原子的。

这里的关联策略和声明属性时的很类似。这种关联策略是通过使用预先定义好的常量来表示的。

associated_object_policy

4. 应用

在本文开始就提出,关联对象作为给Category添加成员变量是一种高效可行的方案

但是,我们仍然需要提醒你

  • 关联对象应该被当做最后的手段来使用(不得不用时才用),而不是为了寻求一个解决方案就行。
  • category本身就不应该是解决问题优先选择的工具

说到这里,我们在开发中常常看到一种,或者经历过一种现象——某个开发者,在博客或其他方式中得知某一个技术点,这个技术点具备一些特定——是巧妙的伎俩、hack手段或者是变通的解决方案,开发者这时候跃跃欲试,为了用而用,在不完全吃透该技术点或原理时,就贸然使用。

我给出的建议就是,**吃透原理,分析场景,**这些小伎俩再用到平时开发中。

下面给出了一些其他开源库中用到的场景:

4.1 类增加状态

添加私有变量来帮助实现细节 。当拓展一个内置类时,可能有必要跟踪一些额外的状态,这是关联对象最普遍的应用场景。

例如:AFNetworking中在UIImageView的分类中使用关联对象来存储一个请求操作对象(operation object),用于异步的从远程获取图片。

4.2 解耦

使用关联对象来代替X,其中X代表下面的一些项:

  • 子类化,当使用继承比使用组合更合适的时候。
  • target-action给响应者添加交互事件。
    • 按钮
    • 手势识别,当target-action模式不够用的时候。
  • 代理,当事件可以委托给其他对象。
  • 消息 & 消息中心使用低耦合的方式来广播消息。

三、原理

1.核心对象

对应的API为:

void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) 

1.1 AssociationsManager

全局关联对象管理类,其拥有一个map字典用于存放所有被关联对象object的关联对象。

 class AssociationsManager {
    static AssociationsHashMap *_map;

    AssociationsHashMap &associations() {
        if (_map == NULL)
        _map = new AssociationsHashMap();
        return *_map;
    }
 };

1.2 AssociationsHashMap

AssociationsHashMap就是存放一个被关联对象object的所有管理对象的字典,根据下面的类定义:

class AssociationsHashMap : public unordered_map<disguised_ptr_t, ObjectAssociationMap *, DisguisedPointerHash, DisguisedPointerEqual, AssociationsHashMapAllocator> {
    };
  • 继承于unordered_map,后面三个参数,分别是hashequalallocator函数的实现。

其中该字典对应的:

  • key:disguised_ptr_t类型
  • value:ObjectAssociationMap类型

1.3 ObjectAssociationMap

ObjectAssociationMap是真正存储object中关联对象的载体。

class ObjectAssociationMap : public std::map<void *, ObjcAssociation, ObjectPointerLess, ObjectAssociationMapAllocator> {
};

其中该字典:

  • key:void *指针类型,即传进来的key。
  • value:ObjcAssociation类型,该类型存储了传进来的valuepolicy

1.4 ObjcAssociation

ObjcAssociation用于存放关联对象的valuepolicy

class ObjcAssociation {
    uintptr_t _policy;
    id _value;
};

2. 关联对象的结构

image-20181202214605229

3.核心流程

3.1 设置关联对象

  • object不能为nil
  • value可为nil,nil表示清除该关联对象
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
  if (new_value) {
        // break any existing association.
        // 1.value有值
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i != associations.end()) {
            // 1.1.1 找到该object对应的ObjectAssociationMap
            ObjectAssociationMap *refs = i->second;
            // 1.1.2 在ObjectAssociationMap寻找key对应的ObjcAssociation
            ObjectAssociationMap::iterator j = refs->find(key);
            if (j != refs->end()) {
                // 1.1.2.1 找到ObjcAssociation,将旧值保存在old_association
                // 1.1.2.2 重新设新值
                old_association = j->second;
                j->second = ObjcAssociation(policy, new_value);
            } else {
                // 1.1.2.3 未找到对应ObjcAssociation,直接设新值
                (*refs)[key] = ObjcAssociation(policy, new_value);
            }
        } else {
            // 1.2.1 未找到ObjectAssociationMap,即之前object未设置过关联对象
            // ObjectAssociationMap新增,ObjcAssociation新增
            ObjectAssociationMap *refs = new ObjectAssociationMap;
            associations[disguised_object] = refs;
            (*refs)[key] = ObjcAssociation(policy, new_value);
            object->setHasAssociatedObjects();
        }
    } else {
        // 2. value为空
        // 2.1 寻找object对应的ObjectAssociationMap
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i !=  associations.end()) {
            ObjectAssociationMap *refs = i->second;
            // 2.1.1 寻找key对应的ObjcAssociation
            ObjectAssociationMap::iterator j = refs->find(key);
            if (j != refs->end()) {
                // 2.2.1.1 找到ObjcAssociation,将旧值保存在old_association
                // 2.2.1.2 重新设新值
                old_association = j->second;
                refs->erase(j);
            }
        }
    }
}

3.2 获取关联对象

id _object_get_associative_reference(id object, void *key) {
    id value = nil;
    uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
    {
        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();
                if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) {
                    objc_retain(value);
                }
            }
        }
    }
    if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
        objc_autorelease(value);
    }
    return value;
}

3.3 移除关联对象

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

参考

链接

  1. objc源码
  2. 示例代码01-Category成员变量
  3. 示例代码02-关联对象