关联对象的作用
在分类里面,不可以直接为分类添加属性
在代理中,不可以直接为代理添加属性
在普通类中,@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:));
}
@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
关联,这将增加运行时的复杂度和性能负担。
问:如果想实现一个weak属性,怎么做?
在iOS中,由于分类(Category)本身不支持直接添加属性的存储空间,要在分类中添加一个弱引用属性通常需要结合使用关联对象(Associated Object)和一些额外的技巧来实现。下面是一个实现弱引用属性的步骤:
方法一:定义一个中间对象
- 定义一个中间对象
由于objc_setAssociatedObject
不直接支持weak
关联,你可以通过创建一个中间对象来持有实际的弱引用。这个中间对象将有一个weak
属性,用于指向你想要弱引用的对象。
@interface WeakReferenceObject : NSObject
//此处需要用weak,就是我们的目标值属性
@property (nonatomic, weak) id target;
@end
@implementation WeakReferenceObject
@end
- 在分类中使用关联对象
在分类中,使用objc_setAssociatedObject
和objc_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