想突破自身的技术瓶颈,提高自身iOS开发水平,深挖 Runtime源码 一定是个不错的方法。本篇博客我们结合 Runtime源码 讲一下 Objective-C Associated Objects 的实现原理。 由于在iOS开发中经常需要使用分类,如果我们需要为已经存在的类添加属性,使用 @property 并不能在分类中正确创建实例变量和存取方法,不过 通过 Objective-C 运行时中的关联对象,也就是 Associated Objects ,可以实现上述需求。
首先看一段Persion类代码:
@interface Persion : NSObject
@property (nonatomic, copy) NSString *name;
@end
上述代码,无所不能的LLVM会帮我们做3件事:
_name
- (NSString *)name
- (void)setName:(NSString *)name
伪代码如下:(这些代码都是编译器默认为我们生成的,虽然我们看不到它,但是它确实在这里。)
@implementation Persion {
NSString *_name;
}
- (NSString *)name {
return _name;
}
- (void)setName:(NSString *)name {
_name = name;
}
@end
如果在分类中LLVM还会帮@property 做这些吗?
我们来验证一下,新建分类 Persion+SmallThree 代码如下:
#import "Persion.h"
@interface Persion (SmallThree)
@property (nonatomic, copy) NSString *adreass;
@end
Build Demo,发现有这么一个警告,LLVM似乎不是很友好…
读取上面的警告,我们得知, Category Property 属性的存取方法需要自己手动去实现,或者使用 @dynamic 在运行时实现这些方法。
换句话说,分类中 LLVM 和 @property 闹掰了, 并没有为我们生成实例变量以及存取方法,而需要我们手动实现。
单纯的技术辕们总是听不进去别人的话,总喜欢用代码来验证一些问题
#import "ViewController.h"
#import "Persion.h"
#import "Persion+SmallThree.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Persion *persion = [[Persion alloc] init];
persion.adreass = @"北京";
}
@end
Build Demo,程序果然直接 Crash。
看一下 Crash Message
Crash Message 提示我们 找不到 setAdress: 这个方法 。经历了事故现场,最终确认 LLVM 和 @property 确实闹掰了,Category Property 属性的存取方法确实需要自己手动去实现。那么如何实现,装B的环节到了,用 Associated Object 。
使用 Associated Object, 造一个伪属性,代码如下:
#import "Persion.h"
@interface Persion (SmallThree)
@property (nonatomic, copy) NSString *adreass;
@end
#import "Persion+SmallThree.h"
#import
@implementation Persion (SmallThree)
- (NSString *)adreass {
return objc_getAssociatedObject(self, _cmd);
}
- (void)setAdreass:(NSString *)adreass {
objc_setAssociatedObject(self, @selector(adreass), adreass, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end
这里的 _cmd 代指当前方法的选择子,也就是 @selector(adreass)。
#import "ViewController.h"
#import "Persion.h"
#import "Persion+SmallThree.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Persion *persion = [[Persion alloc] init];
persion.adreass = @"北京";
NSLog(@"persion.adreass==%@",persion.adreass);
}
@end
bulid Demo,运行结果:
2017-01-20 15:56:56.022 ZYY_ CategoryStudy[5661:212619] persion.adreass==北京
卧槽,通过objc_getAssociatedObject 以及 objc_setAssociatedObject 两个方法 ,轻松的实现了我们的需求。
那么Objective-C 中 Associated Object 的实现原理是什么?
先从 Associated Object 使用场景说起:
按照 Mattt Thompson 大神的文章 Associated Object(传送门) 中的说法,Associated Objects 主要有以下三个使用场景:
从本质上看,第 1 、2 个场景其实是一个意思,唯一的区别就在于新添加的这个属性是公有的还是私有的而已。就目前来说,我在实际工作中使用得最多的是第 2 个场景,而第 3 个场景我还没有使用过。
相关函数
与 Associated Objects 相关的函数主要有三个,我们可以在 Runtime 源码 的 runtime.h 文件中找到它们的声明:
OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
OBJC_EXPORT id objc_getAssociatedObject(id object, const void *key)
OBJC_EXPORT void objc_removeAssociatedObjects(id object)
简单的介绍一下三个函数的作用:
// 用于给object添加关联对象value,传入 nil 则可以移除已有的关联对象,注意object自身为被关联的对象。
OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
// 用于获取关联对象
OBJC_EXPORT id objc_getAssociatedObject(id object, const void *key)
// 删除绑定关系,移除一个被关联对象的所有关联对象,函数我们一般是用不上的,因为这个函数会移除一个被关联对象的所有关联对象,将该对象恢复成“原始”状态。这样做就很有可能把别人添加的关联对象也一并移除,这并不是我们所希望的。所以一般的做法是通过给 objc_setAssociatedObject 函数传入 nil 来移除某个已有的关联对象
OBJC_EXPORT void objc_removeAssociatedObjects(id object)
key 值
关于前两个函数中的 key 值是我们需要重点关注的一个点, 一般来说,有以下三种推荐的 key 值:
1、声明 static char kAssociatedObjectKey; ,使用 &kAssociatedObjectKey 作为 key 值;
代码实例:
#import "Persion+SmallThree.h"
#import
static char kAdreassKey;
@implementation Persion (SmallThree)
- (NSString *)adreass {
return objc_getAssociatedObject(self, &kAdreassKey);
}
- (void)setAdreass:(NSString *)adreass {
objc_setAssociatedObject(self, &kAdreassKey, adreass, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end
2、声明 static void *kAssociatedObjectKey = &kAssociatedObjectKey; ,使用 kAssociatedObjectKey 作为 key 值;
代码实例:
#import "Persion+SmallThree.h"
#import
static void *kAdreassKey = &kAdreassKey;
@implementation Persion (SmallThree)
- (NSString *)adreass {
return objc_getAssociatedObject(self, kAdreassKey);
}
- (void)setAdreass:(NSString *)adreass {
objc_setAssociatedObject(self, kAdreassKey, adreass, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end
3、用 selector ,使用 getter 方法的名称作为 key 值。
代码实例;
#import "Persion+SmallThree.h"
#import
@implementation Persion (SmallThree)
- (NSString *)adreass {
return objc_getAssociatedObject(self, @selector(adreass)); }
- (void)setAdreass:(NSString *)adreass {
objc_setAssociatedObject(self, @selector(adreass), adreass, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end
上述代码可以进一步简化为:
#import "Persion+SmallThree.h"
#import
@implementation Persion (SmallThree)
- (NSString *)adreass {
return objc_getAssociatedObject(self, _cmd);
}
- (void)setAdreass:(NSString *)adreass {
objc_setAssociatedObject(self, @selector(adreass), adreass, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end
我个人最喜欢的是 第 3 种方式,因为它省掉了一个变量名,非常优雅地解决了命名难题。
关联策略
在给一个对象添加关联对象时有五种关联策略可供选择:
关联策略 | 等价属性 | 说明 |
---|---|---|
OBJC_ASSOCIATION_ASSIGN | @property (assign) or @property (unsafe_unretained) | 弱引用关联对象 |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | @property (strong, nonatomic) | 强引用关联对象,且为非原子操作 |
OBJC_ASSOCIATION_COPY_NONATOMIC | @property (copy, nonatomic) | 复制关联对象,且为非原子操作 |
OBJC_ASSOCIATION_RETAIN | @property (strong, atomic) | 强引用关联对象,且为原子操作 |
OBJC_ASSOCIATION_COPY | @property (copy, atomic) | 复制关联对象,且为原子操作 |
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) {
// retain the new value (if any) outside the lock.
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);
if (new_value) {
// break any existing association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// secondary table exists
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 {
// create the new association (first time).
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
object->setHasAssociatedObjects();
}
} else {
// setting the association to nil breaks the association.
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);
}
}
}
}
// release the old value (outside of the lock).
if (old_association.hasValue()) ReleaseValue()(old_association);
}
在看这段代码前,我们需要先了解一下几个数据结构以及它们之间的关系:
AssociationsManager源代码
class AssociationsManager {
static spinlock_t _lock;
static AssociationsHashMap *_map;
public:
AssociationsManager() { _lock.lock(); } // 初始化时候
~AssociationsManager() { _lock.unlock(); } // 析构的时候
// associations 方法用于取得一个全局的 AssociationsHashMap 单例
AssociationsHashMap &associations() {
if (_map == NULL)
_map = new AssociationsHashMap();
return *_map;
}
};
spinlock_t AssociationsManager::_lock;
AssociationsHashMap *AssociationsManager::_map = NULL;
也就是说 AssociationsManager 通过持有一个自旋锁 spinlock_t ,(关于iOS中各种锁的作用和区别,不在本篇博客讨论)保证对 AssociationsHashMap 的操作是线程安全的,即每次只会有一个线程对 AssociationsHashMap 进行操作。
AssociationsHashMap源码
class AssociationsHashMap : public unordered_map {
public:
void *operator new(size_t n) { return ::malloc(n); }
void operator delete(void *ptr) { ::free(ptr); }
};
AssociationsHashMap 用与保存从对象的 disguised_ptr_t 到 ObjectAssociationMap 的映射
typedef unsigned long uintptr_t;
typedef uintptr_t disguised_ptr_t;
disguised_ptr_t 其实就是 unsigned long
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); }
};
ObjectAssociationMap 则保存了从 key 到关联对象 ObjcAssociation 的映射,这个数据结构保存了当前对象对应的所有关联对象
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 就是真正的关联对象的类,上面的所有数据结构只是为了更好的存储它。
最关键的 ObjcAssociation 包含了 policy 以及 value:
如果你对源码不感兴趣,那么记住下面这张图,绝对事半功倍。
我们再举一个简单的例子来说明关联对象在内存中以什么形式存储的,以下面的代码为例:
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 方法的分析.
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// retain the new value (if any) outside the lock.
uintptr_t old_policy = 0; // NOTE: old_policy is always assigned to when old_value is non-nil.
id new_value = value ? acquireValue(value, policy) : nil, old_value = nil; // 调用 acquireValue 对 value 进行 retain 或者 copy
{
// & 取地址 *是指针,就是地址的内容
AssociationsManager manager; // 初始化一个 AssociationsManager 类型的变量 manager
AssociationsHashMap &associations(manager.associations()); // 取得一个全局的 AssociationsHashMap 单例
if (new_value) {
// 如果new_value不为空,开始遍历associations指向的map,查找object对象是否存在保存联合存储数据的ObjectAssociationMap对象
// 查找map中是否包含某个关键字条目,用 find() 方法,传入的参数是要查找的key(被关联对象的内存地址),在这里需要提到的是begin()和end()两个成员,分别代表map对象中第一个条目和最后一个条目,这两个数据的类型是iterator.
// 定义一个条目变量 i (实际是指针)
AssociationsHashMap::iterator i = associations.find(object); // AssociationsHashMap 是一个无序的哈希表,维护了从对象地址到 ObjectAssociationMap 的映射;
// iterator是 C++ 中的迭代器 , 这句话是定义一个 AssociationsHashMap::iterator 类型的变量 i,初始化为 associations.find(object) , associations是AssociationsHashMap类型对象。
// 通过map对象的方法获取的iterator数据类型 是一个std::pair对象
// 根据对象地址获取起对应的 ObjectAssociationMap对象
if (i != associations.end()) {
// 存在
// object对象在associations指向的map中存在一个ObjectAssociationMap对象refs
// ObjectAssociationMap 是一个 C++ 中的 map ,维护了从 key(就是外界传入的key) 到 ObjcAssociation 的映射,即关联记录
ObjectAssociationMap *refs = i->second; // 指针 调用方法 需要用 -> i 是 AssociationsHashMap i->second 表示ObjectAssociationMap i->first 表示对象的地址
ObjectAssociationMap::iterator j = refs->find(key); // 根据传入的关联对象的key(一个地址)获取其对应的关联对象 ObjectAssociationMap
// 关联对象是否存在
if (j != refs->end()) {
// 使用过该key保存value,用新的value和policy替换掉原来的值
// 如果存在 持有旧的关联对象
ObjcAssociation &old_entry = j->second;
old_policy = old_entry.policy;
old_value = old_entry.value;
// 存入新的关联对象
old_entry.policy = policy;
old_entry.value = new_value;
} else {
// 没用使用过该key保存value,将value和policy保存到key映射的map中
// 如果不存在 直接存入新的关联对象
(*refs)[key] = ObjcAssociation(policy, new_value); // 对map 插入元素
}
}
else {
// 不存在
// 没有object就创建
// create the new association (first time).
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
_class_setInstancesHaveAssociatedObjects(_object_getClass(object));
}
} else {
// setting the association to nil breaks the association.
AssociationsHashMap::iterator i = associations.find(object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
ObjcAssociation &old_entry = j->second;
old_policy = old_entry.policy;
old_value = (id) old_entry.value;
// 从 map中删除该项
refs->erase(j);
}
}
}
}
// 旧的关联对象是否存在,如果存在,释放旧的关联对象。
// release the old value (outside of the lock).
if (old_value) releaseValue(old_value, old_policy);
}
弄清楚这些数据结构之间的关系后,再回过头来看上面的代码就不难了。我们发现,在苹果的底层代码中一般都会充斥着各种 if else ,可见写好 if else 后我们就距离成为高手不远了。开个玩笑,我们来看下面的流程图,一图胜千言:
objc_getAssociatedObject源码
同样的,我们也可以在 objc-references.mm 文件中找到 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 函数后,objc_getAssociatedObject 函数对我们来说就是小菜一碟了。这个函数先根据对象地址在 AssociationsHashMap 中查找其对应的 ObjectAssociationMap 对象,如果能找到则进一步根据 key 在 ObjectAssociationMap 对象中查找这个 key 所对应的关联结构 ObjcAssociation ,如果能找到则返回 ObjcAssociation 对象的 value 值,否则返回 nil 。
objc_removeAssociatedObjects源码
同理,我们也可以在 objc-references.mm 文件中找到 objc_removeAssociatedObjects 函数最终调用的函数:
void _object_remove_assocations(id object) {
vector< ObjcAssociation,ObjcAllocator > 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()) {
// 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.
for_each(elements.begin(), elements.end(), ReleaseValue());
}
这个函数负责移除一个对象的所有关联对象,具体实现也是先根据对象的地址获取其对应的 ObjectAssociationMap 对象,然后将所有的关联结构保存到一个 vector 中,最终释放 vector 中保存的所有关联对象。在一个对象被释放时,也正是调用的这个函数来移除其所有的关联对象。
关于实现
关联对象是如何实现并且管理的呢:
关联对象其实就是 ObjcAssociation 对象
关联对象由 AssociationsManager 管理并在 AssociationsHashMap 存储
对象的指针以及其对应 ObjectAssociationMap 以键值对的形式存储在 AssociationsHashMap 中
ObjectAssociationMap 则是用于存储关联对象的数据结构
每一个对象都有一个标记位 has_assoc 指示对象是否含有关联对象
在弄懂 Associated Objects 的实现原理后,可以帮助我们更好地使用它,在出现问题时也能尽快地定位问题,最后希望本文能够对你有所帮助。
更多iOS技术交流
请加群: 553633494