什么是循环引用?
顾名思义, 就是几个对象某种方式互相引用, 形成了"环"。由于 Objective-C 内存管理使用引用计数的架构, 而并不是 GC(garbage collector), 而在 ARC(自动引用计数) 下所有 OC 对象的内存都交由系统统一管理。在 ARC 下 retain
、rerlease
、autorelease
、dealloc
都无法被调用, 因为 ARC 要分析何处应该自动调用内存管理方法, 如果手动调用的话会干扰其工作。更多关于内存管理的内容我会在之后的文章解答。
两个或两个以上对象彼此强引用而形成循环应用
循环引用中只剩一个对象还引用产生循环引用的某个对象
移除此引用后 ABCD 四个对象所造成的循环引用就泄露了
那么在 ARC 下经常产生循环引用的就只有三种情况了:
Delegate:
在声明 delegate 的时候, 使用 retain
、strong
、copy
等强引用属性关键字修饰时, 会导致代理方拥有被代理方的引用, 被代理方又通过 delegate 拥有了代理方的引用, 这样就造成了循环引用。
解决方式就是在 ARC 下将关键字改为 weak 即可。
Block:
有几种我们常见的 block 的使用:
1、类方法不会造成循环引用, 因为类不会持有对象
[UIView animateWithDuration:2 animations:^{
}];
2、self 并没有对 block 进行引用, 只是 block 对 self 单方面引用, 所以没有造成循环引用
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
});
3、在某个作用域内创建对象并且的 block 回调调用 self, self 没有持有该对象, 没有造成循环引用
TestObject *test = [[TestObject alloc] init];
[test somethingBlock:^{
[self doSomething];
}];
4、self 强引用了 object, object 又强引用了 block, 而在 block 回调里又调用了 self, 导致 block 强引用 self, 造成循环引用, 导致 self 无法被释放
[self.object somethingBlock:^{
[self doSomething];
}];
通常会做如下处理:
// 弱引用 self 这个对象
__weak ViewController *weakSelf = self;
[self.object somethingBlock:^{
// 捕获 weakSelf 这个引用(由于 __weak 修饰的都是在栈内, 有可能被系统释放, 导致 block 内使用 weakSelf 调用的代码无效)
__strong ViewController *strongSelf = weakSelf;
[strongSelf doSomething];
};
也可以根据不同应用场景做不同的处理:
- 当 object 不再使用时可以主动置为 nil, 从而打破循环引用。 如果 block 声明为属性, 也可以将属性主动置为 nil, 也可打破循环引用。
[self.object somethingBlock:^{
[self doSomething];
self.object = nil;
}];
- 如果某类内部将 block 作为私有属性保存并使用, 当 block 后续不会再被使用到时, 可以主动将置为 nil, 从内部打破循环引用。
下面是某类的具体实现, 内部有一私有属性将 block 捕获, 使用 somethingBlock
做一系列事情后, 将 block 回调。
#import "TestObject.h"
@interface TestObject ()
@property (nonatomic, copy) void(^somethingBlock)(void);
@end
@implementation TestObject
- (void)somethingBlock:(void(^)(void))block {
_somethingBlock = block;
// 使用 _somethingBlock 做一些事情
!_somethingBlock ? : _somethingBlock();
_somethingBlock = nil;
}
@end
于是是使用此类的代码就可以这样写:
[self.object somethingBlock:^{
[self doSomething];
}];
NSTimer:
当使用 NSTimer
定时器时, 定时器会强引用 target
, 等自身失效时再释放此对象。执行完相关任务后, 没有循环的定时器会自动失效, 但是如果需要循环的定时器, 则需要调用 - (void)invalidate;
使定时器失效。
由于定时器会保留目标对象, 所有循环执行任务的时候通常会导致循环引用, 先看下面代码:
@interface RepeatTimer ()
- (void)startTimer;
- (void)stopTimer;
@end
@implementation RepeatTimer {
NSTimer *_repeatTimer;
}
- (id)init {
return [super init];
}
- (void)dealloc {
[_repeatTimer invalidate];
}
- (void)startTimer {
_repeatTimer = [NSTimer scheduledTimerWithTimeInterval:5
target:self
selector:@selector(doSomething)
userInfo:nil
repeats:YES];
}
- (void)stopTimer {
[_repeatTimer invalidate];
_repeatTimer = nil;
}
- (void)doSomething {
}
当使用者创建了 RepeatTimer
的对象并且调用 - (void)startTimer
后, startTimer
内部实现将 RepeatTimer
的对象自身传入 NSTimer
, 使得 NSTimer
保留了此对象, 而 RepeatTimer
内部有持有了 NSTimer
的对象, 造成了循环引用, 只有当使用者调用 - (void)stopTimer
时, 才可以打破循环引用。
除非使用该类的代码完全在你的掌控之中, 否则没有办法保证其他在开发人员一定会调用 - (void)stopTimer
方法, 所以这并不是一个很好的解决方案。此外如果想在系统回收该类时令定时器无效也是没有用的, 因为 NSTimer
和 RepeatTimer
在相互引用, 所以 RepeatTimer
的对象绝对不会被释放。 当指向 RepeatTimer
实例的最后一个外部引用移走之后, 除了 NSTimer
再无其它类在对其保持引用, 也就是说该实例已经"丢失"了, 并永远不会被释放。
- 可以添加一个中介者
target
来绑定selector
, 之后在dealloc
中释放该 timer 即可:
@interface ViewController ()
@property (nonatomic, strong) id target;
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_target = [NSObject new];
IMP imp = class_getMethodImplementation([self class], @selector(doSomething));
class_addMethod([_target class], @selector(doSomething), imp, "v@:");
_timer = [NSTimer scheduledTimerWithTimeInterval:5.0
target:_target
selector:@selector(doSomething)
userInfo:nil
repeats:YES];
}
- (void)dealloc {
[_timer invalidate];
_timer = nil;
}
- 也可以创建一个
NSProxy
虚类的对象去解决这个问题:
@interface TimerProxy : NSProxy
@property (nonatomic, weak) id target;
@end
@implementation TimerProxy
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.target];
}
@end
在使用的地方将其alloc
, 绑定代理的对象target
即可
@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, strong) TimerProxy *timerProxy;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_timerProxy = [TimerProxy alloc];
_timerProxy.target = self;
_timer = [NSTimer scheduledTimerWithTimeInterval:5.0
target:_timerProxy
selector:@selector(doSomething)
userInfo:nil
repeats:YES];
}
- (void)dealloc {
[_timer invalidate];
_timer = nil;
}
上面代码利用消息转发来断开NSTimer
对象与视图之间的引用关系。
- 当然为
NSTimer
添加一个 category, 增加一个带有 block 的方法来解决此问题更为直观:
@interface NSTimer (RepeatBlockTimer)
+ (NSTimer *)scheduledMyTimerWithTimeInterval:(NSTimeInterval)timeInterval
repeats:(BOOL)repeats
block:(void(^)(void))block;
@end
@implementation NSTimer (RepeatBlockTimer)
+ (NSTimer *)scheduledMyTimerWithTimeInterval:(NSTimeInterval)timeInterval
repeats:(BOOL)repeats
block:(void(^)(void))block {
return [self scheduledTimerWithTimeInterval:timeInterval
target:self
selector:@selector(blockInvoke:)
userInfo:[block copy]
repeats:repeats];
}
+ (void)blockInvoke:(NSTimer *)timer {
void (^block)(void) = timer.userInfo;
!block ? : block();
}
@end
上面代码将定时器所执行的任务封装成 block, 在调用定时器的时候作为 userInfo
的参数传进去 , 传入时将 block 拷贝的堆上, 否则稍后执行它的时候, 该 block 可能已经无效。定时器现在的 target
是 NSTimer
的对象, 这是个单例, 所以不需要关心定时器是否会保留它。 不过此处依然有循环引用, 不过因为类对象是不需要回收的, 所以不考虑。
然后在之前 - (void)stopTimer
里做如下修改:
- (void)startTimer {
__weak typeof(self) weakSelf = self;
_repeatTimer = [NSTimer scheduledMyTimerWithTimeInterval:5
repeats:self
block:^(void) {
RepeatTimer *strongSelf = weakSelf;
[strongSelf doSomething];
}];
}
使用 __weak
定义一个弱引用指向 self, 在 block 内部捕获这个引用。这样做的好处是保证 self 不会被定时器所引用, 保证实例(也就是捕获的引用)在执行期间持续存活。
这样在外部指向 RepeatTimer
的引用为0时, 该实例对象就会被回收, 同时会停止定时器循环所做的操作。
不过 iOS 在 10.0 以后系统已经提供了此方法:
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block
在使用时只需下面这样就可以了
- (void)startTimer {
__weak typeof(self) weakSelf = self;
_repeatTimer = [NSTimer scheduledTimerWithTimeInterval:5 repeats:self block:^(NSTimer *timer) {
RepeatTimer *strongSelf = weakSelf;
[strongSelf doSomething];
}];
}