【iOS】——分类拓展关联对象
分类
OC的动态特征允许使用类别为现有的类添加新方法并且不需要创建子类,不需要访问原有类的源代码。通过使用类别即可动态为现有的类添加新方法,而且可以将类定义模块化分布到多个相关文件。
- 分类是 Objective-C 中的一种语言特性,它允许你为现有类添加新的方法,而无需修改原始类的源代码。
- 分类就像一个“扩展”,你可以用它来添加新的功能,而不必创建子类。
- 分类不能添加新的实例变量,只能添加方法。
- 分类也可以把framework私有方法公开化,通过在分类中声明类的实现部分的私有方法即可通过分类来调用类中的某个私有方法
- 分类中添加的属性并没有自动生成成员变量,也没有实现set和get方法,只是生成了set和get方法的声明
示例代码如下:
@interface NSString (MyCategory)
- (NSString *)reverseString;
@end
@implementation NSString (MyCategory)
- (NSString *)reverseString {
NSMutableString *reversedString = [[NSMutableString alloc] init];
for (NSInteger i = self.length - 1; i >= 0; i--) {
[reversedString appendString:[self substringWithRange:NSMakeRange(i, 1)]];
}
return reversedString;
}
@end
分类应用场景:
- 扩展系统类功能: 例如,为
NSString
类添加reverseString
方法来反转字符串。 - 模块化代码: 将相关的方法分组到分类中,例如将所有与网络相关的操作方法放到一个名为
Networking
的分类中。 - 扩展第三方库功能: 为第三方库的类添加新方法,例如为
AFNetworking
添加一个方法来处理特定类型的 API 请求。 - 实现协议方法: 分类可以用来实现协议方法,而无需子类化。
- 委托模式: 分类可以用来实现委托方法,而无需子类化。
- 延迟加载: 分类可以用来实现延迟加载,例如将一些耗时的操作放到分类方法中,并在需要时才加载。
分类的定义如下:
struct category_t {
// 分类名称
const char *name;
// 分类所属的类
classref_t cls;
// 实例方法列表
struct method_list_t *instanceMethods;
// 类方法列表
struct method_list_t *classMethods;
// 协议列表
struct protocol_list_t *protocols;
// 实例属性列表
struct property_list_t *instanceProperties;
// 类属性列表(可能不存在)
struct property_list_t *_classProperties;
// 获取实例方法或类方法列表
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
// 获取实例属性或类属性列表
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
从结构体可以看出,分类能
- 给类添加实例方法 (instanceMethod)
- 给类添加类方法 (classMethod)
- 实现协议 (protocol)
- 添加属性 (instancePropertie)
但是不能添加实例变量,即无法自动生成实例变量的setter和getter方法
扩展
扩展与类别相似,有时候被称为匿名分类。但是两者实质上不是一个内容。
扩展是在编译阶段与该类同时编译的,是类的一部分。扩展中声明的方法只能在该类的@implementation中实现。所以这也就意味着我们无法对系统的类使用扩展。
同时与分类不同,扩展不但可以声明方法,还可以声明成员变量,这是分类所做不到的。
示例代码如下:
#import "Car1.h"
@interface Car1 ()
@property (nonatomic, copy) NSString *color;
- (void)drive:(NSString *)owner;
@end
#import "Car1.h"
#import "Car1+drive.h"
@implementation Car1
- (void)drive {
NSLog (@"%@汽车在路上跑", self);
}
- (void)drive:(NSString*)owner {
NSLog(@"%@正在驾驶%@汽车在路上跑", owner, self);
}
- (NSString*)description {
return [NSString stringWithFormat:@"<Car[_brand = %@, _model = %@, _color = %@]>",self.brand, self.model, self.color];
}
@end
两者区别
- 分类原则上只能增加方法,但是也可以通过关联属性增加属性
- 拓展可以增加方法和成员变量,都是私有的,实现部分在类中。
- 扩展只能在自身类中使用,而不是子类或者其他地方。
- 扩展是在编译阶段添加到类中,而分类是在运行时添加到类中
关联对象
关联对象允许你为一个对象添加额外的属性,即使这个对象本身没有定义这些属性。
因此,可以使用关联对象给分类添加属性
下面是关联对象的API
//添加关联对象
void objc_setAssociatedObject(id object, const void * key, id value, objc AssociationPolicy policy)
//获得关联对象
id objc_getAssociatedObject(id object, const void * key)
//移除所有的关联对象
void objic_removeAssociatedObjects(id object)
object
: 要添加属性的对象。key
: 用于标识属性的键,通常使用一个NSString
对象。value
: 要添加的属性值。policy
: 关联策略,用于指定属性的生命周期和访问权限。
关联策略如下:
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; //指定相关的对象被复制, 原子性
};
给key设置值一般来说有三种方法
- 针对每个属性,定义-个全局的key名, 然后取其地址,这一定是唯一的加上static,只在文件内部有效
static const void *NameKey = &NameKey;
static const void *WeightKey = &WeightKey;
- 针对每个属性,因为类中的属性名是唯一的,直接拿属性名作为key
#define NameKey = @"name";
#define WeightKey = @"weight";
- 使用@selector作为key
@selector(name)//直接用属性名对应的get方法的selector,有提示不容易写错。并且get方法隐藏参数cmd 可以直接用,看上去就会更加简洁
下面是示例代码:
#import <UIKit/UIKit.h>
#import "objc/runtime.h"
NS_ASSUME_NONNULL_BEGIN
@interface UIView (defaultColor)
@property (nonatomic, strong)UIColor* defaultColor;
@end
NS_ASSUME_NONNULL_END
#import "UIView+defaultColor.h"
@implementation UIView (defaultColor)
@dynamic defaultColor;
static char kDefaultColorKey;
- (void)setDefaultColor:(UIColor *)defaultColor {
//objc_setAssociatedObject(self, &kDefaultColorKey, defaultColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_setAssociatedObject(self, @selector(defaultColor), defaultColor, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)defaultColor {
//return objc_getAssociatedObject(self, &kDefaultColorKey);
return objc_getAssociatedObject(self, _cmd);
}
@end
通过上面的关联对象我们就给系统提供的UIView设置了默认颜色的属性
_cmd
是一个特殊的参数,它代表当前正在执行的方法的选择器(selector)。选择器是一个字符串,它标识了方法的名称。
_cmd
可以让方法在执行过程中获取自身的信息,例如方法名称
关联对象底层探索
实现关联对象技术的核心对象有
- AssociationsManager
- AssociationsHashMap
- ObjectAssociationMap
- ObjcAssociation
objc_setAssociatedObject
首先来看创建关联对象的函数,源码如下:
void
_object_set_associative_reference(id object, const void *key, id value, uintptr_t policy)
{
//isa有一位信息为禁止关联对象,如果设置了,直接报错
if (!object && !value) return;
// 判断runtime版本是否支持关联对象
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));
// 将 object 封装成 DisguisedPtr 目的是方便底层统一处理
DisguisedPtr<objc_object> disguised{(objc_object *)object};
// 将 policy和value 封装成ObjcAssociation,目的是方便底层统一处理
ObjcAssociation association{policy, value};
// (如果有新值)保留锁外的新值。
// retain the new value (if any) outside the lock.
// 根据传入的缓存策略,创建一个新的value对象
association.acquireValue();
bool isFirstAssociation = false;
{
//调用构造函数,构造函数内加锁操作
AssociationsManager manager;
// 创建一个管理对象管理单例,类AssociationsManager管理一个锁/哈希表单例对。分配一个实例将获得锁
// 并不是全场唯一,构造函数中加锁只是为了避免重复创建,在这里是可以初始化多个AssociationsManager变量的
//获取全局的HasMap
// 全场唯一
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 */
// 这是我们建立的第一个关联
//说明是第一次设置关联对象,把是否关联对象设置为YES
isFirstAssociation = true;
}
// 建立或替换关联
/* establish or replace the association */
// 获取ObjectAssociationMap中存储值的地址
auto &refs = refs_result.first->second;
// 移除之前的关联,根据key
// 将需要存储的值存放在关联表中存储值的地址中
// 同时会根据key去查找,如果查找到`result.second` = false ,如果找不到就创建`result.second` = true
// 创建association时,当(association的个数+1)超过3/4,就会进行两倍扩容
auto result = refs.try_emplace(key, std::move(association));
if (!result.second) {
// 交换association和查询到的`association`
// 其实可以理解为更新查询到的`association`数据,新值替换旧值
association.swap(result.first->second);
}
} else {
// 这里相当于传入的nil,移除之前的关联
// 到AssociationsHashMap找到ObjectAssociationMap,将传入key对应的值变为空。
// 查找disguised 对应的ObjectAssociationMap
auto refs_it = associations.find(disguised);
// 如果找到对应的 ObjectAssociationMap 对象关联表
if (refs_it != associations.end()) {
// 获取 refs_it->second 里面存放了association类型数据
auto &refs = refs_it->second;
// 根据key查询对应的association
auto it = refs.find(key);
if (it != refs.end()) {
// 如果找到,更新旧的association里面的值
association.swap(it->second);
refs.erase(it);
if (refs.size() == 0) {
// 如果该对象关联表中所有的关联属性数据被清空,那么该对象关联表会被释放
associations.erase(refs_it);
}
}
}
}
}
// 在锁外面调用setHasAssociatedObjects,因为如果对象有一个,这个//将调用对象的noteAssociatedObjects方法,这可能会触发initialize,这可能会做任意的事情,包括设置更多的关联对象。
if (isFirstAssociation)
object->setHasAssociatedObjects();
// release the old value (outside of the lock).
// 释放旧的值(在锁外部)
association.releaseHeldValue();
}
- 首先就是进行参数检查和进行安全处理(检查
object
和value
是否都为nil
,检查object
的类是否允许关联对象) - 接着封装数据类型,便于底层处理(将
object
封装成DisguisedPtr<objc_object>
类型,将policy
和value
封装成ObjcAssociation
类型) - 接着使用
association.acquireValue()
保留新值,确保新值不会被释放 - 然后创建一个
AssociationsManager
对象,并通过它来获取全局的AssociationsHashMap
对象 - 在
AssociationsHashMap
中查找object
对应的关联表ObjectAssociationMap
- 如果没有找到
ObjectAssociationMap
,则创建一个新的ObjectAssociationMap
,并将其插入到AssociationsHashMap
中。 - 如果找到
ObjectAssociationMap
,则在ObjectAssociationMap
接着查找key
对应的关联信息 - 如果找到,则使用新值替换旧值。
- 如果没有找到,则创建一个新的关联信息,并将新值存储在
ObjectAssociationMap
中。 - 如果
value
为nil
,则表示要移除关联。系统会查找ObjectAssociationMap
中key
对应的关联信息,并将其移除。
objc_getAssociatedObject
获取关联对象的源码如下:
id
_object_get_associative_reference(id object, const void *key)
{
ObjcAssociation association{};//创建空的关联对象
{
AssociationsManager manager;//创建一个AssociationsManager管理类
AssociationsHashMap &associations(manager.get());//获取全局唯一的静态哈希map
AssociationsHashMap::iterator i = associations.find((objc_object *)object);//找到迭代器,即获取buckets
if (i != associations.end()) {//如果这个迭代查询器不是最后一个 获取
ObjectAssociationMap &refs = i->second; //找到ObjectAssociationMap的迭代查询器获取一个经过属性修饰符修饰的value
ObjectAssociationMap::iterator j = refs.find(key);//根据key查找ObjectAssociationMap,即获取bucket
if (j != refs.end()) {
association = j->second;//获取ObjcAssociation
association.retainReturnedValue();
}
}
}
return association.autoreleaseReturnedValue();//返回value
}
- 首先创建AssociationsManager对象,接着通过它来获取全局的
AssociationsHashMap
- 在
AssociationsHashMap
中查找object
对应的ObjectAssociationMap
- 如果找到
ObjectAssociationMap
,则在ObjectAssociationMap
中接着查找key对应的关联信息并赋值给value - 最后返回value
objc_removeAssociatedObjects
移除关联对象的源码如下:
// 与设置/获取关联引用不同,此函数对性能敏感,因为原始isa对象(如OS对象)不能跟踪它们是否有关联对象。
void
_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.
//如果我们没有回收,那么SYSTEM_OBJECT关联会被保留。
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.
//如果我们不是在释放,那么RELEASE_LATER关联不会被释放
if (deallocating)
laterRefs.append(&i.second);
} else {
i.second.releaseHeldValue();
}
}
for (auto *later: laterRefs) {
later->releaseHeldValue();
}
}
- 首先使用
AssociationsManager
获取全局的AssociationsHashMap
,并查找object
对应的关联表ObjectAssociationMap
。 - 如果找到关联表,则将其复制到
refs
变量中,以便在锁外进行操作。 - 如果对象正在被释放,则所有关联对象都会被移除,但如果对象只是被修改,则系统关联对象会被保留
- 如果没有保留任何关联对象,则从
AssociationsHashMap
中移除object
对应的关联表 - 遍历
refs
中的所有关联对象,并根据关联对象的策略进行释放。 - 如果关联对象的策略包含
OBJC_ASSOCIATION_SYSTEM_OBJECT
,则将其添加到laterRefs
列表中,以便在所有其他关联对象释放后进行释放。 - 如果关联对象的策略不包含
OBJC_ASSOCIATION_SYSTEM_OBJECT
,则立即释放关联对象。 - 最后遍历
laterRefs
中的所有关联对象,并释放它们
总结
- 分类可以用来为类动态的添加方法,通过关联对象还能动态添加属性
- 分类默认只能声明属性不会生成成员变量和对应的get和set方法
- 分类也用于模块化设计
- 分类是在运行期生成,扩展是类的一部分在编译期生成
- 关联对象的API的实现都是通过操作AssociationsManager、AssociationsHashMap、ObjectAssociationMap、ObjcAssociation来实现
Category的方法会“覆盖”掉原来类的同名方法?
-
Category的方法没有“完全替换掉”原来类已经有的方法,也就是说如果Category和原来类都有methodA,那么Category附加完成之后,类的方法列表里会有两个methodA
-
Category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的Category的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就返回了,不会在理会后面的同名方法。
关联对象被存储在什么地方,是不是存放在被关联对象本身的内存中?
关联对象存放在名为ObjectAssociationMap的哈希表中,存放关联对象的哈希表又被存放在名为AssociationsHashMap的哈希表中,通过AssociationsManager来管理。也就是说所有对象的关联对象都存在一个全局map里面。而map的的key是这个对象的指针地址,而这个map的value又是另外一个全局map,里面保存了关联对象的key。
关联对象的生命周期是怎样的,什么时候被释放,什么时候被移除?
关联对象的释放时机与移除时机并不总是一致。关联对象的生命周期取决于关联策略和目标对象的生存期。弱引用策略的关联对象会随着目标对象的释放而被释放,而强引用策略和复制引用策略的关联对象会继续存在,直到它们的引用计数降为 0。