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

96
__block
2018.06.13 11:29 字数 1271

什么是循环引用?

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

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

循环引用中只剩一个对象还引用产生循环引用的某个对象
移除此引用后 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 调用的代码无效)
    // 假设某 View 加载在 ViewController 上, 需要在 block 内重新布局, 而执行到 block 时 ViewController 已经销毁, 也就是说 View 没有重新布局的必要, 这种情况就不需要捕获 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 再无其它类在对其保持引用, 也就是说该实例已经"丢失"了, 并永远不会被释放。
  可以为 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];
    }];
}
日记本
Web note ad 1