IOS解决循环引用
虽然目前IOS都普遍使用了ARC开发,但是还是有一些情况下是必然存在循环引用的,为了更好的说明循环引用。先以MRC的代码来描述一些内存关系。
@interface ViewController ()
@end
@implementation ViewController
-
(void)open{
//av 引用计数 1
AViewController *av = [[AViewController alloc] init];
// presentViewController的时候 AViewController引用计数 2
[self presentViewController:av animated:YES completion:^{
}];
// 这里释放一次 AViewController 引用计数 1
[av release];
} -
(void)addButton{
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
button.backgroundColor = [UIColor redColor];
button.frame = CGRectMake(100, 100, 100, 100);
[button addTarget:self action:@selector(open) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];
} -
(void)viewDidLoad {
[super viewDidLoad];
[self addButton];
}
@end
这里我们看到 AViewController *av 的引用计数是1, 以下是 AViewController 的代码
@implementation AViewController
-
(void)back{
// AViewController 引用计数-1 (正常情况的话会形成 0)
[self dismissViewControllerAnimated:YES completion:^{
}];
} -
(void)viewDidLoad {
[super viewDidLoad];
}
@end
正常逻辑下 当我们 调用 back 的时候 这个时候 AViewController 会因为 dismissViewControllerAnimated 使得引用计数变成0,然后完成正常的内存释放。
这个时候我们入一个 BClass 先看 BClass的定义
@protocol BClassDelegate <NSObject>
- (void)finished;
@end
@interface BClass : NSObject
@property (nonatomic, retain) id delegate; - (void)startAnimation;
@end
注意 delegate 是 retain 关键点来了。这种设计摆明了跟其他 委托模式不一样(参考UITableView 的 delegate 是assign[因为目前是MRC 所以不是weak])。
这个时候 在 AViewController 使用 BClass。
@interface AViewController ()
@property (nonatomic, retain) BClass *animation;
@end
-
(void)userBClass{
//tempAnimation 引用计数 1
_animation = [[BClass alloc] init];
//关键点(这里 AViewController 引用计数变成了 2)
_animation.delegate = self;
} -
(void)viewDidLoad {
[super viewDidLoad];
[self userBClass];
}
为了降低复杂度 我们先只考虑AViewController 的内存释放,先暂时放下 animation的内存释放问题。
这里重点情况是 _animation.delegate = self; 这里会使得AViewController 引用计数变成2。
看下 BClass 源码
@implementation BClass
- (void)dealloc{
NSLog(@"%s", func);
[_delegate release];
_delegate = nil;
[super dealloc];
} - (void)startAnimation{
}
// 这里传进来的 adelegate 是 AViewController - (void)setDelegate:(id)adelegate{
[_delegate release];
[adelegate retain]; //关键的一部使得这个引用计数 + 1 AViewController(引用计数变成2)
_delegate = adelegate;
}
@end
关键点是因为 delegate 设计成 retain类型,所以 _animation.delegate = self; 这句代码使得 AViewController 变成了2,
这个时候 dismissViewControllerAnimated 引用计数减1, 但是 AViewController 引用计数没有变成0,所以AViewController内存没有释放,这就造成内存泄漏。到这里给出完整BClass的代码
@interface AViewController ()
@property (nonatomic, retain) BClass *animation;
@end
@implementation AViewController
-
(void)dealloc
{
[_animation release];
_animation = nil;
[super dealloc];
} -
(void)back{
// AViewController 引用计数-1 (正常情况的话会形成 0)
[self dismissViewControllerAnimated:YES completion:^{
}];
} -
(void)userBClass{
//tempAnimation 引用计数 1
_animation = [[BClass alloc] init];
//关键点(这里 AViewController 引用计数变成了 2)
_animation.delegate = self;
}
这里因为 back的时候 我们没有办法使得AViewController引用计数变成0,所以没有调用AViewController的dealloc,所以无法
触发 [_animation release]; 进而使得 BClass也没有被释放内存。这里就是循环引用了。
解决方案可以这样考虑,只要在调用back之前我们能够使得 AViewController的引用计数由2变成1就可以了。
这个时候改写back
- (void)back{
// 先使得AViewController 引用计数-1
[_animation.delegate release];
// AViewController 引用计数-1 (正常情况的话会形成 0)
[self dismissViewControllerAnimated:YES completion:^{
}];
}
这样就可以在调用back的时候 我们的AViewController引用计数减了2次,这种写法非常丑陋,虽然能够解决问题,但是不够方便。
参考网络上有一种通过Proxy来处理这种循环引用的 先看下TestProxy的定义
@interface TestProxy : NSProxy
@property (nonatomic, assign) id target;
@end
@implementation TestProxy
-
(void)dealloc{
[super dealloc];
} -
(NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
NSMethodSignature *signature = [_target methodSignatureForSelector:selector];
return signature;
} -
(void)forwardInvocation:(NSInvocation *)invocation{
SEL sel = invocation.selector;
if (_target) {
if ([_target respondsToSelector:sel]) {
[invocation invokeWithTarget:_target];
}
}
}
@end
这里可以先忽略 methodSignatureForSelector 和 forwardInvocation 先重点放在内存泄漏上。
引入TestProxy后,改写AViewController里面的userBClass和dealloc,back
-
(void)dealloc
{
[_animation release];
[_proxy release];
[super dealloc];
} -
(void)back{
[self dismissViewControllerAnimated:YES completion:^{
}];
} -
(void)userBClass{
//_animation 引用计数1
_animation = [[BClass alloc] init];
// _proxy 引用计数1
_proxy = [TestProxy alloc];
// _proxy 引用计数2
_animation.delegate = _proxy;
}
重点是 _animation.delegate = _proxy 这里并没有引起AViewController 引用计数的改变,所以 back的时候 AViewController能正常释放内存。所以 back触发的时候 会进入 dealloc 这个时候 [_animation release]; 会使得_animation引用计数变成了0。
到这里我们能够正常释放了 AViewController和BClass了。剩下只有能解决NSProxy正常释放那么就能够解决所有的内存泄漏问题了。 目前 proxy引用计数是2,参考代码可以知道 AViewController 的 dealloc 有一次 [_proxy release], BClass里面有一个
[_delegate release], BClass里面的delegate就是_proxy 所以 TestProxy的引用计数变成0,最终 AViewController, BClass 和 TestProxy 出现的3个类都成功完成内存释放。
对比原来非常粗暴的back函数里面调用release来解决内存问题,另一种通过引入 TestProxy 把释放内存的时机放到了 AViewController 的 dealloc。一般情况建议大家这样引用TestProxy, 因为这样写法不需要考虑内存释放的时机的。同理NSTimer这种也可以引入Proxy,然后在 dealloc 调用 invalidate即可。
总结:循环引用计数的关键点在于 setDelegate方法, aaa.delegate = self; aaa.delegate = nil 。