前情回顾
回顾一下什么是分类、什么是扩展:
分类(Category)和类扩展(Extension)是两种常见的代码组织方式,用于扩展类的功能。
分类(Category)
分类是一种将类的实现分散到多个源文件的方式。通过使用分类,你可以将一个类的实现分散到多个源文件中。分类可以为现有的类添加新的方法,这使得你可以向现有的类添加自己的方法。
分类不能添加实例变量,只能添加方法。
分类的特性是可以在运行时阶段动态的为已有的类添加新行为。
分类就是对装饰模式的一种具体实现。它的主要作用是在不改变原有类的前提下,动态地给这个类添加一些方法。
分类也可以把framework私有方法公开化
现在我们通过源码看一下分类:
struct _category_t {
const char *name; // 分类的名称
struct _class_t *cls; // 分类所属的类
const struct _method_list_t *instance_methods; // 分类中定义的实例方法列表
const struct _method_list_t *class_methods; // 分类中定义的类方法列表
const struct _protocol_list_t *protocols; // 分类实现的协议列表
const struct _prop_list_t *properties; // 分类中定义的属性列表
};
extern "C" __declspec(dllimport) struct objc_cache _objc_empty_cache;
- 从结构体可以知道,分类可以添加属性,但是只会生成这些属性的getter和setter方法的声明,并不会自动实现这些方法。也就是说,如果你在分类中添加了一个属性,你还需要自己去实现这个属性的getter和setter方法。
- 分类不像类,它没有成员变量列表,所以你不能在分类中添加成员变量。
- 分类中的方法可以在运行时动态改变,但结构体不能。如果你想要在运行时给原来的结构体添加一个属性,你只能通过关联对象的方式。关联对象的本质是在类的定义之外为类增加额外的存储空间,实现了一种映射关系。
扩展(Extension)
类扩展与分类类似,有时候也被称为匿名分类,但是类扩展可以添加实例变量和属性。与分类不同,类扩展的方法和属性必须在类的主实现文件中实现。扩展中声明的所有方法和属性必须在主类中实现,否则会导致编译错误。此外,类扩展中添加的实例变量默认为@private类型,其使用范围只能在自身类中。
扩展是在编译阶段与该类同时编译的,是类的一部分。扩展中声明的方法只能在该类的@implementation中实现。所以这也就意味着我们无法对系统的类使用扩展。
分类和扩展的区别
- 分类原则上只能增加方法,但是也可以通过关联属性增加属性
- 拓展可以增加方法和属性,都是私有的。
- 扩展只能在自身类中使用,而不是子类或者其他地方。
- 类扩展是在编译阶段添加到类中,而分类是在运行时添加到类中
关联对象
在OC中,关联对象是一种动态给对象添加属性的机制。
正如我们知道的,OC的类可以有实例变量和属性,这些都是在编译时期就已经确定的。然而,有时候我们可能希望在运行时给对象动态添加一些属性,就可以用关联对象。
关联对象还可以为分类添加属性:
Category(分类)的底层结构中,没有成员变量(ivar),因此不能给分类添加成员变量;在分类里面声明的属性,只会生成 get/set 方法的声明,没有方法的实现,所以我们不能直接给分类添加成员变量,但是可以间接实现,那就是使用关联对象
关联对象实际上是通过Objective-C的运行时系统实现的。使用以下三个函数来添加、获取或者删除关联对象:
给对象添加关联对象:
void objc_setAssociatedObject(id object, const void * key, id value, objc_AssociationPolicy policy)
其中objc_AssociationPolicy policy
是指关联策略。
获取对象的关联对象:
id objc_getAssociatedObject(id object, const void * key)
删除对象的所有关联对象:
void objc_removeAssociatedObjects(id object)
使用方法:
- 创建ZSXPerson类,以及它的分类ZSXPerson+Text
- ZSXPerson+Text.h中声明想要添加的属性
- ZSXPerson+Text.m中使用关联对象API实现 get/set 方法
修饰符
关联对象并不会改变对象的类或类的定义,它们只是在运行时环境中与特定的对象关联在一起。关联对象的生命周期与对象本身的生命周期相同,当对象被销毁时,其关联的对象也会被销毁。
为分类添加关联对象的本质
关联对象的本质
每一个对象的关联对象实际上都存储在AssociationHashMap内。所有对象的关联内容都在同一个全局容器中。
关联对象由AssociationManager管理并在AssociationHashMap存储。
所以说, 假如给分类添加了一个关联对象, 那么该关联内容既不需要分类管理, 也不是由原类管理。
class AssociationsManager {
static spinlock_t _lock;
static AssociationsHashMap *_map; // associative references: object pointer -> PtrPtrHashMap.
public:
AssociationsManager() { _lock.lock(); }
~AssociationsManager() { _lock.unlock(); }
AssociationsHashMap &associations() {
if (_map == NULL)
_map = new AssociationsHashMap();
return *_map;
}
};
AssociationsHashMap内部维护了一个ObjectAssociationMap哈希表:
class AssociationsHashMap : public unordered_map<disguised_ptr_t, ObjectAssociationMap *, DisguisedPointerHash, DisguisedPointerEqual, AssociationsHashMapAllocator> {
public:
void *operator new(size_t n) { return ::malloc(n); }
void operator delete(void *ptr) { ::free(ptr); }
};
ObjectAssociationMap内部维护了一个ObjcAssociation哈希表:
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); }
};
key的常见用法
使用的get方法的@selecor作为key
- objc_setAssociatedObject(obj, @selector(getter), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
- objc_getAssociatedObject(obj, @selector(getter))
使用指针的地址作为key
static void *MyKey = &MyKey;
- objc_setAssociatedObject(obj, MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
- objc_getAssociatedObject(obj, MyKey)
使用static字符作为key
static char MyKey;
- objc_setAssociatedObject(obj, &MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
- objc_getAssociatedObject(obj, &MyKey)
使用属性名作为key
- objc_setAssociatedObject(obj, @“property”, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
- objc_getAssociatedObject(obj, @“property”);
错误用法举例
static void *MyKey;
- objc_setAssociatedObject(obj, MyKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC)
- objc_getAssociatedObject(obj, MyKey)
如上写法,没有给*MyKey,他相当于是NULL,这时候,如果再有另一个属性使用Keystatic void *OtherKey;,MyKey和OtherKey都是 NULL,这就相当于把多个属性都与NULLKey关联,明显就出问题了
关联对象的实现原理
学习OC对象本质时,我们知道对象实际被转化成struct ClassName_IMPL结构体,对象的成员变量的值保存在该结构体中。
使用关联对象给分类添加的成员变量,他并不是保存在该结构体下,因为Category
的底层结构中并没有ivar
。
他是存储在全局的统一的一个AssociationsManager
中。
核心对象
- AssociationsManager
- AssociationsHashMap
- ObjectAssociationMap
- ObjcAssociation
下面来看它们之间的关系:
AssociationsManager
AssociationsManager
是管理所有关联对象的中心管理器。它负责存储和检索关联对象的整体结构。它是一个单例,确保在整个运行时环境中只有一个实例。
- 职责:管理和维护所有对象的关联数据。
AssociationsHashMap
AssociationsHashMap
是一个哈希表,用于将对象映射到它们的关联数据。它在内部使用了一个哈希表,这个字段的 key 是被添加关联对象的对象的地址,value 是个ObjectAssociationMap
- 职责:高效地查找和存储对象与其关联数据之间的映射。
ObjectAssociationMap
ObjectAssociationMap
是一个结构体或类,用于存储单个对象的所有关联数据。key 是我们调用objc_setAssociatedObject
时传入的 key,value 是ObjectAssociation
- 职责:管理一个对象的所有关联键及其对应的关联值。
ObjcAssociation
ObjcAssociation
是具体的关联对象,包含实际的数据以及数据的存储策略,_policy是我们设置的修饰符(比如:OBJC_ASSOCIATION_ASSIGN),_value是我们传入的值value 等。
- 职责:存储实际的关联数据以及定义数据的内存管理语义。
工作流程
-
设置关联对象:
- 调用
objc_setAssociatedObject
。 AssociationsManager
查找或创建与目标对象相关的ObjectAssociationMap
。- 在
ObjectAssociationMap
中查找或创建对应的ObjcAssociation
。 - 将关联值和存储策略设置到
ObjcAssociation
中。
- 调用
-
获取关联对象:
- 调用
objc_getAssociatedObject
。 AssociationsManager
查找与目标对象相关的ObjectAssociationMap
。- 在
ObjectAssociationMap
中查找对应的ObjcAssociation
。 - 返回
ObjcAssociation
中存储的关联值。
- 调用
-
移除关联对象:
- 调用
objc_removeAssociatedObjects
或objc_setAssociatedObject
设置为nil
。 AssociationsManager
查找与目标对象相关的ObjectAssociationMap
。- 从
ObjectAssociationMap
中移除对应的ObjcAssociation
。 - 如果
ObjectAssociationMap
为空,可能会移除整个映射以释放资源。
- 调用
objc_AssociationPolicy
的值并没有weak,这点我们在使用的时候需要注意