Associated Objects 实现及 weak 方案
Associated Objects 是 OC Runtime 2.0 中的特性之一,提供了为既有类动态添加关联对象的能力,也是对 category 只能拓展行为的一个很好的补充。它通过 Key-Value 的形式将对象与类绑定,并提供类似属性的关联策略,最终保存在全局的 Hash map 中。本文尝试从源码分析 Associated Objects 的实现及其能与不能,并通过一种 walk around 的方式实现 weak Associated Objects 。
源码版本:objc4-818
实现
我们熟悉的 runtime 提供的关联对象的 API 如下:1
2
3
4
5void 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)
一对 set/get 方法和一个移除全部关联对象的方法,下面来逐个分析。
Set Associated Object
objc_setAssociatedObject()
方法内部调用了_object_set_associative_reference()
,将参数透传。_object_set_associative_reference
的实现在 objc-references.mm 文件中。该文件包含了 ObjC 关联对象的相关实现,ObjcAssociation
类是对关联对象的封装,包括关联策略(policy)以及 value 、初始化方法以及一些内部方法,这里包括根据不同关联策略对 value 的处理。AssociationsManager
主要用来管理 Association Map 及相关的锁(spinlock_t)操作。
关于 hash map 可以看下下面的定义:1
2typedef DenseMap<const void *, ObjcAssociation> ObjectAssociationMap;
typedef DenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap> AssociationsHashMap;
可以看到关联对象是通过二层 map 来保存的,每个对象的关联对像保存在 ObjectAssociationMap
中,而所有对象的 ObjectAssociationMap
被保存在全局的 AssociationsHashMap
中。
注意到这里用到了一个数据结构 DenseMap
,它经常在 ObjC runtime 中出现,该结构是一个基于二次探查法的 hash map ,具体实现在 llvm-DenseMap.h 中。
下面回到_object_set_associative_reference()
的实现上来,首先:1
2
3
4
5
6
7
8
9void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
// This code used to work when nil was passed for object and key. Some code
// probably relies on that to not crash. Check and handle it explicitly.
// rdar://problem/44094390
if (!object && !value) return;
...
}
这里限定了 object 和 value 不得同时为 nil ,其实在 runtime.h 中对 object 和 key 参数添加了 _Nonnull
约束,显示传入 nil 编译器会警告,但就算 object 传入 self 也可能会有 nil 的情况,这时会得到运行时错误:EXC_BAD_ACCESS ,而 key 为 nil 时会关联失败。当 value 为 nil 时,这个关联对象会被 erase ,即移除了该 key 下的关联,具体做法后面会看到。
下面是对禁止添加关联对象的宏进行检查:1
2
3
4
5
6
7
8
9{
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));
...
}
bool forbidsAssociatedObjects() {
return (data()->flags & RW_FORBIDS_ASSOCIATED_OBJECTS);
}
下面开始创建存入 hash map 的 key 和 value :1
2
3
4
5
6{
...
DisguisedPtr<objc_object> disguised{(objc_object *)object};
ObjcAssociation association{policy, value};
...
}
其中,key 的类型为 DisguisedPtr<T>
,其作用是通过 disguised
方法获得指向 object 的指针(uintptr_t)。而 value 就是我们之前提到的 ObjcAssociation
,将 policy 和 value 封装其中。
然后根据 policy 对 value 进行内存管理:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18{
// retain the new value (if any) outside the lock.
association.acquireValue();
}
// ObjcAssociation
inline void acquireValue() {
if (_value) {
switch (_policy & 0xFF) {
case OBJC_ASSOCIATION_SETTER_RETAIN:
_value = objc_retain(_value);
break;
case OBJC_ASSOCIATION_SETTER_COPY:
_value = ((id(*)(id, SEL))objc_msgSend)(_value, @selector(copy));
break;
}
}
}
接下来通过 AssociationsManager
获取全局的 AssociationsHashMap
,manager 在初始化时会加锁:1
2
3
4
5
6{
...
AssociationsManager manager;
AssociationsHashMap &associations(manager.get());
...
}
接下来是插入操作,实现上还是比较简单的,可直接看代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29...
if (value) {
auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
if (refs_result.second) {
/* it's the first association we make */
isFirstAssociation = true;
}
/* establish or replace the association */
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);
}
}
}
}
...
try_emplace()
方法特点是如果 container 中存在与参数相等的 key ,则直接返回,否则插入 pair 。因此如果返回值的 second 不为空即证明是第一次插入。如果要更新 value 的话,则需要手动 swap 一次。
如果 value 为 nil ,通过二次遍历找到 key 对应的 old value 后将其移除,如果在移除后该对象没有任何关联对象,则 associations 会将整个 ObjcAssociation
对象移除。
Get Associated Object
Get 方法的实现相对简单,经过两层遍历去寻找关联对象,找不到会返回 nil :1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21id
_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();
}
Remove Assocations
移除方法 _object_remove_assocations(id object, bool deallocating)
中可以看到有一个 deallocating
参数,这是因为 runtime 在销毁对象的 objc_destructInstance
方法中会主动调用移除该对象所有关联对象的方法,如果关联对象为系统对象 SYSTEM_OBJECT
则仅会在 deallocating 时销毁,主动调用时系统对象会被保留:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44void
_object_remove_assocations(id object, bool deallocating)
{
ObjectAssociationMap refs{};
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.get());
AssociationsHashMap::iterator i = associations.find((objc_object *)object);
if (i != associations.end()) {
refs.swap(i->second);
// If we are not deallocating, then SYSTEM_OBJECT associations are preserved.
bool didReInsert = false;
if (!deallocating) {
for (auto &ref: refs) {
if (ref.second.policy() & OBJC_ASSOCIATION_SYSTEM_OBJECT) {
i->second.insert(ref);
didReInsert = true;
}
}
}
if (!didReInsert)
associations.erase(i);
}
}
// Associations to be released after the normal ones.
SmallVector<ObjcAssociation *, 4> laterRefs;
// release everything (outside of the lock).
for (auto &i: refs) {
if (i.second.policy() & OBJC_ASSOCIATION_SYSTEM_OBJECT) {
// If we are not deallocating, then RELEASE_LATER associations don't get released.
if (deallocating)
laterRefs.append(&i.second);
} else {
i.second.releaseHeldValue();
}
}
for (auto *later: laterRefs) {
later->releaseHeldValue();
}
}
从上述分析看到 runtime 提供的关联对象本质上是通过全局的 hash map 对运行时传入的值与对象进行关联,并提供内存管理策略:1
2
3
4
5
6
7typedef 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.
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**< Specifies that the associated object is copied.
OBJC_ASSOCIATION_RETAIN = 01401, /**< Specifies a strong reference to the associated object.
OBJC_ASSOCIATION_COPY = 01403 /**< Specifies that the associated object is copied.
};
这些策略已经能符合大多数的需求,但如果我们想关联一个 weak 对象该如何做呢?上面提到对象与其关联对象拥有相同的生命周期,即对象的关联对象 map 会在析构时被释放,而 get 方法本质上是去查这个对象的 ObjcAssociation 表,这样自然做不到 weak reference 的特性。而 weak reference 也是 runtime 通过类似 Associated Object 的双层 Dense Map 结构实现的。
因此下面就来探讨一下如何实现一个 weak Associated Object 。
Weak Associated Object
其实思路比较简单,通过增加一个中间层的方式来实现。上面的源码看到,当拥有 Associated Object 的对象被析构,policy 为 RETAIN 的 Associated Object 会被发送 objc_release
消息。这样这个 Associated Object 的 weak reference 便可正常的置为 nil,具体如下 :
一个 weak Associated Object 代理类,拥有一个 weak 修饰的属性1
2
3
4
5@interface WeakAssociatedObjectProxy : NSObject
@property (nonatomic, weak) id object;
@end
想要类的 category 中进行对象关联:1
2
3
4
5
6
7
8
9
10
11
12
13@implementation NSObject (Weak)
-(NSString *) myObject {
WeakAssociatedObjectProxy * proxy = objc_getAssociatedObject (self, @selector (myObject));
return proxy.object;
}
-(void) setMyObject:(NSString *) myObject {
WeakAssociatedObjectProxy *proxy = [[WeakAssociatedObjectProxy alloc]init];
proxy.object = myObject;
objc_setAssociatedObject (self, @selector (myObject), proxy, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
用 RETAIN 关联的 proxy 对象在 self 被析构时会被发送 release 消息,进而会将其 weak 属性设为 nil 。
另一种方式可以通过 block 来作为中间层,通过 COPY 的 policy 让 block 持有传入的 myObject ,这种方式更加简洁:1
2
3
4
5
6
7
8
9
10
11
12
13@implementation NSObject (Weak)
-(NSString *) myObject {
id (^block)(void) = objc_getAssociatedObject (self, @selector (myObject));
return (block ? block () : nil);
}
-(void) setMyObject:(NSString *) myObject {
id __weak weakObject = myObject;
id (^block)(void) = ^{ return weakObject; };
objc_setAssociatedObject (self, @selector (myObject), block, OBJC_ASSOCIATION_COPY);
}
@end