循环引用
文章目录
- 循环引用
- 1.自循环引用
- 2.相互循环引用
- 3.多循环引用
- 常见的循环引用问题
- 1.delegate
- 解决方法:
- 2.block
- 解决方法:
- 1.强弱共舞
- 2.把当前类作为block的参数
- 3.用__block修饰变量,在block内部置nil
- 3.NSTimer
- 解决方案:
- 1.使用中间类
- 2.使用类方法
- 3.使用 weakProxy
- 4.改用block类型的定时器API
对象A和对象B,相互引用了对方作为自己的成员变量,只有当自己销毁时,才会将成员变量的引用计数减1,这就导致了A的销毁依赖于B的销毁,同样B的销毁依赖于A的销毁,这样就造成了循环引用问题。
1.自循环引用
假如有一个对象,内部强持有它的成员变量obj,若此时我们给obj赋值为原对象时,就是自循环引用。
2.相互循环引用
对象A内部强持有obj,对象B内部强持有obj,若此时对象A的obj指向对象B,同时对象B中的obj指向对象A,就是相互引用。
3.多循环引用
假如类中有对象1…对象N,每个对象中都强持有一个obj,若每个对象的obj都指向下个对象,就产生了多循环引用。
常见的循环引用问题
1.delegate
我们平时经常用的协议传值,如果我们委托方的delegate属性使用strong强引用,就会造成代理方和委托方互相强引用出现循环引用问题。代理方强引用委托方对象,并且委托方对象中的delegate属性又强引用代理方对象,这就造成了循环引用问题。
@property (nonatomic, strong) id <MyDelegate> delegate;
解决方法:
需要将委托方的delegate属性改为weak修饰就行了,这样委托方的delegate就不会强引用代理方对象了,简单解决了这个循环引用问题。
@property (nonatomic, weak) id <MyDelegate> delegate;
2.block
不是所有的block都会循环引用,我们需要判断什么情况会循环引用,xcode会给出warning:
//self -> reachabilityManager -> block -> self 都是循环引用self.reachabilityManager.stateBlock = ^(int number){NSLog(@"%@",self. reachabilityManager);};
//或者(block内部没有显式地出现"self",只要你在block里用到了self所拥有的东西,一样会出现循环引用!)self.reachabilityManager.stateBlock = ^(int number){NSLog(@"%@",_ reachabilityManager);};
要判断block是否造成了循环引用,我们要看block中的引用的变量和block外部引用block的变量会不会形成一个强引用的闭环,以此来判断block是否造成了循环引用的问题。
解决方法:
1.强弱共舞
解决它其实很简单,无非就是self
引用了block
,block
又引用了self
,让他们其中一个使用weak
修饰不就行了:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{[self dismissViewControllerAnimated:YES completion:nil];__weak typeof(self) weakSelf = self;void (^Block) (void) = ^{dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{NSLog(@"%@", weakSelf);});};Block();
}
但是仅仅使用__weak
修饰self
存在一个缺陷:__weak
可能会导致内存提前回收weakSelf
,在未执行NSLog()
时,weakSelf
就已经被释放了(例如上述情况),然后执行NSLog()
时就打印(null)。
所以为了解决这个缺陷,我们需要这样在block
内部再用__strong
去修饰weakSelf:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{[self dismissViewControllerAnimated:YES completion:nil];__weak typeof(self) weakSelf = self;void (^Block) (void) = ^{__strong typeof(self) strongSelf = weakSelf;dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{NSLog(@"%@", strongSelf);});};Block();
}
我们发现上述这个方法确实解决所有问题,但是可能会有两个不理解的点:
即使用weakSelf
又使用strongSelf
,这么做和直接用self有什么区别?为什么不会有循环引用?这是因为block
外部的weakSelf
是为了打破环循环引用,而block
内部的strongSelf
是为了防止weakSelf
被提前释放,strongSelf
仅仅是block中的局部变量,在block
执行结束后被回收,不会再造成循环引用。
这么做和使用weakSelf
有什么区别?唯一的区别就是多了一个strongSelf
,而这里的strongSelf
会使self
的引用计数+1,使得self
只有在block
执行完,局部的strongSelf
被回收后,self
才会dealloc
。
2.把当前类作为block的参数
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {void (^Block) (ViewController *) = ^(ViewController *vc){dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{NSLog(@"%@", vc);});};Block(self);
}
3.用__block修饰变量,在block内部置nil
__block Person *person = [[Person alloc] init];person.age = 20;person.newblock = ^{NSLog(@"%d", person.age);person = nil;};person.newblock();
3.NSTimer
NSTimer
也会出现很多经典的循环引用问题:
- (void)viewDidLoad {[super viewDidLoad];self.myTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doSomething) userInfo:nil repeats:YES];
}- (void)doSomething {}- (void)dealloc {[self.myTimer invalidate];self.myTimer = nil;
}
这个问题用上面两种的weak
就无法解决,因为不管是weakSelf
还是strongSelf
,最终Runloop
强引用NSTimer
其也就间接的强引用了对象,结果就会导致循环引用。
两种解决方法:
- 让视图控制器对
NSTimer
的引用变成弱引用 - 让
NSTimer
对视图控制器的引用变成弱引用
第一种方法如果控制器对NSTimer
的引用改为弱引用,则会出现NSTimer
直接被回收,所以不可使,因此我们只能从第二种方法入手。
解决方案:
1.使用中间类
创建一个继承NSObject
的子类MyTimerTarget
,并创建开启计时器的方法。
// MyTimerTarget.h
#import <Foundation/Foundation.h>
@interface MyTimerTarget : NSObject
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval target:(id)target selector:(SEL)selector userInfo:(id)userInfo repeats:(BOOL)repeats;
@end// MyTimerTarget.m
#import "MyTimerTarget.h"
@interface MyTimerTarget ()
@property (assign, nonatomic) SEL outSelector;
@property (weak, nonatomic) id outTarget;
@end@implementation MyTimerTarget
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval target:(id)target selector:(SEL)selector userInfo:(id)userInfo repeats:(BOOL)repeats {MyTimerTarget *timerTarget = [[MyTimerTarget alloc] init];timerTarget.outTarget = target;timerTarget.outSelector = selector;NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:interval target:timerTarget selector:@selector(timerSelector:) userInfo:userInfo repeats:repeats];return timer;
}
- (void)timerSelector:(NSTimer *)timer {if (self.outTarget && [self.outTarget respondsToSelector:self.outSelector]) {[self.outTarget performSelector:self.outSelector withObject:timer.userInfo];} else {[timer invalidate];}
}
@end// 调用方
@interface ViewController ()
@property (nonatomic, strong) NSTimer *myTimer;
@end@implementation ViewController
- (void)viewDidLoad {[super viewDidLoad];self.myTimer = [MyTimerTarget scheduledTimerWithTimeInterval:1 target:self selector:@selector(doSomething) userInfo:nil repeats:YES];
}- (void)doSomething {}- (void)dealloc {NSLog(@"MyViewController dealloc");
}
@end
VC
强引用timer
,因为timer
的target
是MyTimerTarget
实例,所以timer
强引用MyTimerTarget
实例,而MyTimerTarget
实例弱引用VC
,解除循环引用。这种方案VC
在退出时都不用管timer
,因为自己释放后自然会触发timerSelector:
中的[timer invalidate]
逻辑,timer
也会被释放。
2.使用类方法
我们还可以对NSTimer
做一个category
,通过block
将 timer
的target
和selector
绑定到一个类方法上,来实现解除循环引用:
// NSTimer+MyUtil.h
#import <Foundation/Foundation.h>
@interface NSTimer (MyUtil)
+ (NSTimer*)MyUtil_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void(^)(void))block repeats:(BOOL)repeats;
@end// NSTimer+MyUtil.m
#import "NSTimer+MyUtil.h"
@implementation NSTimer (MyUtil)
+ (NSTimer *)MyUtil_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void(^)(void))block repeats:(BOOL)repeats {return [self scheduledTimerWithTimeInterval:interval target:self selector:@selector(MyUtil_blockInvoke:) userInfo:[block copy] repeats:repeats];
}+ (void)MyUtil_blockInvoke:(NSTimer *)timer {void (^block)(void) = timer.userInfo;if (block) {block();}
}
@end// 调用方
@interface ViewController ()
@property (nonatomic, strong) NSTimer *myTimer;
@end
@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];self.myTimer = [NSTimer MyUtil_scheduledTimerWithTimeInterval:1 block:^{NSLog(@"doSomething");} repeats:YES];
}
- (void)dealloc {if (_myTimer) {[_myTimer invalidate];}NSLog(@"MyViewController dealloc");
}
@end
这种方案下,VC强引用timer
,但是不会被timer
强引用,但有个问题是VC退出被释放时,如果要停掉timer
需要自己调用一下timer
的invalidate
方法。
3.使用 weakProxy
创建一个继承NSProxy
的子类MyProxy
,并实现消息转发的相关方法。NSProxy
是iOS开发中一个消息转发的基类,它不继承自NSObject
。因为他也是Foundation
框架中的基类,通常用来实现消息转发,我们可以用它来包装NSTimer
的target
,达到弱引用的效果。
//TestNSproxy.h中:
#import <Foundation/Foundation.h>NS_ASSUME_NONNULL_BEGIN@interface TestNSproxy : NSProxy@property (nonatomic, weak) id target;+ (instancetype)proxyWithTarget:(id)target;@endNS_ASSUME_NONNULL_END//TestNSproxy.m中:
#import "TestNSproxy.h"@implementation TestNSproxy//实现我们的类方法,创建一个TestNSproxy类型的对象,然后将传入的参数作为target便于后续的消息转发
+ (instancetype)proxyWithTarget:(id)target {TestNSproxy *proxy = [TestNSproxy alloc];proxy.target = target;return proxy;
}//下面实现第三次消息拯救中的那两个方法
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {return [self.target methodSignatureForSelector:sel];
}- (void)forwardInvocation:(NSInvocation *)invocation {[invocation invokeWithTarget:self.target];
}@end//作为中间类作为timer的target的时候:
//只需要调用TestProxy类的类方法时,将参数传成当前界面的self即可
_timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[TestNSproxy proxyWithTarget:self] selector:@selector(timerSelector) userInfo:nil repeats:YES];
4.改用block类型的定时器API
结合self
强弱共舞的方法来解决循环引用问题,使得定时器可以及时释放。而且对于CADisplayLink
和NSTimer
来说,无论外面传递的target
是弱指针还是强指针,都会传入一个内存地址,定时器内部都是对这个内存地址产生强引用,所以传递弱指针没有用。
// 让 timer 对self 产生弱引用__weak typeof(self) weakSelf = self;self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {[weakSelf test];}];