对象关联

runtime对象关联

1.对象关联

使用关联,我们可以在不修改类定义的前提下,为其对象增加存储空间。

适合的场景:

  1. 把某对象与特定对象相关联;
  2. 解决分类中属性不会自动生成getter、setter函数,调用时会闪退的问题;

1.1.创建关联

1
OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);

参数:

  • object:源对象;
  • *key:键;
  • value:将关联到源对象中的对象;
  • policy:关联策略;

关联策略字段与声明属性时的关键字类似:

1
2
3
4
5
6
7
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.2.获取关联

1
OBJC_EXPORT id objc_getAssociatedObject(id object, const void *key);

1.3.断开关联

断开关联可使用objc_setAssociatedObject函数,第三个参数传入nil值即可,此时关联策略也就无所谓了。

objc_removeAssociatedObjects也可以断开关联,但不建议使用这个函数,它会断开所有的关联。只有在需要把对象恢复到原始状态时才推荐使用这个函数。

1
OBJC_EXPORT void objc_removeAssociatedObjects(id object)

1.4.示例

#示例1:

1
2
3
4
5
6
7
8
9
10
11
#import <Foundation/Foundation.h>

@interface NSObject (Associate)

//属性 分类中允许声明属性,但不会自动生成getter、setter函数;
@property (nonatomic, strong) id mAssociateObj;

//移除关联
- (void)removeAssociate;

@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#import "NSObject+Associate.h"
#import <objc/runtime.h>

static void *mAssociateObjKey = &mAssociateObjKey;

@implementation NSObject (Associate)


