在 Category 的探究 中我们看到 Category 的底层结构为结构体,这个结构体的成员有:实例方法列表、类方法列表、协议列表以及属性信息。在分类中添加方法的表现和类中添加方法的表现略有不同。
假如在 Valenti+Category
分类中添加了如下属性:
@interface Valenti (Category)
@property (nonatomic, copy) NSString* name;
@end
实际等价于:
@interface Valenti (Category)
-(void)setName:(NSString * _Nonnull)name;
-(NSString*)name;
@end
并且在 .m
文件中无 setter/getter 方法的实现。
所以在编译过后 Xcode 会报警告信息:
Property 'name' requires method 'name' to be defined - use @dynamic or provide a method implementation in this category
Property 'name' requires method 'setName:' to be defined - use @dynamic or provide a method implementation in this category
而且也没有添加对应的成员变量:
那么分类中能不能直接添加成员变量?答案是否定的,当我添加如下代码的时候,Xcode 给我一个红色的错误信息:
@interface Valenti (Category)
{
@public
NSInteger i;
}
@end
Instance variables may not be placed in categories
分类中确实可以实现添加属性的效果,就是通过关联对象来实现的。
关联对象的基本使用
关联对象亦是借助于 Runtime
实现的,API 对应 set/get 方法,为:
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key, id _Nullable value, objc_AssociationPolicy)
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key);
参数 | 含义 |
---|---|
object | 目标对象 |
key | 关联的键,唯一 |
value | 关联键对应的值 |
policy | 内存管理策略 |
这里的 key-value 和字典的一一对应关系一样。
内存管理策略:
objc_AssociationPolicy | 对应修饰 |
---|---|
OBJC_ASSOCIATION_ASSIGN | assign |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | strong, nonatomic |
OBJC_ASSOCIATION_COPY_NONATOMIC | copy, nonatomic |
OBJC_ASSOCIATION_RETAIN | strong, atmoic |
OBJC_ASSOCIATION_COPY | copy, atmoic |
那么在前面要给 Valenti 的分类中增加一个 name
属性该如何实现?首先 .h
中:
@property (nonatomic, copy) NSString* name;
此时已经有了 setter/getter 的方法的声明,实现需要手写。
setter:
-(void)setName:(NSString *)name {
objc_setAssociatedObject(self, VALENTI_NAME_KEY, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
self 表示给 Valenti 的对象关联属性,因为字符串通常为
nonatomic
和copy
修饰,故内存管理策略为OBJC_ASSOCIATION_COPY_NONATOMIC
getter:
-(NSString*)name {
return objc_getAssociatedObject(self, VALENTI_NAME_KEY);
}
两个方法的 key 值是要求唯一的,并且是
const void*
类型,故,我们可像给 KVO 的 context 赋值那样取内存地址做 key。
key 的声明:
static const void* VALENTI_NAME_KEY = &VALENTI_NAME_KEY;
static 是为了使 key 的作用域仅限定在当前文件,外界也是无法对其进行篡改的。
当然,也可以将 key 直接写成字符串的形式:
objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_COPY_NONATOMIC);
@"name"
存储在常量区,并且在这里是以地址的形式传进去的,也能保证唯一,这种形式相比上面的好处是不用在外部声明一长串,并且使 key 所表达意思更加清晰。
当我们运行如下的代码的时候:
Valenti* v1 = [[Valenti alloc] init];
v1.name = @"Christina Aguilera";
Valenti* v2 = [[Valenti alloc] init];
v2.name = @"BoA";
NSLog(@"v1.name --> %@, v2.name --> %@", v1.name, v2.name);
运行结果为:
v1.name --> Christina Aguilera, v2.name --> BoA
其他 API
有关联也就有移除:
objc_removeAssociatedObjects(id _Nonnull object)
不过该方法是移除所有关联对象。
关联对象原理
实现关联对象的核心类有四个:
- AssociationsManager
- AssociationsHashMap
- ObjectAssociationMap
- ObjcAssociation
有关关联对象的源码在 objc-refrences.mm
文件中。
首先要明白核心的四个类的关系,首先 AssoicationsManager 的结构是这样的:
class AssociationsManager {
static AssociationsHashMap *_map;
public:
AssociationsManager() { AssociationsManagerLock.lock(); }
~AssociationsManager() { AssociationsManagerLock.unlock(); }
AssociationsHashMap &associations() {
if (_map == NULL)
_map = new AssociationsHashMap();
return *_map;
}
};
可看到 AssociationsHashMap
是 AssociationsManager 的一个成员。
AssociationsHashMap 的声明:
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); }
};
其中 disguised_ptr_t
为 key,ObjectAssociationMap *
为 value。也就是说 Map 中的 value 还是一个 Map。
而 ObjectAssociationMap
的声明为:
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); }
};
void *
为 ObjectAssociationMap 的 key,ObjcAssociation
为 value。
最后,我们再看看 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
内部出现了策略和 value 相关,是不是很眼熟?就是关联对象 API 中的 value 和 policy 参数。
到此为止,四个类的关系就像洋葱一样层层包裹:
例子中的 Valenti 对象也就是 setter 方法中的 object
对应 AssociationsHashMap 的 disguised_ptr_t
,也就是 AssociationsHashMap 的 key,一个 Valenti 对象对应一个 ObjectAssociationMap。而 ObjectAssociationMap 中存放着要关联的 key,也就是 setter 方法中第二个参数 key,一个 key 对应一个 ObjectAssociation,最后 value 和 policy 就对应 ObjectAssociation 的 _policy
和 _value
。
源码
那么再来看设值和取值的源码。
set 方法实现:
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
...
{
AssociationsManager manager;
// 获取 map
AssociationsHashMap &associations(manager.associations());
// 传进 Valenti 对象,生成 AssociationsHashMap 的 key
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
// 由上面的 key 得到得到迭代器 iterator
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// 通过迭代器得到 ObjectAssociationMap
ObjectAssociationMap *refs = i->second;
// ObjectAssociationMap 通过传进来的 key,也就是例子中的 @“name” 可以得到迭代器
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
// 通过迭代器找到 ObjectAssociation
old_association = j->second;
// 存储策略和值(value)
j->second = ObjcAssociation(policy, new_value);
} else {
...
}
set 方法已然明白其原理,那么同理 get 也就很好解读,就是迭代器拿到 Map 然后取值的过程,在这里不贴出源码。
注意
我们可发现:
- 关联对象并不是存储在被关联的对象本身中
- 关联对象存储在全局的 AssociationHashMap 中
- 设置关联对象为 nil 的时候等于移除关联对象
源码中有对应:
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;
// C++ 中用来擦除的方法
refs->erase(j);
}
}
我们在移除关联对象的源码中同样可以发现大量的 erase()
操作。
-
关联对象不是属性,没有 weak 操作
若我们执行:
Valenti* v1 = [[Valenti alloc] init];
v1.name = @"Christina Aguilera";
{
NSObject* obj = [[NSObject alloc] init];
objc_setAssociatedObject(v1, @"obj", obj, OBJC_ASSOCIATION_ASSIGN);
}
NSLog(@"%@", objc_getAssociatedObject(v1, @"obj"));
可发现程序报 EXC_BAD_ACCESS
错:
在离开
{}
局部代码块后 obj 对象已经销毁,但 ObjectAssociation 中的 _value 还在存储着它的内存值,所以在访问已经销毁的 obj 会发生 EXC_BAD_ACCESS
错误。这就说明 _value 不是弱引用,若是弱引用 _value 会置为 nil。