什么是关联对象
所谓关联对象就是,让一个对象和一个值(通常也是一个对象或者一个指针)通过给定的key和association policy(关联策略)进行关联。association policy实际是内存管理策略,是一个枚举,它的定义如下:
/**
* Policies related to associative references.
* These are options to objc_setAssociatedObject()
*/
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. */
};
常用的关联对象设置和读取两个接口的声明如下:
/**
* Sets an associated value for a given object using a given key and association policy.
*
* @param object The source object for the association.
* @param key The key for the association.
* @param value The value to associate with the key key for object. Pass nil to clear an existing association.
* @param policy The policy for the association. For possible values, see “Associative Object Behaviors.”
*
* @see objc_setAssociatedObject
* @see objc_removeAssociatedObjects
*/
OBJC_EXPORT void
objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
id _Nullable value, objc_AssociationPolicy policy)
OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);
/**
* Returns the value associated with a given object for a given key.
*
* @param object The source object for the association.
* @param key The key for the association.
*
* @return The value associated with the key \e key for \e object.
*
* @see objc_setAssociatedObject
*/
OBJC_EXPORT id _Nullable
objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key)
OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);
除了关联策略枚举objc_AssociationPolicy,没有其他什么特殊的数据结构。
关联对象的使用
关联对象一般用于运行时给某个对象设置关联,最常用的就是在Category中设置属性时,通过关联对象实现属性的setter和getter方法点击了解Category相关的内容。接下来我们创建一个自定义类,并实现一个Category,演示关联对象的使用:
/*Extension*/
@interface TestObject()
@property(nonatomic, strong) NSString *ext_name;
- (void)ext_testMethod;
@end
const static NSString *kExtName;
@implementation TestObject
- (void)testMethod{
NSLog(@"cate_name:%@",self.cate_name);
}
@end
@implementation TestObject (Cate)
- (void)setCate_name:(NSString *)cate_name
{
NSLog(@"设置self和值的关联");
objc_setAssociatedObject(self, &kExtName, cate_name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)cate_name
{
NSLog(@"self读取关联的值");
return objc_getAssociatedObject(self, &kExtName);
}
- (void)cate_testMethod{
self.cate_name = @"你哈";
}
@end
首先set方法通过调用objc_setAssociatedObject让self和cate_name进行关联,通过kExtName的地址&kExtName作为key值,关联策略OBJC_ASSOCIATION_RETAIN_NONATOMIC表示retain,表示cate_name会被强引用。在get方法里面通过objc_getAssociatedObject读取关联的值。
通过关联对象的使用,我们发现他的存取操作很类似与哈希表的存取操作。那么它底层的结构到底是什么样的呢?
关联对象的底层原理
问题:
关联对象是如何将属性和对象关联的?
关联对象是如何存储的?
关联对象何时会被释放?
关联对象的底层结构
关联对象底层是通过一个二维哈希表来存储的。第一层的哈希表AssociationsHashMap是以对象指针(指针的伪装)为key,value为ObjectAssociationMap,ObjectAssociationMap是以objc_setAssociatedObject传入的key和value构成的哈希表,
关联对象的底层数据结构可以总结如下图所示:
- DenseMap
图中的AssociationsHashMap和ObjectAssociationMap都是DenseMap类型是哈希表:
class DenseMap : public DenseMapBase,
KeyT, ValueT, ValueInfoT, KeyInfoT, BucketT> {
friend class DenseMapBase;
// Lift some types from the dependent base class into this class for
// simplicity of referring to them.
using BaseT = DenseMapBase;
BucketT *Buckets;
unsigned NumEntries;
unsigned NumTombstones;
unsigned NumBuckets;
...
}
- AssociationsManager
class AssociationsManager {
using Storage = ExplicitInitDenseMap, ObjectAssociationMap>;
static Storage _mapStorage;
public:
AssociationsManager() { AssociationsManagerLock.lock(); }
~AssociationsManager() { AssociationsManagerLock.unlock(); }
AssociationsHashMap &get() {
return _mapStorage.get();
}
static void init() {
_mapStorage.init();
}
};
typedef DenseMap, ObjectAssociationMap> AssociationsHashMap;
这里边首先注意的一点是AssociationsManager不是单利。而是他的属性static Storage _mapStorage是个单利。_mapStorage其实就是一个AssociationsHashMap,关联对象都存在这个表里。
- AssociationsHashMap
typedef DenseMap ObjectAssociationMap;
AssociationsHashMap中的DisguisedPtr
- ObjectAssociationMap
typedef DenseMap, ObjectAssociationMap> AssociationsHashMap;
ObjectAssociationMap中的const void *就是objc_setAssociatedObject的key参数,ObjcAssociation是用来接收policy和value的,这样第二层哈希Map就建立了这个key和value的映射关系。
这样设计的好处是:第一层保证可以有多个对象object被关联,第二层保证了每个对象可以关联多个key和value。
插入流程源码解析
通过源码解析了解关联对象底层原理。我们直接通过源码的调用逻辑来进行分析。其调用流程是objc_setAssociatedObject -> _object_set_associative_reference,它的主要实现逻辑在_object_set_associative_reference函数里面。接下来看它的实现:
void
_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;
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));
DisguisedPtr disguised{(objc_object *)object};
ObjcAssociation association{policy, value};
// retain the new value (if any) outside the lock.
association.acquireValue();
bool isFirstAssociation = false;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.get());
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);
}
}
}
}
}
// Call setHasAssociatedObjects outside the lock, since this
// will call the object's _noteAssociatedObjects method if it
// has one, and this may trigger +initialize which might do
// arbitrary stuff, including setting more associated objects.
if (isFirstAssociation)
object->setHasAssociatedObjects();
// release the old value (outside of the lock).
association.releaseHeldValue();
}
1、首先对关联的对象指针进行包装,得到disguised:
DisguisedPtr disguised{(objc_object *)object};
2、创建ObjcAssociation的实例用于接收policy和value,通过acquireValue方法对value进行内存管理:
ObjcAssociation association{policy, value};
acquireValue();
//acquireValue的实现
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;
}
}
}
根据policy,对value进行retain或者copy操作,如果是assign,则不处理。
3、接下来就创建一个管理类AssociationsManager。
AssociationsManager manager;
AssociationsManager包含一个_mapStorage静态变量,这个静态变量是为了实现单利,存储全局唯一的二维哈希表AssociationsHashMap,用于所有关联对象的存储。
4、读取全局唯一的关联对象二维哈希表AssociationsHashMap:
AssociationsHashMap &associations(manager.get());
有了这个表之后,就看进入关联对象如何插入associations表的过程。
5、判断value如果不为空:
5.1、创建一个空的ObjectAssociationMap,去查询是否有已经关联的键值对表:
auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
5.2、如果是第一次插入数据,则记录isFirstAssociation状态:
if (refs_result.second) {
/* it's the first association we make */
isFirstAssociation = true;
}
5.3、如果已经存在则直接返回;如果不存在则新建一个Bucket
(ObjectAssociationMap),然后返回:
template
std::pair try_emplace(const KeyT &Key, Ts &&... Args) {
BucketT *TheBucket;
if (LookupBucketFor(Key, TheBucket))
return std::make_pair(
makeIterator(TheBucket, getBucketsEnd(), true),
false); // Already in map.
// Otherwise, insert the new element.
TheBucket = InsertIntoBucket(TheBucket, Key, std::forward(Args)...);
return std::make_pair(
makeIterator(TheBucket, getBucketsEnd(), true),
true);
}
5.4、插入数据:
/* 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);
}
6、如果value不为空
6.1、根据DisguisedPtr找到AssociationHashMap中的iterator迭代查询器;
auto refs_it = associations.find(disguised);
6.2、清理迭代器;
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);
}
}
}
7、如果是第一次插入数据,标记对象已被关联:
if (isFirstAssociation)
object->setHasAssociatedObjects();
8、关联新的value需要对旧的value进行释放:
// release the old value (outside of the lock).
association.releaseHeldValue();
总结:以上的插入流程可以总结为如下
1、创建一个AssociationsMaanger管理类;
2、获取全局唯一的静态哈希Map;
3、判断当前插入的关联值(value)是否存在。如果存在,走关联值value不为空的流程;如果不存在,走关联值value为空的流程;
4、关联值value不为空的流程:
4.1、创建一个空的ObjectAssociationMap去取查询键值对;
4.2、如果发现没有这个key,就插入一个空的Bucket进去,也就是第一步的ObjectAssociationMap,第一次正常都是空的,然后返回;
4.3、标记对象存在关联对象;
4.4、用当前修饰策略和值组成ObjcAssociation替换原来BuckeT中的空值;
4.5、标记一下ObjectAssociationMap的第一次为false;
5、关联值value为空的流程:
5.1、根据DisguisedPtr找到AssociationHashMap中的iterator迭代查询器;
5.2、清理迭代器;
备注:这里要注意的一点是,当value为空,而key不为空,它其实就是一个删除操作。
关联对象何时被删除
关联对象在对象被释放时会被清理,详情参考iOS内存管理底层原理;