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 。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • *面试心声:其实这些题本人都没怎么背,但是在上海 两周半 面了大约10家 收到差不多3个offer,总结起来就是把...
    Dove_iOS阅读 27,210评论 30 471
  • 内存管理 简述OC中内存管理机制。与retain配对使用的方法是dealloc还是release,为什么?需要与a...
    丶逐渐阅读 2,008评论 1 16
  • 1.OC里用到集合类是什么? 基本类型为:NSArray,NSSet以及NSDictionary 可变类型为:NS...
    轻皱眉头浅忧思阅读 1,394评论 0 3
  • 37.cocoa内存管理规则 1)当你使用new,alloc或copy方法创建一个对象时,该对象的保留计数器值为1...
    如风家的秘密阅读 887评论 0 4
  • 一片云,一阵风,一抹残阳。 一道河,一座桥,一处人家。 别问我的脚步走的多快, 只等你来的时间有多久。 一支笔,一...
    睿阳公子阅读 481评论 0 4