如何优雅的处理循环引用(retain cycle)

什么是循环引用?

顾名思义, 就是几个对象某种方式互相引用, 形成了"环"。由于 Objective-C 内存管理使用引用计数的架构, 而并不是 GC(garbage collector), 而在 ARC(自动引用计数) 下所有 OC 对象的内存都交由系统统一管理。在 ARC 下 retainrerleaseautoreleasedealloc 都无法被调用, 因为 ARC 要分析何处应该自动调用内存管理方法, 如果手动调用的话会干扰其工作。更多关于内存管理的内容我会在之后的文章解答。

两个或两个以上对象彼此强引用而形成循环应用


  两个或两个以上对象彼此强引用而形成循环应用

循环引用中只剩一个对象还引用产生循环引用的某个对象

  
  循环引用中只剩一个对象还引用产生循环引用的某个对象
  

  
移除此引用后 ABCD 四个对象所造成的循环引用就泄露了

  
  移除此引用后 ABCD 四个对象所造成的循环引用就泄露了
  
那么在 ARC 下经常产生循环引用的就只有三种情况了:

Delegate:

在声明 delegate 的时候, 使用 retainstrongcopy 等强引用属性关键字修饰时, 会导致代理方拥有被代理方的引用, 被代理方又通过 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 方法, 所以这并不是一个很好的解决方案。此外如果想在系统回收该类时令定时器无效也是没有用的, 因为 NSTimerRepeatTimer 在相互引用, 所以 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 可能已经无效。定时器现在的 targetNSTimer 的对象, 这是个单例, 所以不需要关心定时器是否会保留它。 不过此处依然有循环引用, 不过因为类对象是不需要回收的, 所以不考虑。
  然后在之前 - (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];
    }];
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容

  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,321评论 8 265
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,089评论 1 32
  • OC语言基础 1.类与对象 类方法 OC的类方法只有2种:静态方法和实例方法两种 在OC中,只要方法声明在@int...
    奇异果好补阅读 4,250评论 0 11
  • 第一条 总则:为严明纪律,奖惩分明,调动员工工作积极性,提高工作效率和经济效率;本着公平竞争,公正管理的原则,进一...
    西风冽阅读 9,592评论 0 6
  • 立春,是二十四节气之首。 立春是中国民间重要的传统节之一。“立”是“开始”的意思,自秦代以来,中国就一直以立春作为...
    聿靈隨筆阅读 1,049评论 0 1