关联对象介绍

本文详细解释了在Objective-C中如何使用关联对象为分类添加属性,以及如何在分类中实现弱引用属性的不同方法,包括通过中间对象、NSProxy、Block和NSMapTable。同时讨论了关联对象与weak引用的区别和使用限制。
摘要由CSDN通过智能技术生成

关联对象的作用

分类里面,不可以直接为分类添加属性
代理中,不可以直接为代理添加属性

在普通类中,@property (assign, nonatomic) int age;
会做三件事:

  1. 生成age的成员变量
  2. 生成age的get、set方法的声明
  3. 生成age的get、set方法的实现

而在分类中,@property (assign, nonatomic) int age;可以写,但是它的作用只有一个:
生成age的get、set方法的声明

如果想给分类添加属性,则可以使用关联对象
即实现效果:

  1. 手动实现set\get方法
  2. 在set\get方法中,可以存储、取出属性值
    间接实现为分类添加属性的效果

关联对象的使用:

@interface YZPerson : NSObject
@property (assign, nonatomic) int age;
@end

@interface YZPerson (Eat)
@property (copy, nonatomic) NSString *name;
@end

#import <objc/runtime.h>
@implementation YZPerson (Eat)
- (void)setName:(NSString *)name
{
	//第一个参数,给谁添加管理对象(self)
	//第二个参数,是关联对象的key,就是通过key找value
	//第三个参数,关联的值,很明显是name
	//第四个参数,关联策略(相当于assign\strong这种作用)
    objc_setAssociatedObject(self, @selector(setName:), name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)name
{
	//key需要保持一致
    return objc_getAssociatedObject(self, @selector(setName:));
}
@end

YZPerson *person1 = [[YZPerson alloc] init];
person1.age = 10;
person1.name = @"zhangSan";
        
YZPerson *person2 = [[YZPerson alloc] init];
person2.age = 20;
person2.name = @"liSi";
        
NSLog(@"person1.age = %d, person2.age = %d", person1.age, person2.age);
NSLog(@"person1.name = %@, person2.name = %@", person1.name, person2.name);

结果:
2020-02-27 16:26:56.015710+0800 Category[6423:189583] person1.age = 10, person2.age = 20
2020-02-27 16:26:56.015980+0800 Category[6423:189583] person1.name = zhangSan, person2.name = liSi

关联对象值的作用:

关联对象赋值的时候,第二个参数,key的赋值:
第二个参数的值类型:const void * _Nonnull key
在这里插入图片描述

关联对象赋值时,第四个参数objc_AssociationPolicy的取值:
在这里插入图片描述

关联对象的存储:

在这里插入图片描述

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

关联对象被一个全局的AssociationsManager管理
AssociationsManager里面有多个map
map的key就是关联对象的(第一个参数)对象的内存地址
map的value又是一个map

第二个map里面
key是第二个参数:key
value是 第三个参数和第四个参数

这就可以解读:
每一个分类对象YZPerson+eat,都可以作为map的key
然后在每一个分类里面,又有多个属性name\age,每一个属性,都是一个map的key

移除关联对象

一种是单个移除,只需赋值是传nil即可,也就是第三个参数接收的是nil,然后会进行改属性的擦除操作
objc_setAssociatedObject(self, @selector(setName:), nil, OBJC_ASSOCIATION_COPY_NONATOMIC);

另外一个是移除该对象(YZPerson+eat)中所有的关联对象
void objc_removeAssociatedObjects(id _Nonnull object)


问:关联对象里面怎么没有weak?

本质上是因为关联对象在保存value时,没有将value加入到对象的弱引用表中,所以对象销毁清空弱引用表时,关联对象存的这个指针不在其中,所以不会随着对象销毁而被置为nil。

主要原因在于关联对象的实现机制和weak引用的内存管理策略之间的复杂性。weak属性的引用是自动置为nil的,当所指向的对象被释放时,任何weak引用都会自动清零。这种行为要求运行时维护一个所谓的“弱引用表”来跟踪和更新所有weak引用。如果关联对象直接支持weak关联,那么每次对象释放时,运行时都需要遍历与之相关联的所有对象,更新或清除这些weak关联,这将增加运行时的复杂度和性能负担。

OC 底层探索 - Association 关联对象评论区


问:如果想实现一个weak属性,怎么做?

在iOS中,由于分类(Category)本身不支持直接添加属性的存储空间,要在分类中添加一个弱引用属性通常需要结合使用关联对象(Associated Object)和一些额外的技巧来实现。下面是一个实现弱引用属性的步骤:

方法一:定义一个中间对象

在这里插入图片描述

  1. 定义一个中间对象

由于objc_setAssociatedObject不直接支持weak关联,你可以通过创建一个中间对象来持有实际的弱引用。这个中间对象将有一个weak属性,用于指向你想要弱引用的对象。

@interface WeakReferenceObject : NSObject
//此处需要用weak,就是我们的目标值属性
@property (nonatomic, weak) id target;
@end

@implementation WeakReferenceObject
@end
  1. 在分类中使用关联对象

在分类中,使用objc_setAssociatedObjectobjc_getAssociatedObject来分别设置和获取这个中间对象,从而间接实现了一个弱引用的属性。

#import <objc/runtime.h>
@interface NSObject (MyCategory)
//中间对象,此处用weak或strong都可以,因为不会生成对应的属性,属性修饰是看具体的关联策略
@property (nonatomic, weak) id myWeakProperty;
@end

@implementation NSObject (MyCategory)

- (void)setMyWeakProperty:(id)myWeakProperty {
	//获取中间对象
    WeakReferenceObject *weakReference = [[WeakReferenceObject alloc] init];
    //中间对象的target,引用weakCar
    //target为弱引用
    weakReference.target = myWeakProperty;
    //OBJC_ASSOCIATION_RETAIN_NONATOMIC强引用
    objc_setAssociatedObject(self, @selector(myWeakProperty), weakReference, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (id)myWeakProperty {
	//获取中间对象
    WeakReferenceObject *weakReference = objc_getAssociatedObject(self, @selector(myWeakProperty));
    //返回的是中间对象的target,即weakCar
    return weakReference.target;
}

@end

注意事项:

使用这种方法时,需要注意内存管理和潜在的循环引用问题。由于是通过关联对象机制实现的,还是要确保关联策略正确选择,以避免内存泄露。在这个例子中,我们使用的是OBJC_ASSOCIATION_RETAIN_NONATOMIC,因为我们希望保持对中间WeakReferenceObject对象的强引用,而WeakReferenceObject则对目标对象保持弱引用。

通过这种方式,虽然稍显复杂,但可以在分类中成功添加一个表现为弱引用的属性。

相当于,关联对象正常strong引用一个中间对象,然后中间对象引用一个weak属性
关联对象对strong的中间对象可以正常拥有或释放
而中间对象对weak属性,也可以正常拥有或释放

方法二:使用NSProxy做中间对象

我们需要创建一个NSProxy的子类作为中间对象,把弱引用存储在这个中间对象内。
创建一个WeakProxy

#import "objc/runtime.h"

@interface WeakProxy : NSProxy
@property (nonatomic, weak, readonly) id target;
- (instancetype)initWithTarget:(id)target;
@end

@implementation WeakProxy
- (instancetype)initWithTarget:(id)target {
    _target = target;
    return self;
}

//方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [self.target methodSignatureForSelector:sel];
}

//把target封装到invocation去
- (void)forwardInvocation:(NSInvocation *)invocation {
    if (self.target) {
        [invocation invokeWithTarget:self.target];
    }
}
@end

有了WeakProxy后,就可以在分类中借助它来添加弱指针属性了。比如我们有一个NSObject的分类,想要为它添加一个弱引用的delegate属性:

#import "YZPerson+eat.h"//分类
#import "objc/runtime.h"
#import "WeakProxy.h"

- (YZCar *)weakCar
{
    //获取中间对象
    WeakProxy *proxy = objc_getAssociatedObject(self, @selector(setWeakCar:));
    //返回的是中间对象的target,即weakCar
    return proxy.target;
}

- (void)setWeakCar:(YZCar *)weakCar
{
	//创建中间对象Proxy,Proxy引用弱属性weakCar
    WeakProxy *proxy = [[WeakProxy alloc] initWithTarget:weakCar];
    //将proxy做关联对象处理
    objc_setAssociatedObject(self, @selector(setWeakCar:), proxy, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

方法三:使用__weak关键字的Block封装

还可以通过一个封装的Block来持有弱引用,这个Block捕获外部变量的弱引用,在需要时返回这个弱引用:

#import <objc/runtime.h>

@interface YZPerson (eat)
//弱引用属性
@property (nonatomic, weak) YZCar *weakCar;
//自己定义的block
@property (nonatomic, copy) YZCar *(^myWeakCarBlock)(void);
@end

@implementation YZPerson (eat)
- (void)setWeakCar:(YZCar *)weakCar
{
    __weak YZCar *weakProperty = weakCar;
    //block赋值,返回的是弱指针指向的weakProperty
    YZCar *(^myWeakCarBlock)(void) = ^{ return weakProperty; };
    //关联对象保存block
    objc_setAssociatedObject(self, @selector(myWeakCarBlock), myWeakCarBlock, OBJC_ASSOCIATION_COPY);
}

- (YZCar *)weakCar
{
    //获取关联对象的block
    YZCar *(^myWeakCarBlock)(void) = objc_getAssociatedObject(self, @selector(myWeakCarBlock));
    //block调用,就是返回值,weakCar
    return myWeakCarBlock ? myWeakCarBlock() : nil;
}
@end

这种方法利用了Block的捕获特性来维护一个弱引

在这里插入图片描述

方法四:使用NSMapTable

这种方法跟关联对象就没啥关系了

NSMapTable是一个灵活的集合类,可以配置键和值的内存管理策略,包括弱引用。可以利用NSMapTable的弱引用特性来实现类似弱引用的属性:

#import <objc/runtime.h>

@interface NSObject (MyCategory)
@property (nonatomic, weak) id myWeakProperty;
@end

@implementation NSObject (MyCategory)

static NSMapTable *weakProperties;

+ (void)load {
    weakProperties = [NSMapTable weakToWeakObjectsMapTable];
}

- (void)setMyWeakProperty:(id)myWeakProperty {
    @synchronized (self) {
        [weakProperties setObject:myWeakProperty forKey:self];
    }
}

- (id)myWeakProperty {
    @synchronized (self) {
        return [weakProperties objectForKey:self];
    }
}

@end

在这个方法中,NSMapTable 用于存储所有对象的弱引用,实现了类似于弱引用属性的效果。

直接使用关联对象的OBJC_ASSOCIATION_ASSIGN修饰可以吗?

不可以
会在代码结束后,再次访问该分类弱引用属性,导致崩溃

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.p1 = [[YZPerson alloc] init];
    self.p1.height = 20;
    
    YZCar *strongCar = [[YZCar alloc] init];
    strongCar.speed = 30;
    strongCar.color = @"white";
    self.p1.weakCar = strongCar;
    
    NSLog(@"1---%f, %@, %f, %@", self.p1.height, self.p1.weakCar, self.p1.weakCar.speed, self.p1.weakCar.color);
    //1---20.000000, <YZCar: 0x600003a42160>, 30.000000, white
}

然后在其他地方,再次打印:

NSLog(@"2---%f, %@, %f, %@", self.p1.height, self.p1.weakCar, self.p1.weakCar.speed, self.p1.weakCar.color);
直接crash
讨论
有没有可能,在弱引用属性weakCar释放的时候,对weakCar手动进行nil操作?

首先,讨论weakCar什么时候被释放?
一种是person对象销毁,即本来是self.person.weakCar,现在self.person=nil,则weakCar也需要等于nil

一种是person对象不销毁,而weakCar由于没有强制针引用,从而导致weakCar的对象应该被释放

{
	@autoreleasepool{
		YZCar *strongCar = [[YZCar alloc] init];
		self.person.weakCar = strongCar;
	}
	NSLog(@"%@", self.person.weakCar);
}

此时,self.person还存在,但是strongCar已经被释放,从而导致weakCar还指向着对应的内存地址,而没有做nil操作

目前想到的办法是:
在Person和Car的dealloc方法里面,做一个通知,通知person+eat方法,进行weakCar释放操作
Person和Car的dealloc方法里面:

- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] postNotificationName:@"deallocCar" object:nil];
    NSLog(@"YZPerson-dealloc");
}

