一、实际问题
1.提出问题
首先,一切都要从一个问题开始:在Objective-C中,能否在Category中为类添加属性及对应的实例变量?
该题的答案是:不能。
2.分析解答
为什么不能通过Category来为Objective-C的类添加属性及对应的实例变量呢?原因在于在编译完成之后,类的内存布局就已经确定了,如果此时为类添加实例变量,则会破坏已经确定的内存布局,这显然是不合理的。这一点,可以通过Category在runtime中的定义得以验证。
以下是Category在runtime中对应的结构体:
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;
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta) {
if (isMeta) return nil; // classProperties;
else return instanceProperties;
}
};
通过Category结构体的定义我们也可以知道,通过Category我们可以为类做以下事情:
- 添加实例方法。
- 添加类方法。
- 遵守相关协议。
- 添加属性。
而添加实例变量并不在Category的能力范围之内。
3.提出疑问
从上述描述我们可以知道,Category可以为类添加属性,那么既然添加了属性,不是会自动添加对应的实例变量吗?
答案很简单:对于Category来说,此时添加的属性,是存在一些缺陷的。
对于一个类来说,当我们在原类中添加一个属性时,该属性会默认识别为存储属性,即编译器会为该属性默认添加@synthesize
关键字,来将属性合成为实例变量和对应的getter/setter方法。
但是对于Category来说,当我们新增一个属性时,编译器会给出我们两个警告:
该警告提示我们,我们需要自己手动去实现属性对应的getter/setter方法,即编译器会将该属性识别为计算属性。
如果简单粗暴的忽略该警告,那么在调用属性的getter/setter方法时,会发生运行时崩溃。
4.解决方案
对于Objective-C来说,runtime提供了很多黑科技,其中一项技术就可以通过曲线救国的方法来达到为类添加属性及对应的实例变量的效果。这个黑科技就是管理对象(AssociatedObject)。
二、关联对象应用
下面我们就通过Category与关联对象来为一个叫Model的类添加一个名为name的实例变量以及对应的getter/setter方法。
/// 声明
@interface Model (Association)
@property (nonatomic, copy) NSString *name;
@end
/// 实现
@implementation Model (Association)
- (void)setName:(NSString *)name {
objc_setAssociatedObject(self, sel_getName(@selector(name)), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name {
return objc_getAssociatedObject(self, sel_getName(@selector(name)));
}
@end
使用情况如下:
Model *model = [[Model alloc] init];
model.name = @"123";
NSLog(@"%@", model.name);
使用和在原类中声明name属性的使用完全一致。
三、原理分析
接下来我们来对关联对象的实现原理进行一下分析。
1.相关API
首先我们从runtime中关联对象的相关API来入手:
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);
关联对象的API就只有这三个,它们的作用分别是:
- 为一个对象通过一个key来设置一个关联值,同时指定关联策略(关联策略在后面解释)。
- 获取一个对象中通过key值关联的值。
- 为一个对象移除所有关联值。
2. objc_setAssociatedObject解析
objc_setAssociatedObject
的实现很简单,它通过方法调用链objc_setAssociatedObject
-> objc_setAssociatedObject_non_gc
-> _object_set_associative_reference
,最终在_object_set_associative_reference
方法中完成对应逻辑,在开始阅读逻辑之前,我们先了解一下该逻辑中一些我们第一次见的一些东西:
数据结构:
(1) 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;
}
};
AssociationsManager维护了一个静态的(全局只存在一个)锁_lock
以及一个静态的(全局只存在一个)、懒加载的AssociationsHashMap
的哈希表。
同时,每创建一个AssociationsManager时,就会对_lock加锁,在释放AssociationsManager时,会对_lock解锁。
(2) AssociationsHashMap
AssociationsHashMap基于不同的平台有不同的实现,但是对外的接口都是一个哈希表,它的key值为disguised_ptr_t
类型,value值为ObjectAssociationMap
类型。
(3) ObjectAssociationMap
ObjectAssociationMap也是基于不同的平台有不同的实现,但对外的接口也是一个哈希表,它的key值为void *
的指针,value值为ObjcAssociation
类型。
(4) ObjcAssociation
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; }
};
ObjcAssociation保存了一个id类型的值以及一个_policy的变量,从该变量的命名以及类型来看,该值应为前面提到的关联策略。
相关方法:
(1) acquireValue
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;
}
(2) releaseValue
static void releaseValue(id value, uintptr_t policy) {
if (policy & OBJC_ASSOCIATION_SETTER_RETAIN) {
((id(*)(id, SEL))objc_msgSend)(value, SEL_release);
}
}
struct ReleaseValue {
void operator() (ObjcAssociation &association) {
releaseValue(association.value(), association.policy());
}
};
关联策略对应关系
从上述两个方法中可以看出,我们传入的关联策略与关联对象底层实现时所用的关联策略不一样,我们来列举一下:
我们传入的关联策略:
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
};
底层实现时用到的策略:
enum {
OBJC_ASSOCIATION_SETTER_ASSIGN = 0,
OBJC_ASSOCIATION_SETTER_RETAIN = 1,
OBJC_ASSOCIATION_SETTER_COPY = 3,
OBJC_ASSOCIATION_GETTER_READ = (0 << 8),
OBJC_ASSOCIATION_GETTER_RETAIN = (1 << 8),
OBJC_ASSOCIATION_GETTER_AUTORELEASE = (2 << 8)
};
由定义可以看出,我们传入的关联策略与底层使用的关联策略具有对应关系:
传入关联策略 | 包含底层使用关联策略 | 隐式包含底层使用关联策略 |
---|---|---|
OBJC_ASSOCIATION_ASSIGN | OBJC_ASSOCIATION_SETTER_ASSIGN | |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | OBJC_ASSOCIATION_SETTER_RETAIN | |
OBJC_ASSOCIATION_COPY_NONATOMIC | OBJC_ASSOCIATION_SETTER_COPY | OBJC_ASSOCIATION_SETTER_RETAIN |
OBJC_ASSOCIATION_RETAIN | OBJC_ASSOCIATION_SETTER_RETAIN OBJC_ASSOCIATION_GETTER_RETAIN OBJC_ASSOCIATION_GETTER_AUTORELEASE | |
OBJC_ASSOCIATION_COPY | OBJC_ASSOCIATION_SETTER_COPY OBJC_ASSOCIATION_GETTER_RETAIN OBJC_ASSOCIATION_GETTER_AUTORELEASE | OBJC_ASSOCIATION_SETTER_RETAIN |
理清了一些新见的数据结构、方法以及策略所对应的关系,我们就可以读懂_object_set_associative_reference
方法了:
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// 使用OBJC_ASSOCIATION_ASSIGN和nil来初始化一个ObjcAssociation,该变量用于保存原始关联值
ObjcAssociation old_association(0, nil);
// 对需要新关联的值进行处理
// 若新值不为nil,则在acquireValue方法中根据传入策略是否包含OBJC_ASSOCIATION_SETTER_RETAIN/OBJC_ASSOCIATION_SETTER_COPY来进行对应的增加计数/拷贝
id new_value = value ? acquireValue(value, policy) : nil;
{
// 通过新建AssociationsManager对象来获取到全局的AssociationsHashMap对象并对全局的_lock进行加锁
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
// 对传入的object进行伪装,获取到对应的disguised_ptr_t变量
// 该类型可用作AssociationsHashMap的key值
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
// 如果关联的值不为nil
// 从全局的AssociationsHashMap中获取该object对应的ObjectAssociationMap
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// 该object对应的ObjectAssociationMap存在,说明之前对该object进行过关联对象
ObjectAssociationMap *refs = i->second;
// 通过传入的key值获取该ObjectAssociationMap对应的ObjcAssociation值
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
// ObjcAssociation值存在,说明之前已经通过key键对该object关联过值,此时将旧值保存至临时变量,将新值写入map中
old_association = j->second;
j->second = ObjcAssociation(policy, new_value);
} else {
// ObjcAssociation值不存在,说明之前没有通过key键对该object关联过值,此时需要新创建ObjcAssociation并写入map中
(*refs)[key] = ObjcAssociation(policy, new_value);
}
} else {
// 该object对应的ObjectAssociationMap不存在,说明之前对该object没有进行过关联对象
// 需要为该对象新建ObjectAssociationMap对象,并将需要关联的值封装为ObjcAssociation对象,存入到该ObjectAssociationMap中
// 同时将ObjectAssociationMap存入到全局AssociationsHashMap中
// 最后将该对象标记为存在关联对象
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
object->setHasAssociatedObjects();
}
} else {
// 关联值为nil
// 从全局的AssociationsHashMap中获取该object对应的ObjectAssociationMap
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// 该object对应的ObjectAssociationMap存在,说明之前对该object进行过关联对象
ObjectAssociationMap *refs = i->second;
// 通过传入的key值获取该ObjectAssociationMap对应的ObjcAssociation值
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
// ObjcAssociation值存在,说明之前已经通过key键对该object关联过值,此时将旧值保存至临时变量,同时对map中旧值进行清除
old_association = j->second;
refs->erase(j);
}
}
}
}
// 对旧值进行相应的计数操作
// 若旧值关联策略含有OBJC_ASSOCIATION_SETTER_RETAIN,即关联策略为
// OBJC_ASSOCIATION_RETAIN_NONATOMIC 或
// OBJC_ASSOCIATION_COPY_NONATOMIC 或
// OBJC_ASSOCIATION_RETAIN 或
// OBJC_ASSOCIATION_COPY,则进行release操作
if (old_association.hasValue()) ReleaseValue()(old_association);
}
2. objc_getAssociatedObject解析
objc_getAssociatedObject
的实现很简单,它通过方法调用链objc_getAssociatedObject
-> objc_getAssociatedObject_non_gc
-> _object_get_associative_reference
,最终在_object_get_associative_reference
方法中完成对应逻辑:
id _object_get_associative_reference(id object, void *key) {
// 声明一个用于返回的临时值
id value = nil;
// 声明一个默认的关联策略
uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
{
// 通过新建AssociationsManager对象来获取到全局的AssociationsHashMap对象并对全局的_lock进行加锁
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
// 对传入的object进行伪装,获取到对应的disguised_ptr_t变量
// 该类型可用作AssociationsHashMap的key值
disguised_ptr_t disguised_object = DISGUISE(object);
// 从全局的AssociationsHashMap中获取该object对应的ObjectAssociationMap
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// 该object对应的ObjectAssociationMap存在,说明之前对该object进行过关联对象
ObjectAssociationMap *refs = i->second;
// 通过传入的key值获取该ObjectAssociationMap对应的ObjcAssociation值
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
// ObjcAssociation值存在,说明之前已经通过key键对该object关联过值
// 将存储的值赋值给用于返回的临时变量中
// 将对应的关联策略赋值给用于保存关联策略的临时变量中
ObjcAssociation &entry = j->second;
value = entry.value();
policy = entry.policy();
// 若关联策略为OBJC_ASSOCIATION_RETAIN 或 OBJC_ASSOCIATION_COPY时,对关联值进行引用计数+1
if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) ((id(*)(id, SEL))objc_msgSend)(value, SEL_retain);
}
}
}
// 若获取到关联值并且关联策略为OBJC_ASSOCIATION_RETAIN 或 OBJC_ASSOCIATION_COPY时,将关联值放入自动释放池中
if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
((id(*)(id, SEL))objc_msgSend)(value, SEL_autorelease);
}
return value;
}
3. objc_removeAssociatedObjects解析
objc_removeAssociatedObjects
通过判断object是否被标记为存在关联对象来调用_object_remove_assocations
:
void _object_remove_assocations(id object) {
vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > elements;
{
// 通过新建AssociationsManager对象来获取到全局的AssociationsHashMap对象并对全局的_lock进行加锁
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
// 如果没有任何对象进行过关联对象,则直接退出
if (associations.size() == 0) return;
// 对传入的object进行伪装,获取到对应的disguised_ptr_t变量
// 该类型可用作AssociationsHashMap的key值
disguised_ptr_t disguised_object = DISGUISE(object);
// 从全局的AssociationsHashMap中获取该object对应的ObjectAssociationMap
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// 该object对应的ObjectAssociationMap存在,说明之前对该object进行过关联对象
ObjectAssociationMap *refs = i->second;
// 将所有关联的值所在的ObjcAssociation对象放入一个数组中
for (ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j != end; ++j) {
elements.push_back(j->second);
}
// 清除该对象在全局AssociationsHashMap中对应的数据
delete refs;
associations.erase(i);
}
}
// 遍历存储有所有关联值所在的ObjcAssociation所在数组,对每一个ObjcAssociation进行相应的计数操作
for_each(elements.begin(), elements.end(), ReleaseValue());
}
该API在我们的实现中并没有调用,那么runtime是如何在一个对象释放时将它所对应的关联对象清除呢?我们可以从对象的dealloc
方法入手进行分析。
dealloc
方法会调用到_objc_rootDealloc
方法,进而调用到obj->rootDealloc()
方法:
inline void objc_object::rootDealloc() {
assert(!UseGC);
if (isTaggedPointer()) return;
if (isa.indexed &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc)
{
assert(!sidetable_present());
free(this);
}
else {
object_dispose((id)this);
}
}
在该方法中,如果对象被标志位存在关联对象,那么就会调用object_dispose
方法,进而调用objc_destructInstance
方法:
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = !UseGC && obj->hasAssociatedObjects();
bool dealloc = !UseGC;
// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);
if (dealloc) obj->clearDeallocating();
}
return obj;
}
在该方法中,对于关联对象,可以看到会调用到_object_remove_assocations
方法,这也就移除了所有关联对象,同时也省去了我们显式去调用_object_remove_assocations
方法。
四、总结
由此可见,我们可以通过关联对象来为每一个对象增加一些相关联的对象,这些对象并没有保存在类的内存结构中,而是统一由一个静态AssociationsHashMap
来保存。该机制的数据结构可以总结如下图: