【iOS】关联对象-在Category中添加属性

本文详细介绍了Objective-C中的Category特性,包括如何创建Category、Category的特点,如只能添加方法不能直接添加成员变量。还探讨了Category的实现原理,以及为何不能添加属性。此外,文章讲解了如何通过关联对象在Category中实现属性,并阐述了关联对象的工作原理和使用场景。最后,展示了Category在实际开发中的优势和应用场景。
摘要由CSDN通过智能技术生成

Category

  • Category是Objective-C 2.0之后添加的语言特性,分类、类别 其实都是指的Category。
  • Category的主要作用是为已经存在的类添加方法。也可以说是将庞大的类代码按逻辑划入几个分区。
  • Objective-C 中的 Category 就是对装饰模式的一种具体实现。它的主要作用是在不改变原有类的前提下,动态地给这个类添加一些方法。

Category的创建

按下面方式创建Category

请添加图片描述

File Type中选择Category,Class中选择已有类中的所需要模块化的类

请添加图片描述

如下图是我创建的三个Person类的分类

请添加图片描述

Category的特点

  • 分类是用于给原有类添加方法的,因为分类的结构体指针中,没有属性列表,只有方法列表。
  • 原则上讲它只能添加方法, 不能添加属性(成员变量),实际上可以通过其它方式添加属性
  • 分类中的可以写@property, 但不会生成setter / getter方法, 也不会生成实现以及私有的成员变量,会编译通过,但是引用变量会报错;
  • 如果分类中有和原有类同名的方法, 会优先调用分类中的方法, 就是说会忽略原有类的方法,同名方法调用的优先级为 分类 > 本类 > 父类
  • 同名分类方法生效取决于编译顺序: 如果多个分类中都有和原有类中同名的方法, 那么调用该方法的时候执行谁由编译器决定;编译器会执行最后一个参与编译的分类中的方法。

分类的实现原理

  • 将 Category 和它的主类(或元类)注册到哈希表中;
  • 如果主类(或元类)已实现,那么重建它的方法列表。

Category 中的实例方法和属性被整合到主类中,类方法则被整合到元类中;
对协议的处理比较特殊,Category 中的协议被同时整合到了主类和元类中。

最终都是通过调用 staticvoid remethodizeClass(Class cls) 函数来重新整理类的数据的。

Category不能添加成员变量

Category的底层结构

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;  // 属性列表
};
  1. 从结构体可以知道,有属性列表,所以分类可以声明属性,但是分类只会生成该属性对应的getset的声明,没有去实现该方法。
  2. 结构体没有成员变量列表,所以不能声明成员变量。
    方法可以运行时改变,结构体不能运行时改变,要想改变原结构体,增加一个属性,只能用关联变量的方式,而关联本质是在类的定义之外为类增加额外的存储空间,是一层映射关系

优点

  • 声明私有方法
  • 分解体积庞大的类文件
  • 把Framework私有方法公开
  • 模拟多继承(另外可以模拟多继承的还有protocol

在Category中添加属性

前面提到在Category中无法添加成员变量,但可以添加属性。如果按老方法直接添加属性会怎么样呢?如下

#import "Person.h"

NS_ASSUME_NONNULL_BEGIN

@interface Person (Per)

@property (nonatomic, strong) NSString* name;

@end

NS_ASSUME_NONNULL_END

如果主函数不涉及到对name属性的存取操作,编译器是能正常通过的,但如果涉及到了就会报错,如下:

请添加图片描述

从上面操作可以看出,

  • 添加的属性系统并没有自动生成成员变量,也没有实现setget方法,只是生成了setget方法的声明。这就是为什么在分类中扩展了属性,在外部并没有办法调用。
  • 在外部调用点语法设值和取值,本质其实就是调用属性的setget方法,现在系统并没有实现这两个方法,所以外部就没法调用分类中扩展的属性。

关联对象

associatedObject又称关联对象。顾名思义,就是把一个对象关联到另外一个对象身上。使两者能够产生联系。目前我能想到的关联对象的使用场景有如下几点:

  • 运行时给cagetory添加gettersetter。因为category中添加的property不会生成带下划线"_"的成员变量以及gettersetter的实现。所以可以通过关联对象实现gettersetter
  • 有时需要在对象中存储一些额外的信息,我们通常会从对象所属的类中继承一个子类。然后给这个子类添加额外的属性,改用这个子类。然而并非所有的情况都能这么做,有时候类的实例可能是由某种机制创建的,而开发者无法另这种机制创建出自己所写的子类实例。此时可以使用“关联对象”。
  • 有时只是给某个类添加一个额外的属性,完全没有必要继承出来一个子类。此时可以使用“关联对象”。
  • delegate回调的方法中使用关联对象。有时候在一些delegate回调的方法中需要处理一些回调任务。比如发起网络请求和在delegate回调的方法中做UI的更新。这样一来,发起网络请求和在回调中更新UI的代码被分散到了两个地方,不利于管理和阅读。此时可以使用“关联对象”。

关联对象相关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)

关联API对象参数说明:

  • 参数一:id object : 给哪个对象添加属性。
  • 参数二:const void *key : 关联对象中属性值存取过程中对应唯一标识, 根据key来设置和取值。
  • 参数三:id value : 关联的值,也就是set方法传入的值给属性去保存。
  • 参数四:objc AssociationPolicy 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; //指定相关的对象被复制, 原子性
};

关联对象原理

实现关联对象的核心对象有

  1. AssociationsManager
  2. AssociationsHashMap
  3. ObjectAssociationMap
  4. ObjcAssociation
  • 其中Map就相当于我们平时使用的字典,也是key-value 存取值。

通过一张图整理其中的关系:

请添加图片描述

  • 关联对象并不是存储在关联对象本身内存中
  • 关联对象存储在全局的一个AssociationsManager
  • 设置关联对象为nil,就相当于移除了关联对象

实例

下面举个例子,定义好一个Person类,然后在其分类Person+Per中添加属性

#import "Person.h"

NS_ASSUME_NONNULL_BEGIN

@interface Person (Per)

@property (nonatomic, strong) NSString* name;
@property (nonatomic, assign) int name;

@end

NS_ASSUME_NONNULL_END
#import "Person+Per.h"
#import <objc/runtime.h>

@implementation Person (Per)
	
	给key设值有三种比较好的的方法
	因为key值类型为const void*,任何类型的值,其实就是给个唯一的标识
	
	1.我们针对每个属性,定义-个全局的key名, 然后取其地址,这一定是唯一的加上static,只在文件内部有效
	static const void *NameKey = &NameKey;
	static const void *WeightKey = &WeightKey;
	
	2.针对每个属性,因为类中的属性名是唯一的,直接拿属性名作为key
	#define NameKey = @"name";
	#define WeightKey = @"weight";
	
	3.使用@selector作为key
	直接用属性名对应的get方法的selector,有提示不容易写错
	并且get方法隐藏参数cmd 可以直接用,看上去就会更加简洁
	以下实例代码就是用的第三种方式
	
	- (void)setName:(NSString *)name {
		//通过一个key给对象关联一个值
		objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
	}
	
	- (NSString *)name {
		//通过一个key,取出对象所关联的值
		//隐式参数 _cmd = @selector(name)
		return objc_getAssociatedObject(self, _cmd); 
	}
	
	- (void)setWeight:(int)weight {
		objc_setAssociatedObject(self, @selector(weight, @(weight), OBJC_ASSOCIATION_COPY_NONATOMIC);
	}

	- (int)weight {
		//通过一个key,取出对象所关联的值
		//隐式参数 _cmd = @selector(weight)
		return objc_getAssociatedObject(self, _cmd); 
	}


@end

设置好关联对象后就可以在外部正常调用添加的属性了

请添加图片描述

什么是_cmd?

在上面使用@selector作为key(即第三种方法)进行关联对象时,涉及到到了_cmd参数

  • _cmd 是隐藏的参数,表示当前方法的selector,他和self表示当前方法调用的对象实例。
  1. 获取当前被调用方法: NSStringFromSelector(_cmd)
  2. 在运行时时使用:在某个分类方法里为对象动态添加属性,由于_cmd是在编译时候(compile - time)就可以确定的值,因此可以直接使用
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值