关联对象的作用
在分类里面,不可以直接为分类添加属性
在代理中,不可以直接为代理添加属性
在普通类中,@property (assign, nonatomic) int age;
会做三件事:
- 生成age的成员变量
- 生成age的get、set方法的声明
- 生成age的get、set方法的实现
而在分类中,@property (assign, nonatomic) int age;
可以写,但是它的作用只有一个:
生成age的get、set方法的声明
如果想给分类添加属性,则可以使用关联对象
即实现效果:
- 手动实现set\get方法
- 在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:));
}
@endYZPerson *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
关联,这将增加运行时的复杂度和性能负担。
尽管没有直接的weak
关联选项,但开发者可以通过一些技巧间接实现类似weak
引用的效果。例如,可以使用OBJC_ASSOCIATION_ASSIGN
作为关联策略来模拟弱引用,但需要确保在引用的对象被释放时手动清除这种关联,以避免悬挂指针的风险。此外,还可以通过包装一个weak
属性的对象作为关联对象,这样就能在对象被释放时自动清零,模拟weak
引用的行为。
总之,虽然关联对象没有直接提供weak
引用的选项,但这是出于管理复杂性和性能考虑的结果。需要类似weak
功能时,可以通过其他方式间接实现。
OC 底层探索 - Association 关联对象评论区
问:如果想实现一个weak属性,怎么做?
在iOS中,由于分类(Category)本身不支持直接添加属性的存储空间,要在分类中添加一个弱引用属性通常需要结合使用关联对象(Associated Object)和一些额外的技巧来实现。下面是一个实现弱引用属性的步骤:
方法一:定义一个中间对象
- 定义一个中间对象
由于objc_setAssociatedObject
不直接支持weak
关联,你可以通过创建一个中间对象来持有实际的弱引用。这个中间对象将有一个weak
属性,用于指向你想要弱引用的对象。
@interface WeakReferenceObject : NSObject
@property (nonatomic, weak) id target;
@end@implementation WeakReferenceObject
@end
- 在分类中使用关联对象
在分类中,使用objc_setAssociatedObject
和objc_getAssociatedObject
来分别设置和获取这个中间对象,从而间接实现了一个弱引用的属性。
#import <objc/runtime.h>
@interface NSObject (MyCategory)
//弱引用,相当于自己的weakCar
@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,即weakCarreturn weakReference.target;
}@end
注意事项:
使用这种方法时,需要注意内存管理和潜在的循环引用问题。由于是通过关联对象机制实现的,还是要确保关联策略正确选择,以避免内存泄露。在这个例子中,我们使用的是
OBJC_ASSOCIATION_RETAIN_NONATOMIC
,因为我们希望保持对中间WeakReferenceObject
对象的强引用,而WeakReferenceObject
则对目标对象保持弱引用。
通过这种方式,虽然稍显复杂,但可以在分类中成功添加一个表现为弱引用的属性。
方法二:使用__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赋值,返回的是弱指针指向的weakPropertyYZCar *(^myWeakCarBlock)(void) = ^{ return weakProperty; };//关联对象保存blockobjc_setAssociatedObject(self, @selector(myWeakCarBlock), myWeakCarBlock, OBJC_ASSOCIATION_COPY);
}- (YZCar *)weakCar
{//获取关联对象的blockYZCar *(^myWeakCarBlock)(void) = objc_getAssociatedObject(self, @selector(myWeakCarBlock));//block调用,就是返回值,weakCarreturn 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