专注、坚持

iOS 中的关联对象

2019.05.18 by kingcos
Date Notes Source Code Demo
2019-05-18 首次提交 objc4-750 -

Preface

提到 iOS 中的关联对象,即 Associated Objects,又可以算是一项利用 Runtime 的「黑魔法」。然而作为初学者,很难从其名称联想到是为谁关联对象,以及是如何关联对象的,那么今天就来一起研究下 iOS 中的关联对象是什么、怎么用、以及为什么。

What

虽然这部分的官方文档已经「年久失修」,不过我们仍然简单看一下官方的概括:

Associative References

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.

—— The Objective-C Programming Language, Apple Developer

译:

关联引用

从 OS X 10.6 开始,关联引用(这一技术)被引入,用来假装为一个已存在的类添加对象实例变量。使用关联引用,不修改类的声明即可以为对象添加存储。当如果不能访问到类的源码时,或者由于二进制兼容的原因,无法修改对象布局时,这将变得十分有用。

—— Objective-C 编程语言,苹果开发者

由于 Obj-C 强大的运行时,使得其「反射」机制也异常完善。通过上述官方文档,我们也能简单地了解关联对象是为了运行时给已经存在的类动态添加对象类型的成员变量。

How

类与分类(Category)的属性

在类(Class)中定义一个属性(@property),默认会生成 getter & setter,以及 _ 开头的成员变量:

// Person.h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end
NS_ASSUME_NONNULL_END

// Person.m
#import "Person.h"

@implementation Person
@end

// main.m
Person *person = [[Person alloc] init];
[person setName:@"kingcos"];
NSLog(@"Name: %@", [person name]);
// OUTPUT:
// Name: kingcos

// LLDB:
// (lldb) po person->_name
// kingcos

而在「iOS 中的 Category」一文中,我们介绍了分类中也是可以添加属性的,因为分类的结构 category_t 中也有存储属性列表的指针:

struct _category_t {
    // ...
    const struct _prop_list_t *properties;          // 属性列表指针
};

我们也尝试为「Person+Life」分类添加一个属性并使用:

// Person+Life.h
@interface Person (Life)
@property (nonatomic, copy) NSString *nickname;
@end

// Person+Life.m
#import "Person+Life.h"

@implementation Person (Life)
@end

// main.m
Person *person = [[Person alloc] init];
[person setNickname:@"kingcos"];
NSLog(@"Name: %@", [person nickname]);
// Crash:
// '-[Person setNickname:]: unrecognized selector sent to instance 0x10050fdf0'
// '-[Person nickname]: unrecognized selector sent to instance 0x102185520'

// LLDB:
// (lldb) po person->_nickname
// error: 'Person' does not have a member named '_nickname'

然而我们发现程序在这里崩溃了,这是因为分类中虽然可以定义属性,但其只会为我们声明 getter & setter,并不会创建实例变量,也不会实现 getter & setter,因为分类的结构 category_t 中也确实没有存储成员变量的地方。那么这时我们使用关联对象就可以为分类「假装」添加成员变量。

关联对象的使用

由于关联对象是 Obj-C 运行时的特性之一,因此在使用相关 API 前需要引入 #import <objc/runtime.h>。其中与关联对象相关的函数主要为以下三个:

// runtime.h
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
                         id _Nullable value, objc_AssociationPolicy policy)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);

objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);

objc_removeAssociatedObjects(id _Nonnull object)
    OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);

这三个函数从名称也能认出其分别为设置关联对象、获取关联对象、以及移除关联对象;即我们可以在 setter 的实现中设置关联对象,在 getter 中获取关联对象,在不再需要时移除关联对象。尝试将上一节崩溃的程序使用关联对象进行改写:

// Person+Life.m
#import "Person+Life.h"
#import <objc/runtime.h>

@implementation Person (Life)

static const void *nicknameKey = &nicknameKey;

- (NSString *)nickname {
    return objc_getAssociatedObject(self, nicknameKey);
}

