我们在 iOS 开发中经常需要使用分类(Category),为已经存在的类添加属性的需求,但是使用 @property 并不能在分类中正确创建实例变量和存取方法。但是,通过OC的runtime关联对象,也就是Associated Object,可以实现上述需求.
使用关联对象为已经存在的类中添加属性.
关联对象的应用,分类中的@property
@property可以说是一个OC编程中的”宏”.
@interface MHFObject : NSObject
@property (nonatomic, strong) NSString *property;
@end
使用上述代码时会做三件事情:
- 生成实例变量 _property
- 生成getter方法 - property
生成setter方法 - setProperty;
“`
@implementation MHFObject
{
NSString *_property;
}(NSString *)property{
return _property;
}(void)setProperty:(NSString *)property{
_property = property;
}
@end“`
这些代码都是编译器为我们生成的,虽然看不到,但是它确实在这.我们既然可以在类中使用@property生成一个属性,那为啥在分类中不可以呢?
警告categoryProperty属性的存取方法需要自己手动去实现,或者使用@dynamic在运行时实现这些方法.
也就是说,分类中的@property并没有为我们生成实例变量以及存取方法,而需要我们手动实现
因为在分类中@property并不会自动生成实例变量以及存取方法,所以一般用关联对象为已经存在的类添加”属性”.
在分类中实现伪属性:
#import "MHFObject+Category.h"
#import <objc/runtime.h>
@implementation MHFObject (Category)
6. (NSString *)categoryProperty{
return objc_getAssociatedObject(self, _cmd);
}
-(void)setCategoryProperty:(NSString *)categoryProperty{
objc_setAssociatedObject(self, @selector(categoryProperty), categoryProperty, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
这里的_cmd代指当前方法的选择器,也就是@selector(categoryProperty).
我们使用两个方法objc_getAssociatedObject以及objc_setAssociatedObject来模拟属性存取方法,而使用关联对象模拟实例变量.
在这里有必要解释两个问题:
1:为什么向方法中传入 @selector(categoryProperty)?
2:OBJC_ASSOCIATION_RETAIN_NONATOMIC 是干什么的?
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,id _Nullable value, objc_AssociationPolicy policy)
@selector(categoryProperty) 也就是参数中的 key,其实可以使用静态指针 static void * 类型的参数来代替,不过在这里,笔者强烈推荐使用 @selector(categoryProperty) 作为 key 传入。因为这种方法省略了声明参数的代码,并且能很好地保证 key 的唯一性。
OBJC_ASSOCIATION_RETAIN_NONATOMIC 又是什么呢?如果我们使用 Command 加左键查看它的定义:
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. */
};
从这里的注释我们能看到很多东西,也就是说不同的 objc_AssociationPolicy 对应了不同的属性修饰符:
objc_AssociationPolicy modifier
OBJC_ASSOCIATION_ASSIGN assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC nonatomic, strong
OBJC_ASSOCIATION_COPY_NONATOMIC nonatomic, copy
OBJC_ASSOCIATION_RETAIN atomic, strong
OBJC_ASSOCIATION_COPY atomic, copy
而我们在代码中实现的属性 categoryProperty 就相当于使用了 nonatomic 和 strong 修饰符。
@property` 其实有元编程的思想,它能够为我们自动生成实例变量以及存取方法,而这三者构成了属性这个类似于语法糖的概念,为我们提供了更便利的点语法来访问属性:
self.property <=> [self property]
self.property = value <=> [self setProperty:value]
在分类中,因为类的实例变量的布局已经固定,使用 @property 已经无法向固定的布局中添加新的实例变量(这样做可能会覆盖子类的实例变量),所以我们需要使用关联对象以及两个方法来模拟构成属性的三个要素。
关联对象的实现
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
id objc_getAssociatedObject(id object, const void *key);
void objc_removeAssociatedObjects(id object);
三个方法的作用分别是:
以键值对形式添加关联对象
根据 key 获取关联对象
移除所有关联对象
objc_setAssociatedObject
首先是 objc_setAssociatedObject 方法,这个方法的调用栈并不复杂:
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
└── void objc_setAssociatedObject_non_gc(id object, const void *key, id value, objc_AssociationPolicy policy)
└── void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy)
调用栈中的 _object_set_associative_reference 方法实际完成了设置关联对象的任务:
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
ObjcAssociation old_association(0, nil);
id new_value = value ? acquireValue(value, policy) : nil;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
ObjectAssociationMap *refs = i->second;
...
}
if (old_association.hasValue()) ReleaseValue()(old_association);
}
我们需要注意其中的几个类和数据结构,因为在具体分析这个方法的实现之前,我们需要了解其中它们的作用:
AssociationsManager
AssociationsHashMap
ObjcAssociationMap
ObjcAssociation
AssociationsManager
AssociationsManager 在源代码中的定义是这样的:
class AssociationsManager {
static spinlock_t _lock;
static AssociationsHashMap *_map;
public:
AssociationsManager() { _lock.lock(); }
~AssociationsManager() { _lock.unlock(); }
AssociationsHashMap &associations() {
if (_map == NULL)
_map = new AssociationsHashMap();
return *_map;
}
};
spinlock_t AssociationsManager::_lock;
AssociationsHashMap *AssociationsManager::_map = NULL;
它维护了 spinlock_t 和 AssociationsHashMap 的单例,初始化它的时候会调用 lock.lock() 方法,在析构时会调用 lock.unlock(),而 associations 方法用于取得一个全局的 AssociationsHashMap 单例。
也就是说 AssociationsManager 通过持有一个自旋锁 spinlock_t 保证对 AssociationsHashMap 的操作是线程安全的,即每次只会有一个线程对 AssociationsHashMap 进行操作。
如何存储ObjecAssociation
ObjcAssociation 就是真正的关联对象的类,上面的所有数据结构只是为了更好的存储它。
首先,AssociationsHashMap 用与保存从对象的 disguised_ptr_t 到 ObjectAssociationMap 的映射:
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); }
};
而 ObjectAssociationMap 则保存了从 key 到关联对象 ObjcAssociation 的映射,这个数据结构保存了当前对象对应的所有关联对象:
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); }
};
最关键的 ObjcAssociation 包含了 policy 以及 value:
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; }
};
举一个简单的例子来说明关联对象在内存中以什么形式存储的,以下面的代码为例:
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [NSObject new];
objc_setAssociatedObject(obj, @selector(hello), @"Hello", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return 0;
}
这里的关联对象 ObjcAssociation(OBJC_ASSOCIATION_RETAIN_NONATOMIC, @”Hello”) 在内存中是这么存储的:
接下来我们可以重新回到对 objc_setAssociatedObject 方法的分析了。
在这里会将方法的执行分为两种情况:
new_value != nil 设置/更新关联对象的值
new_value == nil 删除一个关联对象
先来分析在 new_value != nil 的情况下,该方法的执行是什么样的:
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
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);
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;
j->second = ObjcAssociation(policy, new_value);
} else {
(*refs)[key] = ObjcAssociation(policy, new_value);
}
} else {
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
object->setHasAssociatedObjects();
}
}
if (old_association.hasValue()) ReleaseValue()(old_association);
}
使用 old_association(0, nil) 创建一个临时的 ObjcAssociation 对象(用于持有原有的关联对象,方便在方法调用的最后释放值)
调用 acquireValue 对 new_value 进行 retain 或者 copy
static id acquireValue(id value, uintptr_t policy) {
switch (policy & 0xFF) {
case OBJC_ASSOCIATION_SETTER_RETAIN:
return ((id(*)(id, SEL))objc_msgSend)(value, SEL_retain);
case OBJC_ASSOCIATION_SETTER_COPY:
return ((id(*)(id, SEL))objc_msgSend)(value, SEL_copy);
}
return value;
}
初始化一个 AssociationsManager,并获取唯一的保存关联对象的哈希表 AssociationsHashMap
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
先使用 DISGUISE(object) 作为 key 寻找对应的 ObjectAssociationMap
如果没有找到,初始化一个 ObjectAssociationMap,再实例化 ObjcAssociation 对象添加到 Map 中,并调用 setHasAssociatedObjects 方法,表明当前对象含有关联对象
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
object->setHasAssociatedObjects();
如果找到了对应的 ObjectAssociationMap,就要看 key 是否存在了,由此来决定是更新原有的关联对象,还是增加一个
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);
}
最后的最后,如果原来的关联对象有值的话,会调用 ReleaseValue() 释放关联对象的值
struct ReleaseValue {
void operator() (ObjcAssociation &association) {
releaseValue(association.value(), association.policy());
}
};
static void releaseValue(id value, uintptr_t policy) {
if (policy & OBJC_ASSOCIATION_SETTER_RETAIN) {
((id(*)(id, SEL))objc_msgSend)(value, SEL_release);
}
}
到这里,该条件下的方法实现就结束了。
new_value == nil
如果 new_value == nil,就说明我们要删除对应 key 的关联对象,实现如下:
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
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);
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);
}
}
}
if (old_association.hasValue()) ReleaseValue()(old_association);
}
这种情况下方法的实现与前面的唯一区别就是,我们会调用 erase 方法,擦除 ObjectAssociationMap 中 key 对应的节点。
setHasAssociatedObjects()
其实上面的两种情况已经将 objc_setAssociatedObject 方法的实现分析得很透彻了,不过,这里还有一个小问题来等待我们解决,setHasAssociatedObjects() 方法的作用是什么?
inline void objc_object::setHasAssociatedObjects() {
if (isTaggedPointer()) return;
retry:
isa_t oldisa = LoadExclusive(&isa.bits);
isa_t newisa = oldisa;
if (!newisa.indexed) return;
if (newisa.has_assoc) return;
newisa.has_assoc = true;
if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;
}
它会将 isa 结构体中的标记位 has_assoc 标记为 true,也就是表示当前对象有关联对象,在这里我还想祭出这张图来介绍 isa 中的各个标记位都是干什么的。
objc_getAssociatedObject
我们既然已经对 objc_setAssociatedObject 的实现已经比较熟悉了,相信对于 objc_getAssociatedObject 的理解也会更加容易。
方法的调用栈和 objc_setAssociatedObject 非常相似:
id objc_getAssociatedObject(id object, const void *key)
└── id objc_getAssociatedObject_non_gc(id object, const void *key);
└── id _object_get_associative_reference(id object, void *key)
而 _object_get_associative_reference 相比于前面方法的实现更加简单。
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) ((id(*)(id, SEL))objc_msgSend)(value, SEL_retain);
}
}
}
if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
((id(*)(id, SEL))objc_msgSend)(value, SEL_autorelease);
}
return value;
}
代码中寻找关联对象的逻辑和 objc_setAssociatedObject 差不多:
获取静态变量 AssociationsHashMap
以 DISGUISE(object) 为 key 查找 AssociationsHashMap
以 void *key 为 key 查找 ObjcAssociation
根据 policy 调用相应的方法
if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) ((id(*)(id, SEL))objc_msgSend)(value, SEL_retain);
if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
((id(*)(id, SEL))objc_msgSend)(value, SEL_autorelease);
}
返回关联对象 ObjcAssociation 的值
objc_removeAssociatedObjects
关于最后的 objc_removeAssociatedObjects 方法,其实现也相对简单,这是方法的调用栈:
void objc_removeAssociatedObjects(id object)
└── void _object_remove_assocations(id object)
这是简化版本的 objc_removeAssociatedObjects 方法实现:
void objc_removeAssociatedObjects(id object) {
if (object && object->hasAssociatedObjects()) {
_object_remove_assocations(object);
}
}
为了加速移除对象的关联对象的速度,我们会通过标记位 has_assoc 来避免不必要的方法调用,在确认了对象和关联对象的存在之后,才会调用 _object_remove_assocations 方法移除对象上所有的关联对象:
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()) {
ObjectAssociationMap *refs = i->second;
for (ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j != end; ++j) {
elements.push_back(j->second);
}
delete refs;
associations.erase(i);
}
}
for_each(elements.begin(), elements.end(), ReleaseValue());
方法会将对象包含的所有关联对象加入到一个 vector 中,然后对所有的 ObjcAssociation 对象调用 ReleaseValue() 方法,释放不再被需要的值。
关于应用
本来在这个系列的文章中并不会涉及关联对象这个话题,不过,有人问过我这么一个问题:在分类中到底能否实现属性?其实在回答这个问题之前,首先要知道到底属性是什么?而属性的概念决定了这个问题的答案。
如果你把属性理解为通过方法访问的实例变量,我相信这个问题的答案是不能,因为分类不能为类增加额外的实例变量。
不过如果属性只是一个存取方法以及存储值的容器的集合,那么分类是可以实现属性的。
分类中对属性的实现其实只是实现了一个看起来像属性的接口而已。
关于实现
关联对象又是如何实现并且管理的呢:
关联对象其实就是 ObjcAssociation 对象
关联对象由 AssociationsManager 管理并在 AssociationsHashMap 存储
对象的指针以及其对应 ObjectAssociationMap 以键值对的形式存储在 AssociationsHashMap 中
ObjectAssociationMap 则是用于存储关联对象的数据结构
每一个对象都有一个标记位 has_assoc 指示对象是否含有关联对象
原文转载链接