通常我们会在分类中添加方法,而无法在在分类中添加属性,我们在分类中添加@property(nonatomic, copy) NSString *name;
时编译器并不会在编译时帮我们自动生成setter和getter方法,也不会生成”_属性名“的成员变量。
但我们可以通过关联对象技术给类添加属性。例如,我们要给Animal
类添加一个name
属性,可以这么实现:
@interface Animal (Cate)
@property(nonatomic, copy) NSString *name;
@end
@implementation Animal (Cate)
- (void)setName:(NSString *)name {
objc_setAssociatedObject(self, "name", name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name {
return objc_getAssociatedObject(self, "name");
}
@end
关联对象技术我们使用了使用了两个api -- objc_setAssociatedObject
和objc_getAssociatedObject
,它的底层是如何实现的呢,我们可以通过objc-7.8.1探究。
objc_setAssociatedObject
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
SetAssocHook.get()(object, key, value, policy);
}
SetAssocHook
是一个封装了一个函数指针的对象,他在源码里是这么定义的
static ChainedHookFunction<objc_hook_setAssociatedObject> SetAssocHook{_base_objc_setAssociatedObject};
关于ChainedHookFunction
,点进去查看它的实现
// Storage for a thread-safe chained hook function.
// get() returns the value for calling.
// set() installs a new function and returns the old one for chaining.
// More precisely, set() writes the old value to a variable supplied by
// the caller. get() and set() use appropriate barriers so that the
// old value is safely written to the variable before the new value is
// called to use it.
//
// T1: store to old variable; store-release to hook variable
// T2: load-acquire from hook variable; call it; called hook loads old variable
template <typename Fn>
class ChainedHookFunction {
std::atomic<Fn> hook{nil};
public:
ChainedHookFunction(Fn f) : hook{f} { };
Fn get() {
return hook.load(std::memory_order_acquire);
}
void set(Fn newValue, Fn *oldVariable)
{
Fn oldValue = hook.load(std::memory_order_relaxed);
do {
*oldVariable = oldValue;
} while (!hook.compare_exchange_weak(oldValue, newValue,
std::memory_order_release,
std::memory_order_relaxed));
}
};
通过它的注释可以了解到,ChainedHookFunction
是用于线程安全链构函数的存储,通过get()
返回调用值,通过set()
安装一个新的函数,并返回旧函数,更确切的说,set()
将旧值写入调用方提供的变量,get()
和 set()
使用适当的栅栏使得在新值调用前安全地写入变量。
所以,SetAssocHook.get()
返回的是传入的函数指针_base_objc_setAssociatedObject
,objc_setAssociatedObject
底层调用的其实是_base_objc_setAssociatedObject
,我们也可以通过符号断点验证调用的是否正确,这里就不做演示。
进入_base_objc_setAssociatedObject
的实现查看,它的底层调用了_object_set_associative_reference
。
static void
_base_objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
_object_set_associative_reference(object, key, value, policy);
}
_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<objc_object> disguised{(objc_object *)object};
ObjcAssociation association{policy, value};
// retain the new value (if any) outside the lock.
association.acquireValue();
{
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 */
object->setHasAssociatedObjects();
}
/* 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);
}
}
}
}
}
// release the old value (outside of the lock).
association.releaseHeldValue();
}
通过阅读源码,关联对象设值流程为:
- 将被关联对象封装成
DisguisedPtr
类型,将策略和关联值封装成ObjcAssociation
类型,并根据策略处理关联值。 - 创建一个
AssociationsManager
管理类 - 获取唯一的全局静态哈希Map
- 判断是否插入的关联值是否存在,如果存在走第4步,如果不存在则执行关联对象插入空流程
- 创建一个空的
ObjectAssociationMap
去取查询的键值对 - 如果发现没有这个key就插入一个空的
BucketT
进去返回 - 标记对象存在关联对象
- 用当前修饰策略和值组成了一个
ObjcAssociation
替换原来BucketT
中的空 - 标记一下
ObjectAssociation
的第一次为false
关联对象插入空流程为:
- 根据
DisguisedPtr
找到AssociationsHashMap
中的迭代查询器、 - 清理迭代器
- 插入空值(相当于清除)
处理传入变量
DisguisedPtr<objc_object> disguised{(objc_object *)object};
ObjcAssociation association{policy, value};
// retain the new value (if any) outside the lock.
association.acquireValue();
这部分代码比较简单,可以进入association.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
是OBJC_ASSOCIATION_SETTER_RETAIN
,则对关联值进行retain
操作,如果是OBJC_ASSOCIATION_SETTER_COPY
,则对关联值进行copy
操作,其他的则不做任何处理。
创建哈希表管理类获取全局哈希表
class AssociationsManager {
using Storage = ExplicitInitDenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap>;
static Storage _mapStorage;
public:
AssociationsManager() { AssociationsManagerLock.lock(); }
~AssociationsManager() { AssociationsManagerLock.unlock(); }
AssociationsHashMap &get() {
return _mapStorage.get();
}
static void init() {
_mapStorage.init();
}
};
在AssociationsManager
的构造方法里,通过AssociationsManagerLock.lock
加了一把锁,在当前AssociationsManager
释放之前,后续创建的AssociationsManager
都无法对其管理的资源进行操作,从而保证了线程安全,在通过get()
拿到全局唯一的哈希表(因为_mapStorage
是static
修饰的)
关联非空值流程
当要关联的值非空时,我们需要将这个值与当前对象关联起来,这一部分代码为
auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
if (refs_result.second) {
/* it's the first association we make */
object->setHasAssociatedObjects();
}
/* 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);
}
通过源码,一步步分析它的原理
分析 try_emplace 实现流程
try_emplace
是DenseMap
类的一个方法,这个方法在这个流程中被调用两次,第一次调用的是全局关联对象哈希表的try_emplace
,传了封装了当前对象的disguised
作为key
和一个空的ObjectAssociationMap
作为第二个元素,第二次,调用的第一次try_emplace
得到的map,传递了关联对象的key值作为key
,传递了封装value和策略的association
作为第二个参数。
try_emplace
内部到底做了什么事情呢,我们可以去源码一探究竟。
// Inserts key,value pair into the map if the key isn't already in the map.
// The value is constructed in-place if the key is not in the map, otherwise
// it is not moved.
template <typename... Ts>
std::pair<iterator, bool> 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<Ts>(Args)...);
return std::make_pair(
makeIterator(TheBucket, getBucketsEnd(), true),
true);
}
try_emplace
具体流程分析如下:
- 创建一个空的
BucketT
,调用LookupBucketFor
查找Key
对应的BucketT
,如果能够找到,将找到的BucketT
赋值给刚刚创建的那个空的BucketT
,并将BucketT
封装成DenseMapIterator
作为类对的第一个元素,将false
作为第二个元素,并将该类对返回,此时的BucketT
存放的是上次存放的key
和value
- 如果没有找到,那么将传入的Key和Value插入创建新的
BucketT
,同样创建一个DenseMapIterator
和Bool
组成的类对,只不过此时传递布尔值为true
,此时的BucketT
已经存放了传入的key
和value
LookupBucketFor
这个方法是在当前DenseMap
中通过key
查找对应的bucket
,如果找到匹配的,并且bucket
包含key和value,则返回true
,并将找到bucket
通过FoundBucket
返回,否则,返回false
并通过FoundBucket
返回一个空的bucket
。
另外贴上代码
/// LookupBucketFor - Lookup the appropriate bucket for Val, returning it in
/// FoundBucket. If the bucket contains the key and a value, this returns
/// true, otherwise it returns a bucket with an empty marker or tombstone and
/// returns false.
template<typename LookupKeyT>
bool LookupBucketFor(const LookupKeyT &Val,
const BucketT *&FoundBucket) const {
const BucketT *BucketsPtr = getBuckets();
const unsigned NumBuckets = getNumBuckets();
if (NumBuckets == 0) {
FoundBucket = nullptr;
return false;
}
// FoundTombstone - Keep track of whether we find a tombstone while probing.
const BucketT *FoundTombstone = nullptr;
const KeyT EmptyKey = getEmptyKey();
const KeyT TombstoneKey = getTombstoneKey();
assert(!KeyInfoT::isEqual(Val, EmptyKey) &&
!KeyInfoT::isEqual(Val, TombstoneKey) &&
"Empty/Tombstone value shouldn't be inserted into map!");
unsigned BucketNo = getHashValue(Val) & (NumBuckets-1);
unsigned ProbeAmt = 1;
while (true) {
const BucketT *ThisBucket = BucketsPtr + BucketNo;
// Found Val's bucket? If so, return it.
if (LLVM_LIKELY(KeyInfoT::isEqual(Val, ThisBucket->getFirst()))) {
FoundBucket = ThisBucket;
return true;
}
// If we found an empty bucket, the key doesn't exist in the set.
// Insert it and return the default value.
if (LLVM_LIKELY(KeyInfoT::isEqual(ThisBucket->getFirst(), EmptyKey))) {
// If we've already seen a tombstone while probing, fill it in instead
// of the empty bucket we eventually probed to.
FoundBucket = FoundTombstone ? FoundTombstone : ThisBucket;
return false;
}
// If this is a tombstone, remember it. If Val ends up not in the map, we
// prefer to return it than something that would require more probing.
// Ditto for zero values.
if (KeyInfoT::isEqual(ThisBucket->getFirst(), TombstoneKey) &&
!FoundTombstone)
FoundTombstone = ThisBucket; // Remember the first tombstone found.
if (ValueInfoT::isPurgeable(ThisBucket->getSecond()) && !FoundTombstone)
FoundTombstone = ThisBucket;
// Otherwise, it's a hash collision or a tombstone, continue quadratic
// probing.
if (ProbeAmt > NumBuckets) {
FatalCorruptHashTables(BucketsPtr, NumBuckets);
}
BucketNo += ProbeAmt++;
BucketNo &= (NumBuckets-1);
}
}
非空值流程流程分析
通过阅读源码,可以总结出他的流程为:
- 尝试通过关联对象全局hashMap的
try_emplace
方法找到当前对象对应的bucket
,此时的bucket
的key
为当前对象,value为一张ObjectAssociationMap
,也是一张hashMap - 如果查找返回的
refs_result
类对第二个元素为true
,也就是说当前对象第一次有关联对象,将当前对象标记为有关联对象(修改isa) - 通过
refs_result.first->second
拿到当前对象对应ObjectAssociationMap
,调用这张ObjectAssociationMap
的try_emplace
找到关联对象标识符对应的value,也就是value
和policy
组装成的ObjcAssociation
对象 - 如果第二个
try_emplace
方法返回的result
的第二个元素为true
说明这是该对象第一次插入该标识符的值,此时的value
和policy
已经在try_emplace
插入到ObjectAssociationMap
,不需要进一步处理 - 如果
result.second
为false
,说明原先的对象的该标识符对应的关联对象有值,调用association.swap(result.first->second)
交换修改关联对象(result.first->second
存放的是value
和policy
组装成的ObjcAssociation
对象)
关联空值流程
关联空值其实就是删除关联对象,这部分代码为:
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);
}
}
}
其实通过非空关键对象流程的探究对关联对象原理了解,这部分代码就显得简单很多。
- 以用当前对象封装的
DisguisedPtr
对象为key
在全局关联对象表associations
中查找得到refs_it
- 如果
refs_it
不等于associations
的最后一个元素,通过refs_it->second
拿到当前对象对象的ObjectAssociationMap
对象refs
,也就是存放标识符和ObjcAssociation
对象it
(value和policy)的那张哈希表 - 通过标识符找到
ObjcAssociation
- 如果
it
不是refs
最后一个元素,交换原有标识符对应的关联对象,因为传入的为空,所以交换后标识符对应的对象为空 - 调用
refs.erase(it)
,删除该标识符对应的bucket
- 如果此时对象对应的
ObjectAssociationMap
大小为0,则删除该对象对应的关联表
objc_getAssociatedObject
objc_getAssociatedObject
底层调用的是_object_get_associative_reference
id
objc_getAssociatedObject(id object, const void *key)
{
return _object_get_associative_reference(object, key);
}
_object_get_associative_reference
进入_object_get_associative_reference
id
_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();
}
这个部分代码比较简单,大致可以分为以下几个步骤:
- 创建
AssociationsManager
对象,通过AssociationsManager
对象拿到全局唯一的关联对象管理表 - 以对象为
key
在关联对象表中查找对象的AssociationsHashMap::iterator
- 如果找到了,取到
iterator
的第二个元素,也就是ObjectAssociationMap
,用标识符为key在这个ObjectAssociationMap
查找 - 如果找到了,取找到的
refs
的第二个元素,也就是set时存放的value
(第一个为policy
),返回 - 以上如果都没有找到,返回nil
总结
关联对象其实使用了两张hashMap,可以用一张图解释他的原理。