- (void)setNickname:(NSString *)nickname {
    objc_setAssociatedObject(self, nicknameKey, nickname, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

@end

// main.m
Person *person = [[Person alloc] init];
[person setNickname:@"kingcos"];
// OUTPUT:
// Name: kingcos

我们在 setter 中使用 objc_setAssociatedObject 实现,其一共需要四个参数:

  • 第一个参数是将要关联到的对象 id _Nonnull object;我们这里要被关联的即是当前的实例对象 self,当然,类对象也可以被关联。

  • 第二个参数是标示存入的 const void * _Nonnull key,可以放入指向任何类型的指针。我们可以仿照「iOS 中的 KVO」中对于 context 的定义,将 key 设置为声明变量自身的内存地址,保证了唯一性,并加上 static,保证该变量仅在该 .m 中可以访问。但这样不太方便的是每次都需要额外定义,有更好的解决方案吗?我们其实可以将 getter 或者 setter 方法的地址作为 key 存入,这样就无需再去单独声明一个 key 且支持编译检查。再进一步,由于 Obj-C 中的方法都会有两个隐式参数 self_cmd 分别代表当前对象和当前方法 SEL,后者在 getter 中就等同于的 @selector(nickname),这样在 getter 中写法就更简单了:

@implementation Person (Life)

- (NSString *)nickname {
    // return objc_getAssociatedObject(self, @selector(nickname));
    return objc_getAssociatedObject(self, _cmd);
}

- (void)setNickname:(NSString *)nickname {
    objc_setAssociatedObject(self, @selector(nickname), nickname, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

@end
  • 第三个参数是要关联上的值 id _Nullable value,注意其类型为 id,因此如果想要存入 int 等 C 语言中的类型时,需要先在 setter 中转换为 Obj-C 的对象类型,并在 getter 中再转回:
@implementation Person (Life)

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

- (void)setAge:(int)age {
    objc_setAssociatedObject(self, @selector(age), @(age), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end
  • 第四个参数是关联的内存语义策略 objc_AssociationPolicy,其中包含了五个枚举项,对应了不同的内存管理策略。可以对照属性的内存管理修饰符来选择合适的策略:
objc_AssociationPolicy 注释 @property
OBJC_ASSOCIATION_ASSIGN 弱引用 assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC 强引用,非原子性 strong(retain), nonatomic
OBJC_ASSOCIATION_COPY_NONATOMIC 拷贝,非原子性 copy, nonatomic
OBJC_ASSOCIATION_RETAIN 强引用,原子性 strong(retain), atomic
OBJC_ASSOCIATION_COPY 拷贝,原子性 copy, atomic

Why

了解了关联对象是什么、怎么用,那么关联对象到底是如何实现的呢?我们先从 objc_setAssociatedObject 着手:

// objc-runtime.mm
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
    // ➡️ 内部实际调用 _object_set_associative_reference
    _object_set_associative_reference(object, (void *)key, value, policy);
}

// objc-references.mm
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;
        // 从关联管理器中取出所有关联,HashMap 数据结构
        AssociationsHashMap &associations(manager.associations());
        // 「伪装」要关联上的对象
        disguised_ptr_t disguised_object = DISGUISE(object);
        if (new_value) {
            // 如果有新值
            // break any existing association.
            // 打破任何现有的关联。
            // 在管理器的所有关联中根据 object 查找其关联,并放入迭代器
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i != associations.end()) {
                // 如果已存在该对象的关联
                // secondary table exists
                // 存在二级表
                ObjectAssociationMap *refs = i->second;
                // 在 object 的所有关联中根据 key 查找对应的某个关联,并放入迭代器
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    // 如果已存在该 key 的关联
                    // 原始关联暂存
                    old_association = j->second;
                    // 用新关联的值和内存管理策略构造对象关联并赋值
                    j->second = ObjcAssociation(policy, new_value);
                } else {
                    // 如果未存在该 key 的关联
                    // 用新关联的值和内存管理策略构造对象关联并赋值
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else {
                // 如果未存在该对象的关联
                // create the new association (first time).
                // 创建新关联(首次)
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                // 将新创建 ObjectAssociationMap 存入所有关联中
                associations[disguised_object] = refs;
                // 用新关联的值和内存管理策略构造对象关联并赋值
                (*refs)[key] = ObjcAssociation(policy, new_value);
                object->setHasAssociatedObjects();
            }
        } else {
            // 如果值为 nil
            // setting the association to nil breaks the association.
            // 设置关系为 nil 来打破关联
            // 在管理器里所有的关联中根据 object 查找其关联,并放入迭代器
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i !=  associations.end()) {
                // 如果已存在该对象的关联
                ObjectAssociationMap *refs = i->second;
                // 在 object 的所有关联中根据 key 查找对应的某个关联,并放入迭代器
                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-references.mm
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; }
};

// objc-references.mm
class AssociationsManager {
    // associative references: object pointer -> PtrPtrHashMap.
    static AssociationsHashMap *_map;
public:
    AssociationsManager()   { AssociationsManagerLock.lock(); }
    ~AssociationsManager()  { AssociationsManagerLock.unlock(); }

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

// objc-references.mm
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); }
};

// objc-references.mm
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); }
};

为了更加直观表述它们的关系,整理成图即:

1

同理我们可以分析关联对象的其它两个方法:

// --- objc_getAssociatedObject ---

// objc-runtime.mm
id objc_getAssociatedObject(id object, const void *key) {
    return _object_get_associative_reference(object, (void *)key);
}

// objc-references.mm
id _object_get_associative_reference(id object, void *key) {
    // 默认值 nil
    id value = nil;
    // 默认策略 OBJC_ASSOCIATION_ASSIGN
    uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
    {
        // 声明关联管理器
        AssociationsManager manager;
        // 从关联管理器中取出所有关联
        AssociationsHashMap &associations(manager.associations());
        // 「伪装」要关联上的对象
        disguised_ptr_t disguised_object = DISGUISE(object);
        // 在管理器的所有关联中根据 object 查找其关联,并放入迭代器
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i != associations.end()) {
            // 如果存在该对象的关联
            ObjectAssociationMap *refs = i->second;
            // 在 object 的所有关联中根据 key 查找对应的某个关联,并放入迭代器
            ObjectAssociationMap::iterator j = refs->find(key);
            if (j != refs->end()) {
                // 如果存在该 key 的关联
                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;
}

// --- objc_removeAssociatedObjects ---

// objc-runtime.mm
void objc_removeAssociatedObjects(id object)
{
    if (object && object->hasAssociatedObjects()) {
        _object_remove_assocations(object);
    }
}

// objc-references.mm
void _object_remove_assocations(id object) {
    // 声明存储 ObjcAssociation 的 elements
    vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > elements;
    {
        // 声明关联管理器
        AssociationsManager manager;
        // 从关联管理器中取出所有关联
        AssociationsHashMap &associations(manager.associations());
        // 如果关联为空,则返回
        if (associations.size() == 0) return;
        // 「伪装」要关联上的对象
        disguised_ptr_t disguised_object = DISGUISE(object);
        // 在管理器的所有关联中根据 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.
    // releaseValue() 的调用发生在锁之外
    for_each(elements.begin(), elements.end(), ReleaseValue());
}

Future

  • 如果被关联的对象销毁了,关联对象会被销毁吗?
  • 关联对象是否线程安全?
  • setHasAssociatedObjects

Reference