前言
相关目标是用来为分类增加成员变量时运用的,那么为什么分类需求运用相关目标来增加成员变量呢?那肯定是由于正常的增加成员变量的方式在分类中不能用。
一般咱们在类中声明一个特点,代码是这样的:
@interface Animal: NSObject
@property (nonatomic, copy) NSString *name;
@end
然后编译器会帮咱们生成如下办法:
@implementation Animal {
NSString *_name;
}
- (NSString *)name {
return _name;
}
- (void)setName:(NSString *)name {
_name = name;
}
便是:
- 生成一个实例变量
_name
- 生成
getter
办法 - 生成
setter
办法
编译器帮咱们生成了一个实例变量,然后存到在类结构中。
当咱们尝试往一个分类中去增加一个特点:
@interface Animal (Category)
@property (nonatomic, copy) NSString *gender;
@end
咱们会得到以下警告:
Property 'gender' requires method 'gender' to be defined - use @dynamic or provide a method implementation in this category
意思便是 gender
特点的存取办法需求自己去手动完成,或许运用 @dynamic
在运行时完成这些办法。
由于在分类中,虽然能够经过 @property
来增加特点,可是不会主动生成私有成员变量,也不会生成 set
和 get
办法,只会生成 set
、get
的声明,详细的办法完成需求咱们自己去完成。
从 Category 讲起
至于为什么不主动给分类生成私有成员变量,最直接的原因便是分类不支持,先来看看类的结构,它在源码中是一个名为 objc_class
的结构体:
struct objc_class {
Class _Nonnull isa;
struct objc_ivar_list * _Nullable ivars; // 成员变量列表
struct objc_method_list * _Nullable * _Nullable methodLists; // 办法列表
struct objc_protocol_list * _Nullable protocols; // 协议列表
...
}
其间有三个成员:
-
ivars
:成员变量列表 -
methodLists
:办法列表 -
protocols
:协议列表
分类在源码中的结构如下,它是一个名为 category_t
的结构体:
struct category_t {
const char *name; // 对应的类名
WrappedPtr<method_list_t, method_list_t::Ptrauth> instanceMethods; // 实例办法列表
WrappedPtr<method_list_t, method_list_t::Ptrauth> classMethods; // 类办法列表
struct protocol_list_t *protocols; // 协议列表
struct property_list_t *instanceProperties; // 特点列表
...
}
它包含:
-
instanceMethods
:实例办法列表 -
protocols
:协议列表 -
instanceProperties
:特点列表
能够看到它是没有成员变量列表的,这便是为什么分类不允许增加成员变量的最直接的原因,从结构规划上就不支持。
为什么要这么规划呢?
其实得问苹果为什么要这么规划,我个人觉得规划成能够增加成员变量好像也没毛病?虽然我没想出来是为啥,不过还是放一份别人的答案,看懂的同学能够指点下:
总得来说便是,
Category
是在运行时才会被运行时库(也便是Runtime
)加载到内存中,而类的内存布局在编译时就已经确定了,不能够再更改。所以不允许增加成员变量,是由于增加成员变量会影响到 “类实例” 的内存布局,所谓 “类实例”,它便是咱们创立出来的类目标,它是一块包含
isa
指针和一切的成员变量的内存区域,咱们不能在运行时再去改变它,而成员变量的增加会直接影响到它的内存的,所以不允许在运行时再增加成员变量。而办法/协议/特点不属于 “类实例” 这个概念,它们归类管,也便是
objc_class
,不论如何增删,都不会影响到 “类实例” 的内存,所以能够随意增删。
详细的原因我还没想明白,可是不论咋说,从源码上看,苹果它就不支持你去在分类中增加成员变量,假如咱们想达到向正常类那样去运用一个特点(会主动生成实例变量和 set
/get
办法),那么咱们能够借助相关目标(主角进场的有点晚)。
当然,相关目标和正常的成员变量在底层是大不相同的,不过运用相关目标完成的特点和正常的特点在运用上并无二致。
运用相关目标
在 Animal
的分类中增加一个 age
特点:
#import "objc/runtime.h"
@interface Animal (Category)
@property (nonatomic, copy) NSString *categoryName;
@end
@implementation Animal (Category)
- (void)setCategoryName:(NSString *)categoryName {
objc_setAssociatedObject(self, @"categoryName", categoryName, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)categoryName {
return objc_getAssociatedObject(self, @"categoryName");
}
@end
运用时:
Animal *animal = [[Animal alloc] init];
animal.categoryName = @"Tom";
NSLog(@"%@", animal.categoryName);
控制台输出:
Tom
就像正常特点相同进行运用即可。
相关目标的完成原理
经过上面的比如,能够看到两个关键的 api,objc_setAssociatedObject
和 objc_getAssociatedObject
,一个是设置,一个是获取,咱们到源码中看看它们做了什么。
void
objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
{
_object_set_associative_reference(object, key, value, policy);
}
老样子,又是调了另一个办法:
void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
DisguisedPtr<objc_object> disguised{(objc_object *)object}; // (1)
ObjcAssociation association{policy, value}; // (2)
AssociationsManager manager; (3)
AssociationsHashMap &associations(manager.get()); (4)
auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
auto &refs = refs_result.first->second;
auto result = refs.try_emplace(key, std::move(association));
...
// 开释旧值
association.releaseHeldValue();
}
这儿省掉了部分代码,咱们需求留意里面的几个类和数据结构,在详细剖析代码之前,需求先了解它们的作用。
- DisguisedPtr
- ObjcAssociation
- AssociationsManager
- AssociationsHashMap
- ObjectAssociationMap
DisguisedPtr
是一个 class
:
class DisguisedPtr {
uintptr_t value;
...
}
它只要一个成员,经过 (1)
咱们能够看到,它传入的是 object
,对应的便是 objc_setAssociatedObject(self, @"categoryName", categoryName, OBJC_ASSOCIATION_COPY_NONATOMIC);
中的 self
。
也便是将 self
放到了一个类中。
再看 ObjcAssociation
:
class ObjcAssociation {
uintptr_t _policy;
id _value;
...
}
ObjcAssociation
只要两个成员,_policy
是 objc_AssociationPolicy
,它是一个枚举:
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403
};
而 value
便是相关目标对应的值。
和 DisguisedPtr
相似,而且经过 (2)
能够看出,是将相关目标的 OBJC_ASSOCIATION_COPY_NONATOMIC
和相关目标的 value
,也便是 categoryName
一起放入了这个目标。
接下来便是 AssociationsManager
:
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();
}
};
其间的 AssociationsManagerLock
是一个自旋锁:
spinlock_t AssociationsManagerLock;
所以它有一个 spinlock_t
(自旋锁)和 AssociationsHashMap
单例,在 &get
办法中回来的是一个大局的 AssociationsHashMap
单例,然后 AssociationsManager
经过持有一个 spinlock_t
来确保对 AssociationsHashMap
的操作是线程安全的。
(4)
中便是经过 &get
办法,拿到了 AssociationsHashMap
。
AssociationsHashMap
的界说为:
typedef DenseMap<DisguisedPtr<objc_object>, ObjectAssociationMap> AssociationsHashMap;
是一个 [DisguisedPtr : ObjectAssociationMap]
的字典,ObjectAssociationMap
的界说如下:
typedef DenseMap<const void *, ObjcAssociation> ObjectAssociationMap;
是一个 [void * : ObjcAssociation]
的字典,而 ObjcAssociation
咱们前面提到过,它存放了相关目标的修饰符和相关目标的值,也能够说它便是咱们所说的 “相关目标” 实践在内存中的结构。
看到这儿,咱们能够得到相关目标的一个存储结构,以咱们在 Animal
中的 categoryName
为例,假如将 categoryName
设置为 Tom
,它在内存中是这么存储的:
所以相关目标是经过一个大局的单例类来办理的,存储的结构也是一个哈希表的结构,所以咱们能够幻想出来,当需求一个目标增加了相关目标,它会以线程安全的方式(加锁)存储到一个大局的表中,key
是目标的 id(地址),而 value
是相关目标的修饰符和值。
那么开释的机遇咱们也很简单揣度出来,只需求在目标的析构办法中,去这个大局的表中以目标的 id 为 key
移除去与它相关的相关目标即可。
从 dealloc
办法中去找:
dealloc
-> rootDealloc
-> object_dispose
-> objc_destructInstance
-> _object_remove_assocations
,_object_remove_assocations
便是开释相关目标的办法,界说如下:
void
_object_remove_assocations(id object, bool deallocating)
{
...
}
能够看到传入了析构目标自身(object
),然后再根据 object
去大局的表中查找该 object
所对应的值,然后移除即可。
源码
剖析完原理之后,咱们来看一下 objc_setAssociatedObject
和 objc_getAssociatedObject
两个办法的详细完成。
void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
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};
association.acquireValue();
bool isFirstAssociation = false;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.get());
if (value) { // a
auto refs_result = associations.try_emplace(disguised, ObjectAssociationMap{});
if (refs_result.second) {
isFirstAssociation = true;
}
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);
}
}
}
}
}
if (isFirstAssociation)
object->setHasAssociatedObjects();
association.releaseHeldValue();
}
提个留意点便是 // a
处,分两种情况:
-
value != nil
,设置/更新相关目标的值 -
value == nil
,删除相关目标
将相关目标的值设置为 nil
的话,会去删除这个相关目标。
objc_getAssociatedObject
办法内部调用了 _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();
}
只放了源码,没有详细到每一条的剖析,个人觉得看个大约,了解原理就能够了。
总结
- 相关目标在源码中其实便是
ObjcAssociation
目标。 -
ObjcAssociation
存放在ObjectAssociationMap
中,然后以目标的指针为key
,ObjectAssociationMap
为value
,存放在AssociationsHashMap
中。 -
AssociationsHashMap
是一个哈希表,由AssociationsManager
办理,AssociationsManager
是一个大局的单例,持有AssociationsHashMap
,AssociationsHashMap
也是大局仅有的一张表。 - 目标在析构函数中,经过
has_assoc
标记位判别目标是否有相关目标,有的话会调用_object_remove_assocations
办法移除相关相关目标。
参考
相关目标 AssociatedObject 彻底解析