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 。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容

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