前言
关联对象是用来为分类添加成员变量时使用的,那么为什么分类需要使用关联对象来添加成员变量呢?那肯定是因为正常的添加成员变量的方式在分类中不能用。
一般我们在类中声明一个属性,代码是这样的:
@interface Animal: NSObject
@property (nonatomic, copy) NSString *name;
@end
然后编译器会帮我们生成如下方法:
@implementation Animal {
NSString *_name;
}
- (NSString *)name {
return _name;
}
- (void)setName:(NSString *)name {
_name = name;
}
就是:
- 生成一个实例变量
_name - 生成
getter方法 - 生成
setter方法
编译器帮我们生成了一个实例变量,然后存到在类结构中。
当我们尝试往一个分类中去添加一个属性:
@interface Animal (Category)
@property (nonatomic, copy) NSString *gender;
@end
我们会得到以下警告:
Property 'gender' requires method 'gender' to be defined - use @dynamic or provide a method implementation in this category
意思就是 gender 属性的存取方法需要自己去手动实现,或者使用 @dynamic 在运行时实现这些方法。
因为在分类中,虽然可以通过 @property 来添加属性,但是不会自动生成私有成员变量,也不会生成 set 和 get 方法,只会生成 set、get 的声明,具体的方法实现需要我们自己去实现。
从 Category 讲起
至于为什么不自动给分类生成私有成员变量,最直接的原因就是分类不支持,先来看看类的结构,它在源码中是一个名为 objc_class 的结构体:
struct objc_class {
Class _Nonnull isa;
struct objc_ivar_list * _Nullable ivars; // 成员变量列表
struct objc_method_list * _Nullable * _Nullable methodLists; // 方法列表
struct objc_protocol_list * _Nullable protocols; // 协议列表
...
}
其中有三个成员:
ivars:成员变量列表methodLists:方法列表protocols:协议列表
分类在源码中的结构如下,它是一个名为 category_t 的结构体:
struct category_t {
const char *name; // 对应的类名
WrappedPtr<method_list_t, method_list_t::Ptrauth> instanceMethods; // 实例方法列表
WrappedPtr<method_list_t, method_list_t::Ptrauth> classMethods; // 类方法列表
struct protocol_list_t *protocols; // 协议列表
struct property_list_t *instanceProperties; // 属性列表
...
}
它包含:
instanceMethods:实例方法列表protocols:协议列表instanceProperties:属性列表
可以看到它是没有成员变量列表的,这就是为什么分类不允许添加成员变量的最直接的原因,从结构设计上就不支持。
为什么要这么设计呢?
其实得问苹果为什么要这么设计,我个人觉得设计成可以添加成员变量好像也没毛病?虽然我没想出来是为啥,不过还是放一份别人的答案,看懂的同学可以指点下:
总得来说就是,
Category是在运行时才会被运行时库(也就是Runtime)加载到内存中,而类的内存布局在编译时就已经确定了,不可以再更改。所以不允许添加成员变量,是因为添加成员变量会影响到 "类实例" 的内存布局,所谓 "类实例",它就是我们创建出来的类对象,它是一块包含
isa指针和所有的成员变量的内存区域,我们不能在运行时再去改变它,而成员变量的增加会直接影响到它的内存的,所以不允许在运行时再添加成员变量。而方法/协议/属性不属于 "类实例" 这个概念,它们归类管,也就是
objc_class,不管如何增删,都不会影响到 "类实例" 的内存,所以可以随意增删。
具体的原因我还没想明白,但是不管咋说,从源码上看,苹果它就不支持你去在分类中添加成员变量,如果我们想达到向正常类那样去使用一个属性(会自动生成实例变量和 set/get 方法),那么我们可以借助关联对象(主角出场的有点晚)。
当然,关联对象和正常的成员变量在底层是大不一样的,不过使用关联对象实现的属性和正常的属性在使用上并无二致。
使用关联对象
在 Animal 的分类中添加一个 age 属性:
#import "objc/runtime.h"
@interface Animal (Category)
@property (nonatomic, copy) NSString *categoryName;
@end
@implementation Animal (Category)
- (void)setCategoryName:(NSString *)categoryName {
objc_setAssociatedObject(self, @"categoryName", categoryName, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)categoryName {
return objc_getAssociatedObject(self, @"categoryName");
}
@end
使用时:
Animal *animal = [[Animal alloc] init];
animal.categoryName = @"Tom";
NSLog(@"%@", animal.categoryName);
控制台输出:
Tom
就像正常属性一样进行使用即可。
关联对象的实现原理
通过上面的例子,可以看到两个关键的 api,objc_setAssociatedObject 和 objc_getAssociatedObject,一个是设置,一个是获取,我们到源码中看看它们做了什么。
void
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
_object_set_associative_reference(object, key, value, policy);
}
老样子,又是调了另一个方法:
void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
DisguisedPtr<objc_object> disguised{(objc_object *)object}; // (1)
ObjcAssociation association{policy, value}; // (2)
AssociationsManager manager; (3)
AssociationsHashMap &associations(manager.get()); (4)
auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
auto &refs = refs_result.first->second;
auto result = refs.try_emplace(key, std::move(association));
...
// 释放旧值
association.releaseHeldValue();
}
这里省略了部分代码,我们需要注意里面的几个类和数据结构,在具体分析代码之前,需要先了解它们的作用。
- DisguisedPtr
- ObjcAssociation
- AssociationsManager
- AssociationsHashMap
- ObjectAssociationMap
DisguisedPtr 是一个 class:
class DisguisedPtr {
uintptr_t value;
...
}
它只有一个成员,通过 (1) 我们可以看到,它传入的是 object,对应的就是 objc_setAssociatedObject(self, @"categoryName", categoryName, OBJC_ASSOCIATION_COPY_NONATOMIC); 中的 self。
也就是将 self 放到了一个类中。
再看 ObjcAssociation:
class ObjcAssociation {
uintptr_t _policy;
id _value;
...
}
ObjcAssociation 只有两个成员,_policy 是 objc_AssociationPolicy,它是一个枚举:
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
};
而 value 就是关联对象对应的值。
和 DisguisedPtr 类似,并且通过 (2) 可以看出,是将关联对象的 OBJC_ASSOCIATION_COPY_NONATOMIC 和关联对象的 value,也就是 categoryName 一起放入了这个对象。
接下来就是 AssociationsManager:
class AssociationsManager {
using Storage = ExplicitInitDenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap>;
static Storage _mapStorage;
public:
AssociationsManager() { AssociationsManagerLock.lock(); }
~AssociationsManager() { AssociationsManagerLock.unlock(); }
AssociationsHashMap &get() {
return _mapStorage.get();
}
static void init() {
_mapStorage.init();
}
};
其中的 AssociationsManagerLock 是一个自旋锁:
spinlock_t AssociationsManagerLock;
所以它有一个 spinlock_t(自旋锁)和 AssociationsHashMap 单例,在 &get 方法中返回的是一个全局的 AssociationsHashMap 单例,然后 AssociationsManager 通过持有一个 spinlock_t 来保证对 AssociationsHashMap 的操作是线程安全的。
(4) 中就是通过 &get 方法,拿到了 AssociationsHashMap。
AssociationsHashMap 的定义为:
typedef DenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap> AssociationsHashMap;
是一个 [DisguisedPtr : ObjectAssociationMap] 的字典,ObjectAssociationMap 的定义如下:
typedef DenseMap<const void *, ObjcAssociation> ObjectAssociationMap;
是一个 [void * : ObjcAssociation] 的字典,而 ObjcAssociation 我们前面提到过,它存放了关联对象的修饰符和关联对象的值,也可以说它就是我们所说的 "关联对象" 实际在内存中的结构。
看到这里,我们可以得到关联对象的一个存储结构,以我们在 Animal 中的 categoryName 为例,如果将 categoryName 设置为 Tom,它在内存中是这么存储的:
所以关联对象是通过一个全局的单例类来管理的,存储的结构也是一个哈希表的结构,所以我们可以想象出来,当需要一个对象添加了关联对象,它会以线程安全的方式(加锁)存储到一个全局的表中,key 是对象的 id(地址),而 value 是关联对象的修饰符和值。
那么释放的时机我们也很容易推断出来,只需要在对象的析构方法中,去这个全局的表中以对象的 id 为 key 移除掉与它相关的关联对象即可。
从 dealloc 方法中去找:
dealloc -> rootDealloc -> object_dispose -> objc_destructInstance -> _object_remove_assocations,_object_remove_assocations 就是释放关联对象的方法,定义如下:
void
_object_remove_assocations(id object, bool deallocating)
{
...
}
可以看到传入了析构对象本身(object),然后再根据 object 去全局的表中查找该 object 所对应的值,然后移除即可。
源码
分析完原理之后,我们来看一下 objc_setAssociatedObject 和 objc_getAssociatedObject 两个方法的具体实现。
void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
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};
association.acquireValue();
bool isFirstAssociation = false;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.get());
if (value) { // a
auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
if (refs_result.second) {
isFirstAssociation = true;
}
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);
}
}
}
}
}
if (isFirstAssociation)
object->setHasAssociatedObjects();
association.releaseHeldValue();
}
提个注意点就是 // a 处,分两种情况:
value != nil,设置/更新关联对象的值value == nil,删除关联对象
将关联对象的值设置为 nil 的话,会去删除这个关联对象。
objc_getAssociatedObject 方法内部调用了 _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();
}
只放了源码,没有具体到每一条的分析,个人觉得看个大概,理解原理就可以了。
总结
- 关联对象在源码中其实就是
ObjcAssociation对象。 ObjcAssociation存放在ObjectAssociationMap中,然后以对象的指针为key,ObjectAssociationMap为value,存放在AssociationsHashMap中。AssociationsHashMap是一个哈希表,由AssociationsManager管理,AssociationsManager是一个全局的单例,持有AssociationsHashMap,AssociationsHashMap也是全局唯一的一张表。- 对象在析构函数中,通过
has_assoc标记位判断对象是否有关联对象,有的话会调用_object_remove_assocations方法移除相关关联对象。