YZPerson+eat方法里面,接受通知,执行:

- (void)deallocCar
{
    if(self.weakCar){
        objc_setAssociatedObject(self, @selector(setWeakCar:), nil, OBJC_ASSOCIATION_ASSIGN);
    }
}

但,有问题的是:这个通知,将所有的wearCar都释放了
如果有两个weakCar,一个需要释放,一个不需要释放,会导致两个都被释放
虽然不会崩溃,但是有其他问题,会导致代码不正确,因此,不可以


分类代码:

在分类YZPerson+eat.h文件下:

#import "YZPerson.h"
#import "YZCar.h"

@interface YZPerson (eat)
@property (nonatomic, assign) CGFloat height;
@property (nonatomic, weak) YZCar *weakCar;
@end

分类YZPerson+eat.m文件下:

#import "YZPerson+eat.h"
#import "objc/runtime.h"

@implementation YZPerson (eat)


- (void)setHeight:(CGFloat)height
{
    objc_setAssociatedObject(self, @selector(setHeight:), @(height), OBJC_ASSOCIATION_ASSIGN);
}

- (CGFloat)height
{
    return [objc_getAssociatedObject(self, @selector(setHeight:)) floatValue];
}

- (void)setWeakCar:(YZCar *)weakCar
{
    objc_setAssociatedObject(self, @selector(setWeakCar:), weakCar, OBJC_ASSOCIATION_ASSIGN);
}

- (YZCar *)weakCar
{
    return objc_getAssociatedObject(self, @selector(setWeakCar:));
}

@end
  • 10
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值