- (void)setMAssociateObj:(id)obj
{
//创建关联
objc_setAssociatedObject(self, mAssociateObjKey, obj, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (id)mAssociateObj
{
//获取关联的对象
return objc_getAssociatedObject(self, mAssociateObjKey);
}

- (void)removeAssociate
{
objc_setAssociatedObject(self, mAssociateObjKey, nil, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end

调用:

1
2
3
4
5
6
NSObject *obj = [NSObject new];
obj.mAssociateObj = @"THIS IS A STRING";
NSLog(@"++++关联对象:%@",obj.mAssociateObj);

[obj removeAssociate];
NSLog(@"++++移除后的关联对象:%@",obj.mAssociateObj);

日志:

1
2
++++关联对象:THIS IS A STRING
++++移除后的关联对象:(null)

#示例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];

UIAlertController *alertControler = [UIAlertController
alertControllerWithTitle:@"Title"
message:@"This is Mess"
preferredStyle:UIAlertControllerStyleAlert];

UIAlertAction *action = [UIAlertAction actionWithTitle:@"OK"
style:UIAlertActionStyleCancel
handler:^(UIAlertAction * _Nonnull action) {

NSString *associateObj = objc_getAssociatedObject(alertControler, mAssociateObjKey);
NSLog(@"++++关联对象:%@",associateObj);
objc_setAssociatedObject(alertControler, mAssociateObjKey, nil, OBJC_ASSOCIATION_COPY_NONATOMIC);

NSString *associateObj2 = objc_getAssociatedObject(alertControler, mAssociateObjKey);
NSLog(@"++++关联对象:%@",associateObj2);
}];
[alertControler addAction:action];

objc_setAssociatedObject(alertControler, mAssociateObjKey, @"This is a string obj", OBJC_ASSOCIATION_COPY_NONATOMIC);

[self presentViewController:alertControler animated:YES completion:^{
}];
}

点击弹窗之后,输出日志:

1
2
++++关联对象:This is a string obj
++++移除关联对象后:(null)

需要说明的是:

  1. 通过关联增加属性,只是把属性对象与类对象的实例相关联,并未真正增加类的成员变量;
  2. 关联的对象与被关联的对象,两者的内存空间是分开的;

1.5.原理

关联可以将对象与某个实例绑定到一起,或者在分类中实现属性值的存取。但是在运行时,被关联的属性并没有添加到 category_t 结构体中,也不会合并到原类对象里,而是存储在一个全局的管理器 AssociationsManager 里。

所有的关联属性、获取关联属性、移除关联属性都是通过一个AssociationsManager来操作,类似于 OC 中 NSFileManager 的角色,通过传递进来的对象作为地址取出这个对象所对应的关联列表,然后通过key取出这个关联列表的关联属性 ObjcAssociation。ObjcAssociation 包含了 关联策略关联值.

2.分类特殊性

1
2
3
4
5
6
7
8
9
10
#import <Foundation/Foundation.h>

@interface NSObject (Associate)
{
int aInt;//此处编译时会报错:“instance variables may not be placed in categories”
}
//属性 分类中允许声明属性,但不会自动生成getter、setter函数;
@property (nonatomic, strong) id mAssociateObj;

@end
  • 分类中不能声明成员变量;
  • 分类中允许声明属性,但不会生成属性对应的成员变量及其 getter\setter;
  • 分类中可以增加新的方法,或者覆盖原类的方法;

2.1.✘成员变量

结论:不能通过分类向原类中增加新的成员变量。

原因分析:OC中的类是一个指向 objc_class 的结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct objc_class {
Class isa

#if !__OBJC2__
Class super_class
const char *name
long version
long info
long instance_size
struct objc_ivar_list *ivars //成员变量列表
struct objc_method_list **methodLists
struct objc_cache *cache
struct objc_protocol_list *protocols
#endif
  • 实例的内存布局在编译期已确定

增加新成员变量,就需要额外的内存来存储新的值。而类结构在编译时就已经确定了,即类实例的内存空间大小"instance_size"和成员变量列表"ivars"的内容在编译阶段就已经确定。

  • 分类的加载晚于原类

分类中的属性和方法是在应用启动的过程中,原类注册完成之后才注册到原类中的,即分类加载时原类的结构早已固定。

基于以上两点,分类不能向一个已经完成编译和加载的原类中再增加成员变量~

有人说可以使用class_addIvar()增加成员变量,需要指出的是,这是个运行时函数~且苹果文档中已经明确说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/** 
* Adds a new instance variable to a class.
* @note This function may only be called after objc_allocateClassPair
* and before objc_registerClassPair.
* Adding an instance variable to an existing class is not supported.
* @note The class must not be a metaclass.
* Adding an instance variable to a metaclass is not supported.
* @note The instance variable's minimum alignment in bytes is 1<<align.
* The minimum alignment of an instance
* variable depends on the ivar's type and the machine architecture.
* For variables of any pointer type, pass log2(sizeof(pointer_type)).
*/
OBJC_EXPORT BOOL class_addIvar(Class cls, const char *name, size_t size,
uint8_t alignment, const char *types)

这个函数只能在objc_allocateClassPairobjc_registerClassPair之间使用。就是说一旦类的定义完成之后,就不能再增加成员变量。我们用到的类,在编译时其结构就已经固定了。即使在运行时,也不能在objc_registerClassPair()之后再增加成员变量!

#示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#import <objc/runtime.h>
#import <objc/message.h>

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
//创建新的类
Class Person = objc_allocateClassPair([NSObject class], "Person", 0);

//增加成员变量
BOOL status = class_addIvar(Person, "nick",
sizeof(NSString*), log2(sizeof(NSString*)), @encode(NSString*));

if (status)
NSLog(@"+++++变量nick添加成功"); // 成功
else
NSLog(@"+++++变量nick添加失败");

//注册类
objc_registerClassPair(Person);

status = class_addIvar(Person, "city",
sizeof(NSString*), log2(sizeof(NSString*)), @encode(NSString*));

if (status)
NSLog(@"+++++变量city添加成功");
else
NSLog(@"+++++变量city添加失败"); // 失败
return YES;
}

输出日志:

1
2
+++++变量nick添加成功
+++++变量city添加失败

示例中在objc_registerClassPair()之后添加成员变量city失败~

2.2.✔️属性

结论:分类中可以增加属性,但不会自动生成对应的成员变量和getter/setter。

1、分类为什么可以声明属性呢?

这是因为分类的结构体中本身就定义了proper_list_t *properties字段,参考这里

2、为什么不能自动生成属性对应的成员变量和存取函数呢?

原类中属性对应的成员变量和存取函数都是由编译器在编译阶段生成的,而分类的则是在应用启动阶段原类加载完成之后才注册到原类中的,所以没法实现属性的@synthesize功能。如果想实现这些功能,我们可以使用上面提到的关联技术。

#示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#import <objc/runtime.h>
#import <objc/message.h>

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
//创建新的类
Class Person = objc_allocateClassPair([NSObject class], "Person", 0);

//增加成员变量
BOOL status = class_addIvar(Person, "nick",
sizeof(NSString*), log2(sizeof(NSString*)), @encode(NSString*));

if (status)
NSLog(@"+++++变量nick添加成功"); // 成功
else
NSLog(@"+++++变量nick添加失败");

//注册类
objc_registerClassPair(Person);

//为类增加属性
objc_property_attribute_t type = {"T","@\"NSString\""};
objc_property_attribute_t ownership1 = {"C",""}; // C = copy
objc_property_attribute_t ownership2 = { "N", "" }; // N = nonatomic
objc_property_attribute_t backIvars = {"V","_name"}; //属性名
objc_property_attribute_t atts[] = {type,ownership1,ownership2,backIvars};

status = class_addProperty(Person, "name", atts, 4);
if (status)
NSLog(@"+++++属性name添加成功"); // 成功
else
NSLog(@"+++++属性name添加成功");
return YES;
}

输出日志:

1
2
+++++变量nick添加成功
+++++属性name添加成功

日志显示,objc_registerClassPair之后是可以动态添加属性的,但是如果你打印此时类的成员变量列表你会发现,并未创建属性对应的以下划线_开头的成员变量。这是因为我们平时所见到的"_属性名"成员变量是由编译器自动创建的,这里的代码是在运行时,并未经过编译器~

2.3.✔️方法

结论:分类中可以增加新的方法,也可以覆盖原类的方法。

类的结构体中,**methodLists是一个指向objc_method_list指针的指针,也就是说**methodLists只是把装方法们的容器的地址保存在类的结构体里。类编译完成后,这个容器的地址不可变了,但是我们可以往这个容器里继续添加元素。因为增加方法不会影响类的内存空间。

分类中定义的方法也会被加入到原类**methodLists容器中,且分类的方法会在原类的方法之前。也正是因为这样,我们在分类中重写原类的方法时,原类的方法会失效,因为在查找方法的过程中分类的方法靠前而最先被找到,直接终止寻找过程并调用分类中的实现去了。

#示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 原类头文件
@interface Person : NSObject <NSCoding>

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;

- (void)callMe;

@end

// 原类m文件
@implementation Person

- (void)callMe{
NSLog(@"+++My name is:%@",self.name);
}
@end


// 分类Person+C.m
@implementation Person (C)

// 重写原类的方法并提供新的实现
- (void)callMe{
NSLog(@"+++My name is not:%@",self.name);
}
@end

对象关联
https://davidlii.cn/2017/08/30/runtime-associate.html
作者
Davidli
发布于
2017年8月30日
许可协议